The sshd_config Hardening Reference: Every Directive That Matters, With the Value and the Why

An SSH server ships with sensible-but-permissive defaults, and the gap between those defaults and a hardened configuration is where most estates quietly carry risk. This is a directive-by-directive reference for the sshd_config settings that actually change your security posture — the value to set, and the reason to set it — for OpenSSH 9.6.

Almost every hardening guide hands you a config blob to paste and never explains a line of it. That is how you end up with a server that rejects your CI runner or breaks a legacy integration at 2am, and nobody can say why, because nobody understood what was copied. The point of a reference is the opposite: configure from understanding, so that when something breaks you know which directive did it and whether loosening it costs you anything. What follows is grouped the way you actually reason about a server — who can authenticate, with what cryptography, from where, and how hard it is to brute-force.

All defaults below are the OpenSSH upstream defaults as of 9.6 (released December 2023). Note that distributions frequently override these in the shipped /etc/ssh/sshd_config or in drop-ins under /etc/ssh/sshd_config.d/, so verify with sshd -T on the host rather than trusting the manual page. Run sshd -t to validate syntax before you reload, every time.

Free · 4 minutes

Would you survive contact with a determined attacker — or an auditor?

Fourteen questions on access, patching, detection, and recovery — the basics that prevent most real incidents, and the ones most often assumed rather than verified. Banded finding on screen, full sheet by email.

Authentication: who gets to try

PermitRootLogin — default prohibit-password (since 7.0). Set it to no. prohibit-password still allows key-based root login; no forces everyone through a named account and sudo, which is what your audit trail wants. If a break-glass root key is genuinely required, scope it in a Match block rather than opening it globally.

PasswordAuthentication — default yes. Set it to no. This is the single highest-value change on the list: it removes the entire class of credential-stuffing and brute-force attacks that make up the bulk of the noise in any SSH log. It presupposes you have working key-based access — confirm that first, or you will lock yourself out.

PubkeyAuthentication — default yes. Leave it on. This is the mechanism you are standing on once passwords are off. Pair it with sensible AuthorizedKeysFile hygiene and, on modern estates, certificate-based auth via TrustedUserCAKeys so you rotate a CA rather than chasing keys across hosts.

KbdInteractiveAuthentication — default yes. This is the directive formerly called ChallengeResponseAuthentication, renamed in 8.7; the old name still works as an alias, which is why you see both in the wild. Set it to no unless you are deliberately using it to drive a TOTP/PAM second factor, in which case leave it on and pin the flow through AuthenticationMethods.

AuthenticationMethods — no default (single successful method suffices). This is how you require more than one factor. publickey,keyboard-interactive means a key and a challenge response must both succeed — the comma is a logical AND within a set, and a space separates alternative sets. For a jump host or anything holding production credentials, this is where two-factor SSH actually lives, not in a bolt-on. The same least-privilege instinct that governs how you scope and rotate API keys applies here: make the strong path the only path.

Cryptography: what the handshake negotiates

The defaults here are already good in 9.6 — this release also shipped the strict key-exchange protocol extension that mitigates the Terrapin attack (CVE-2023-48795). You are not fixing something broken; you are trimming the negotiable set down to the algorithms you are prepared to defend, so a downgrade never lands on something weaker than you intended.

KexAlgorithms — controls key exchange. The 9.6 default already leads with sntrup761x25519-sha512@openssh.com, a post-quantum hybrid, followed by the Curve25519 and ECDH families. Hardening means removing the older diffie-hellman-group14-sha256 and any SHA-1 group exchange from the offered list, leaving the hybrid PQ exchange and curve25519-sha256 variants. Explicit is better than default here because defaults drift between versions.

Ciphers — the symmetric cipher for the session. Restrict to the AEAD constructions: chacha20-poly1305@openssh.com and the two AES-GCM variants. Dropping the plain CTR ciphers costs you nothing against any client from the last decade and removes a downgrade target.

MACs — message authentication for non-AEAD ciphers. If you have restricted Ciphers to AEAD algorithms the MAC list is largely moot for those sessions (the cipher authenticates itself), but set it anyway for any fallback: keep only the encrypt-then-MAC (-etm) variants such as hmac-sha2-256-etm@openssh.com. Never offer bare hmac-sha1 or the 64-bit UMAC variants.

Access scope: from where, as whom

AllowUsers / AllowGroups — no default (if unset, all valid users may log in). This is your allow-list. AllowGroups ssh-users plus a managed group is more maintainable than enumerating usernames, and it means adding SSH access is a group-membership change your directory already audits. Setting one of these flips the server from default-allow to default-deny for login, which is the posture you want.

Match blocks — the conditional override. A Match block re-applies a subset of directives when its criteria (User, Group, Address, LocalPort) are met — how you grant a CI account key-only access with forwarding disabled, or require a second factor only from outside a management subnet, without weakening the global config. Keep Match blocks at the end of the file: everything after a Match line belongs to that block until the next Match, and misplacing them is the most common self-inflicted lockout.

Brute-force and resource limits

LoginGraceTime — default 120 seconds. Drop it to 30 or less. Two minutes is a generous window for an unauthenticated connection to sit open consuming a slot; shortening it reduces exposure to slow-drip and pre-auth resource attacks.

MaxAuthTries — default 6. Set it to 3. This caps authentication attempts per connection. It is not a substitute for fail2ban-style rate limiting at the network layer, but it tightens the per-connection budget an attacker gets.

MaxStartups — default 10:30:100. This throttles concurrent unauthenticated connections: start randomly dropping at 10, at a 30% probability, refusing all at 100. On an internet-facing host, tightening it (for example 10:30:60) blunts connection-flood attempts. Leave headroom for legitimate bursts — a fleet of CI runners connecting at once counts against this. Separately, ClientAliveInterval (default 0) set to 300 reaps idle sessions, since abandoned sessions are abandoned attack surface.

Forwarding and tunnelling

AllowTcpForwarding (default yes), AllowAgentForwarding (default yes), X11Forwarding (upstream default no, but many distros ship yes) and PermitTunnel (default no). Every one is a pivot mechanism. Unless a host is deliberately a jump box, set TCP and agent forwarding and X11 to no and leave PermitTunnel off. Agent forwarding in particular lets a compromised intermediate host use your loaded keys — enable it per-host in a Match block only where you genuinely chain through.

A hardened baseline, annotated

# /etc/ssh/sshd_config.d/10-hardening.conf  (OpenSSH 9.6)
# Validate with: sshd -t     Inspect effective config with: sshd -T

# --- Authentication ---
PermitRootLogin no                 # no root over SSH; use a named account + sudo
PasswordAuthentication no          # keys only; removes brute-force surface
PubkeyAuthentication yes           # the mechanism we now stand on
KbdInteractiveAuthentication no    # was ChallengeResponseAuthentication (pre-8.7)
AuthenticationMethods publickey    # single method here; see Match for MFA hosts

# --- Cryptography (trim to what we will defend) ---
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com

# --- Access scope (default-deny for login) ---
AllowGroups ssh-users

# --- Brute-force / resource limits ---
LoginGraceTime 30
MaxAuthTries 3
MaxStartups 10:30:60
ClientAliveInterval 300
ClientAliveCountMax 2

# --- Forwarding (off unless this is a jump host) ---
AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no
PermitTunnel no

# --- Example: a locked-down CI account, keys only, no forwarding ---
Match Group ci-runners
    AuthenticationMethods publickey
    AllowTcpForwarding no
    PermitTTY no
    X11Forwarding no

# --- Example: require a second factor from outside the mgmt subnet ---
Match Address *,!10.20.0.0/24
    AuthenticationMethods publickey,keyboard-interactive

Treat this file as code, not a one-off. The value of understanding each directive is that you can put the config under review and enforce it — the same discipline as a pre-commit baseline that actually runs in CI — so drift shows up in a diff rather than in an incident. And the mindset carries across your stack: getting SSH authentication right is the same instinct as validating a JWT properly — verify the strong thing, refuse the weak downgrade, and never trust a default you have not read.

The directive you never touch is the one that will be quoted back to you after the incident. Read your sshd -T output line by line at least once; that hour is cheaper than any of the alternatives.

Build and rescue work

Hands-on delivery of this kind is handled by Sixteen Pillars Studio.

Free interactive tool

Website compliance checklist

What your site has to do, based on what it actually does

Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.

Most technology problems are not technology problems. They are control problems.

The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.