sshd -t fails with 'Missing privilege separation directory: /run/sshd' — and quietly undoes your hardening


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:

$ 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:

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

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

- 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.