In early 2026, the CVE-2025-32728 openssh regression exposed servers running default configs to auth-bypass under specific PAM stacking conditions. Admins who had already locked down their SSH configs were unaffected. The ones who hadn’t scrambled through a weekend of emergency patches. This guide walks you through hardening SSH the right way — before the next CVE forces your hand.
Step 1: Audit What You’re Currently Exposing
Before you change a single line, know what you’re running. Use ssh-audit — a tool that fingerprints your SSH server’s algorithms, ciphers, and config weaknesses — to get a baseline.
$ ssh-audit 192.0.2.45
# general
(gen) banner: SSH-2.0-OpenSSH_9.6p1 Ubuntu-4ubuntu0.1
(gen) compatibility: OpenSSH 9.6
(gen) compression: enabled (zlib@openssh.com)
# key exchange algorithms
(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1 hash
(kex) curve25519-sha256 -- [info] available
# encryption ciphers
(enc) aes128-cbc -- [fail] using weak cipher-block chaining mode
(enc) chacha20-poly1305@openssh.com -- [info] available
# message authentication codes
(mac) hmac-sha1 -- [fail] using broken SHA-1 hash
(mac) hmac-sha2-256-etm@openssh.com -- [info] available
The [fail] lines are your immediate targets. diffie-hellman-group14-sha1 and hmac-sha1 have been broken for years — any server still advertising them is negotiating down to weak crypto when a client requests it. aes128-cbc in CBC mode is vulnerable to BEAST-style padding oracle attacks.
An attacker running a MitM tool like mitmproxy or a custom downgrade proxy can force a vulnerable client to negotiate these weak algorithms. You’re not just exposing yourself — you’re exposing every developer who SSHs into that box from a laptop on a conference Wi-Fi.
Step 2: Lock Down sshd_config — The Settings That Actually Matter
Open /etc/ssh/sshd_config on your target server — in this example, prod-web-01.internal running Ubuntu 24.04. Apply this hardened config block:
# /etc/ssh/sshd_config — prod-web-01.internal
# Hardened 2026-08-28
Port 2222
AddressFamily inet
ListenAddress 192.0.2.45
# Authentication
PermitRootLogin no
MaxAuthTries 3
MaxSessions 5
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
# Restrict users
AllowUsers deploy@192.0.2.0/24 jsmith@192.0.2.11
# Crypto — allow only strong algorithms
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512
# Timeouts and connection controls
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
MaxStartups 5:50:10
# Disable legacy features
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
PrintMotd no
Walk through the critical lines. PasswordAuthentication no kills brute-force entirely — no password means no dictionary attack surface. AllowUsers deploy@192.0.2.0/24 jsmith@192.0.2.11 combines username and source IP allowlisting; user jsmith can only connect from one specific workstation. MaxStartups 5:50:10 rate-limits unauthenticated connection attempts — after 5 pending connections, the server randomly drops 50% of new ones up to a hard cap of 10.
AllowTcpForwarding no and AllowAgentForwarding no matter more than most admins realize. A compromised session with agent forwarding enabled lets an attacker pivot to every other server your key touches. Disable both unless a specific service genuinely requires them.
After editing, validate before reloading:
$ sshd -t
# No output = config is valid
$ systemctl reload sshd
Always run sshd -t first. A syntax error in sshd_config and a premature reload can lock you out of your own server. Ask anyone who’s done it on a Friday afternoon.
Step 3: Add Fail2ban and Verify It’s Working
Config hardening stops most attacks. Fail2ban — a log-monitoring daemon that auto-bans IPs after repeated failures — handles the rest. Install it, then check its status after a few minutes of live traffic:
$ fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 3
| |- Total failed: 847
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 12
|- Total banned: 203
`- Banned IP list: 192.0.2.188 192.0.2.201 192.0.2.77 ...
847 total failures means your server is actively being probed — that’s normal for any internet-facing host. The 12 currently banned IPs are sitting in an iptables drop rule and getting nowhere. If you see Currently failed: 0 and Total failed: 0 after running for an hour, your Fail2ban jail isn’t reading the right log source — check journalctl -u fail2ban for filter errors.
Set your ban time aggressively. The default 10 minutes is too short. In /etc/fail2ban/jail.local, set bantime = 24h and maxretry = 3. Persistent scanners return — make them wait a full day.
What To Do Right Now
Run ssh-audit against your most exposed production server today — install it with pip install ssh-audit or pull the Docker image. Note every [fail] line in the output. Then open /etc/ssh/sshd_config and add the KexAlgorithms, Ciphers, and MACs lines from the config block above. Validate with sshd -t and reload. That single change eliminates the entire category of weak-crypto downgrade attacks — and takes under five minutes.
