> ## Content Index
> Fetch the complete content index at: https://blog.coopnet.us/llms.txt
> Use this file to discover other available public pages before exploring further.

# sshd -t fails with 'Missing privilege separation directory: /run/sshd' — and quietly undoes your hardening
- URL: https://blog.coopnet.us/sshd-t-missing-run-sshd/
- Published: 2026-07-17T06:00:00.000Z
- Updated: 2026-10-18T02:30:46.000Z
- Description: Your sshd config is fine, but `sshd -t` exits non-zero anyway — and if you gate a config rollback on that check, you silently revert the hardening you were trying to apply. Here is why, and the one-line fix.
- Author: Renstur

Here is a good habit that bit me: before reloading `sshd` with a new config, validate it with `sshd -t`, and wrap the whole thing so a bad config gets rolled back instead of locking you out. Solid instinct. Except on Debian and Ubuntu, `sshd -t` can fail *even when your config is perfectly valid* — and if your automation treats "validation failed" as "roll it back," you throw away the exact change you were making.

## The symptom

You drop in a hardening snippet, validate, and get:

```text
$ sshd -t
Missing privilege separation directory: /run/sshd
$ echo $?
255

```

Non-zero exit, and the only thing on stderr is that one line. Notice what is *not* there: any complaint about your actual config. That is the tell — `sshd -t` bailed out before it ever looked at your settings.

## Why it happens

`/run/sshd` is the **privilege separation directory**. On Debian/Ubuntu it lives under `/run`, which is a `tmpfs` — it does not survive reboots and is created at runtime, either by the `ssh.service` unit (`RuntimeDirectory=sshd`) or by `systemd-tmpfiles`, when sshd actually starts.

`sshd -t` checks that this directory exists before doing anything else. So in any context where sshd is not currently running with that dir in place, the check fails first:

- inside a container image,
- on a freshly booted host before the ssh unit has started,
- or — in my case — during an automation run on a host where `/run` had been recreated.

It is not a config error. It is a missing directory that the test happens to require.

## The fix

Create the directory before you validate:

```sh
mkdir -p /run/sshd    # or: install -d -m0755 /run/sshd
sshd -t

```

In Ansible, that is one task ahead of the validation:

```yaml
- name: Ensure the sshd privilege-separation dir exists (sshd -t needs it)
  ansible.builtin.file:
    path: /run/sshd
    state: directory
    mode: "0755"

- name: Validate the full sshd config
  ansible.builtin.command: sshd -t
  changed_when: false

```

`mkdir -p` on an already-present directory is a no-op, so this is safe to run every time.

## The part that actually bites

The failure mode is not the error — it is what you *do* with the error. A safe pattern for editing sshd config is:

1. write the drop-in,
2. `sshd -t`,
3. if it fails, **remove the drop-in and abort** (so you never reload a config that would lock you out),
4. if it passes, `systemctl reload sshd`.

That rollback in step 3 is exactly right for a genuinely broken config. But when `sshd -t` false-fails on the missing `/run/sshd`, step 3 fires anyway and *silently deletes your hardening* — the run reports success, and the host is left exactly as unhardened as before. You only notice weeks later when you check whether `PasswordAuthentication no` actually took.

Same caveat applies to the `validate:` parameter on Ansible's `copy`/`template` modules for the main `sshd_config` — it runs `sshd -t -f %s`, which needs `/run/sshd` too.

Keep validating before you reload — that habit is what stops you locking yourself out. Just make sure the privilege-separation directory exists first, so the check is testing your *config* and not the state of `tmpfs`.