Handbook sections

Hardening SSH

Last reviewed

SSH is the front door to a server. Harden the daemon so a remote foothold can't become full control, and verify the effective config, not the file.

The threat: the front door, brute-forced and backdoored

SSH is usually the only remote-admin path to a server, and it is exposed to the network. That makes it the prime target: continuous brute-force and credential-stuffing, abuse of permissive settings (direct root login, password auth, TCP tunneling), and outright supply-chain attacks on the daemon itself. The 2024 XZ Utils backdoor (CVE-2024-3094) was built specifically to subvert sshd authentication, and regreSSHion (CVE-2024-6387) showed a pre-auth RCE in sshd itself. A weak SSH configuration turns a single stolen credential into full system control.

Why harden it

The blast radius is the whole machine. One brute-forced root password or one leaked key, and the attacker is the admin. Hardening SSH shrinks the attack surface and forces every connection through accountable, least-privileged paths, raising both the cost of an attack and the chance of detecting it.

What Pavois audits: the effective sshd -T config

Pavois reads the effective configuration with sshd -T, the daemon's resolved settings, so it evaluates the Includes and drop-ins under /etc/ssh/sshd_config.d/ exactly as sshd applies them, where a file-based scan (OVAL/oscap) would miss them. Across its SSH rules it checks: root login disabled, password and empty-password auth off, strong Ciphers/MACs/KexAlgorithms, MaxAuthTries, LoginGraceTime, idle timeout, the forwarding options, PermitUserEnvironment, the warning banner and LogLevel. This is the differentiator: a control reading /etc/ssh/sshd_config alone is fooled by a drop-in that flips the value back.

The essential directives

Directive Hardened value Why
PermitRootLogin no no direct root; named account then sudo, attributable
PasswordAuthentication no keys only (set this only after a key works)
KbdInteractiveAuthentication no closes keyboard/PAM interactive auth
PermitEmptyPasswords no refuse any account with no password
AllowUsers / AllowGroups explicit list allowlist the accounts that may log in
MaxAuthTries 3 fewer guesses per connection (default 6)
LoginGraceTime 30 short auth window, fewer pending connections
X11Forwarding / AllowTcpForwarding / AllowAgentForwarding no drop unused pivot/tunnel vectors
ClientAliveInterval / ClientAliveCountMax 300 / 2 close idle sessions
LogLevel VERBOSE log key fingerprints for accountability

Modern ciphers, MACs and key exchange

Keep only modern algorithms and drop the legacy ones. Ban: *-cbc, arcfour*, blowfish, 3des-cbc; diffie-hellman-group1-sha1, *-group14-sha1, *-sha1; hmac-md5*, hmac-sha1* and every non-ETM MAC; ssh-dss. Prefer (converges Mozilla, ssh-audit and ANSSI): KexAlgorithms with sntrup761x25519-sha512@openssh.com (post-quantum, default since OpenSSH 9.0) and curve25519-sha256; Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com; MACs in -etm only; HostKeyAlgorithms ssh-ed25519,rsa-sha2-512; RequiredRSASize 3072. Check your binary actually supports an algorithm before pasting it: ssh -Q kex ; ssh -Q cipher ; ssh -Q mac. ssh-audit grades the live result.

How do you apply it without locking yourself out?

This is where people cut their own access. The four-step reflex, never skipped:

  1. Validate syntax: sshd -t (it does not test access logic).
  2. Read the effective config: sshd -T | grep -iE 'permitroot|passwordauth|allowusers|ciphers'.
  3. Reload, do not restart: systemctl reload ssh (sshd on RHEL) keeps your current session alive.
  4. Test in a second session while keeping the first open; on a remote host, keep a KVM/serial console as backup.

sshd -t only checks syntax: you can have a green sshd -t and still lock yourself out (a bad AllowUsers, or PasswordAuthentication no with no key deployed). Hence sshd -T and the second session.

The drop-in trap

On Debian 12, Ubuntu and RHEL 9, /etc/ssh/sshd_config starts with Include /etc/ssh/sshd_config.d/*.conf, and for most directives SSH keeps the first value seen. So a file in sshd_config.d/ (read first via the Include) overrides what you write lower in the main file. The classic trap: on a cloud image, 50-cloud-init.conf often re-enables PasswordAuthentication yes and cancels your hardening. Two rules: put your settings in /etc/ssh/sshd_config.d/00-hardening.conf (the 00- prefix wins lexical order), and always verify with sshd -T, never by re-reading sshd_config.

Pitfalls

  • Cloud images re-enable password auth via 50-cloud-init.conf. Override it with 00-hardening.conf and confirm with sshd -T.
  • Match blocks change the effective value per context: check them with sshd -T -C user=...,host=...,addr=....
  • reload, not restart: a blind restart plus a bad rule equals an unreachable server.
  • PasswordAuthentication no before a key is deployed locks everyone out.
  • A post-quantum KexAlgorithm on OpenSSH < 9.0 makes sshd -t fail; remove it on old daemons.

FAQ

Why does Pavois use sshd -T instead of reading sshd_config? Because sshd -T is the resolved config the daemon enforces, including every Include and drop-in; a file scan misses an override and reports a false pass.

I hardened sshd_config but sshd -T still shows the weak value. Why? A drop-in under sshd_config.d/ (often 50-cloud-init.conf) is read first and wins. Put your settings in 00-hardening.conf.

Restart or reload? reload. It applies the new config without killing your current session, so a mistake does not lock you out immediately.