In early 2026, a misconfigured OpenSSH instance on an exposed jump host let attackers brute-force a root login using a credential list from the RockYou2024 dump — the server had password authentication still enabled. This is not a rare edge case. Default SSH configurations ship insecure, and production servers get hammered the moment they hit the internet. Here is exactly how you lock them down.
\n\n
Step 1: Audit Your Current SSH Exposure First
\n\n
Before you change anything, know what you are actually exposing. Use ssh-audit — a tool that fingerprints your SSH server and flags weak algorithms, outdated key exchanges, and policy violations.
\n\n
$ ssh-audit 192.0.2.45\n\n# general\n(gen) banner: SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6\n(gen) software: OpenSSH 8.9p1\n\n# key exchange algorithms\n(kex) diffie-hellman-group14-sha1 -- [fail] using broken SHA-1\n(kex) curve25519-sha256 -- [info] available\n\n# authentication methods\n(aut) password authentication -- [warn] enabled\n(aut) publickey -- [info] enabled\n\n# ciphers\n(enc) aes128-cbc -- [fail] using deprecated CBC mode\n(enc) chacha20-poly1305@openssh.com -- [info] available\n\n# mac algorithms\n(mac) hmac-sha1 -- [fail] using broken SHA-1\n(mac) hmac-sha2-256-etm@openssh.com -- [info] available
\n\n
Three red flags jump out immediately: SHA-1 key exchange, CBC-mode ciphers, and password authentication. An attacker scanning port 22 with nmap and then running ssh-audit sees the same thing in under 30 seconds. The [fail] lines are not warnings — they are broken cryptography that modern tooling can exploit or downgrade.
\n\n
Your immediate goal is to eliminate every [fail] line before the box takes traffic. Keep this output as your baseline. Run ssh-audit again after each configuration change to confirm the fix landed.
\n\n
Step 2: Apply a Hardened sshd_config
\n\n
Open /etc/ssh/sshd_config on your server (web-prod-01.internal in this example). Replace the defaults with a minimal, explicitly-scoped configuration.
\n\n
# /etc/ssh/sshd_config — web-prod-01.internal\n# Last hardened: 2026-10-16\n\nPort 2222\nAddressFamily inet\nListenAddress 192.0.2.45\n\n# Authentication\nPermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes\nAuthenticationMethods publickey\nPermitEmptyPasswords no\nMaxAuthTries 3\nLoginGraceTime 20\n\n# Restrict users\nAllowUsers deploy@192.0.2.0/24 marcus@192.0.2.10\n\n# Strong algorithms only\nKexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com\nHostKeyAlgorithms ssh-ed25519,rsa-sha2-512\n\n# Session hardening\nClientAliveInterval 300\nClientAliveCountMax 2\nX11Forwarding no\nAllowTcpForwarding no\nAllowAgentForwarding no\nBanner /etc/ssh/banner.txt\nPrintLastLog yes\nLogLevel VERBOSE
\n\n
Walk through the decisions that matter most. PermitRootLogin no forces attackers to know both a valid username and a key — eliminating the single highest-value SSH target. AllowUsers deploy@192.0.2.0/24 marcus@192.0.2.10 means even a valid key from an unexpected IP gets dropped at the daemon level, not the firewall. MaxAuthTries 3 combined with LoginGraceTime 20 kills slow brute-force attempts before they get traction.
\n\n
The algorithm restrictions are the most important cryptographic change. You are removing every legacy option and forcing the client to negotiate only modern primitives. Old automation scripts or ancient clients will break — that is intentional. Validate the config before restarting:
\n\n
$ sudo sshd -t && echo \"Config OK\"
Config OK
\n\n
If you see errors, fix them before restarting. Never restart sshd without testing the config first on a production box — you will lock yourself out.
\n\n
Step 3: Layer Fail2Ban and Key Rotation on Top
\n\n
Configuration alone is not enough. You need runtime protection and key hygiene. Install fail2ban and create a jail specifically for SSH:
\n\n
# /etc/fail2ban/jail.d/sshd-hardened.conf\n\n[sshd]\nenabled = true\nport = 2222\nfilter = sshd\nlogpath = /var/log/auth.log\nmaxretry = 3\nbantime = 3600\nfindtime = 600\naction = iptables-multiport[name=SSH, port=\"2222\", protocol=tcp]
\n\n
After three failed attempts within ten minutes, the offending IP is banned for one hour. Check active bans with:
\n\n
$ sudo fail2ban-client status sshd\n\nStatus for the jail: sshd\n|- Filter\n| |- Currently failed: 2\n| |- Total failed: 47\n| `- File list: /var/log/auth.log\n`- Actions\n |- Currently banned: 3\n |- Total banned: 19\n `- Banned IP list: 192.0.2.201 192.0.2.87 192.0.2.133
\n\n
Forty-seven failed attempts means someone is actively probing. The three currently banned IPs are blocked at the firewall level. Feed these into your SIEM or threat intel pipeline — repeated offenders from the same subnet deserve a longer ban or a full block at the perimeter.
\n\n
On key rotation: set a reminder every 90 days to generate new ed25519 keys for each service account. Revoke old keys by removing them from ~/.ssh/authorized_keys on the server side. There is no central revocation in vanilla SSH, so discipline here is manual but non-negotiable.
\n\n
What To Do Right Now
\n\n
Run ssh-audit against one production server today — install it with pip install ssh-audit if you do not have it. Paste the output into a ticket, assign it to yourself, and give it a two-day deadline. The scan takes 10 seconds. Knowing exactly what is broken before an attacker tells you is the only reasonable starting point.
