LinuxXXVI · SSHHardening
SSH hardening - production checklist
What you'll learn
- Apply a comprehensive SSH hardening
- Verify the hardening with ssh -vvv and external scanners
- Configure rate limiting and intrusion detection
- Document the hardening baseline for comparison
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
SSH hardening is a multi-layer practice: ciphers, authentication, banner, timeouts, logging, and rate limiting. This lesson is a comprehensive checklist applied to a production host.
The hardening matrix
| Layer | Control | Reason |
|---|---|---|
| Cryptography | Modern KexAlgorithms, Ciphers, MACs | Avoid weak algorithms |
| Authentication | Publickey only, disable password | Brute-force resistance |
| Authorisation | AllowGroups, no root login | Limit who can log in |
| Network | Disable DNS lookups, restrict ciphers | Reduce attack surface |
| Sessions | ClientAliveInterval, MaxSessions | Timeouts and limits |
| Forwards | Disable agent forwarding, restrict TCP forwarding | Reduce attack pivots |
| Logging | VERBOSE log level, syslog forwarding | Visibility |
| Rate limit | fail2ban or similar | Slow down brute force |
| Audit | External scans, ssh-audit | Continuous validation |
Step-by-step hardening
Step 1: Generate modern host keys
sudo rm /etc/ssh/ssh_host_*
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
sudo ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""
Ed25519 for new connections, RSA 4096 for backward compatibility with older clients.
Step 2: Apply sshd_config
Use the production baseline from the sshd_configuration
lesson. Save it to /etc/ssh/sshd_config.
Step 3: Validate
# Which unit is it? ssh.service on Debian/Ubuntu, sshd.service on
# RHEL/Rocky/Alma. Never guess this in a script.
systemctl list-unit-files | grep -E '^ssh'
# Validate the config BEFORE reloading. sshd -t exits non-zero on a
# syntax error, and a reload with a broken config leaves the running
# daemon on the old config - or, after a restart, not running at all.
sudo sshd -t
sudo systemctl reload sshd # or: sudo systemctl reload ssh
Step 4: Verify the cipher suite
From a client:
ssh -vvv user@server 2>&1 | grep -E 'kex: |cipher: |mac:'
Expected output uses only modern algorithms.
Step 5: Verify with ssh-audit
# Debian / Ubuntu
sudo apt install ssh-audit
# RHEL / Fedora
sudo dnf install ssh-audit
# Neither packaged? Use an isolated install, not a bare pip:
pipx install ssh-audit
ssh-audit server.example.com
ssh-audit grades the SSH server against best practices and lists any weak algorithms.
A bare pip install ssh-audit is what most guides still show,
and on any current distribution it stops with
error: externally-managed-environment. Debian 12+, Ubuntu
23.04+ and Fedora 38+ ship a PEP 668 marker
(/usr/lib/python3.x/EXTERNALLY-MANAGED) that forbids pip from
writing into the system site-packages, because doing so lets
pip and the package manager fight over the same files and break
distribution tooling written in Python. pipx gives the tool
its own virtualenv and a symlink on PATH, which is what you
actually wanted. Never reach for --break-system-packages on a
production host: the flag does what it says.
Step 6: Configure fail2ban
sudo apt install fail2ban
/etc/fail2ban/jail.local:
[sshd]
enabled = true
port = ssh
filter = sshd
# Read from the journal instead of a log file. /var/log/auth.log is
# Debian/Ubuntu only - on RHEL, Rocky and Alma the file is
# /var/log/secure, and on a journald-only host neither exists.
backend = systemd
# _COMM matches the daemon rather than the unit, so this is correct
# whether the unit is ssh.service (Debian) or sshd.service (RHEL).
journalmatch = _COMM=sshd
maxretry = 3
bantime = 3600
findtime = 600
The backend = systemd form is worth preferring even on Debian.
A file-based jail whose logpath does not exist starts anyway and
bans nobody: fail2ban-client status sshd shows the jail as
active with zero failures, which looks identical to “no attacks”,
and nobody notices until the audit. Reading the journal removes
the path from the equation entirely.
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
After a few failed attempts, fail2ban bans the source IP.
Step 7: External scan
nmap -sV -p 22 server.example.com
ssh-audit server.example.com
The expected result: only SSH-2 (there is no SSH-1 left to disable — OpenSSH removed it in 7.6), modern algorithms, and no weak host key, cipher or MAC offers.
sshd always advertises its version in the protocol banner and there is no directive to suppress it; the banner is exchanged before any configuration applies. Do not record “the version is visible” as a finding — every SSH server on earth does that. An out-of-date version is the finding, and the fix is to patch, not to hide.
Step 8: Document the baseline
SSH HARDENING BASELINE
======================
Host: server.example.com
Date: 2026-08-09
Hardening applied: yes
Algorithms:
- Kex: curve25519-sha256, curve25519-sha256@libssh.org, diffie-hellman-group16-sha512
- Ciphers: chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-ctr
- MACs: hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com
- Host keys: ed25519, rsa-sha2-512
Authentication:
- Publickey: yes
- Password: no
- Root login: no
Logging:
- SyslogFacility: AUTH
- LogLevel: VERBOSE
- fail2ban: enabled
ssh-audit grade: A
Save for future comparison. After any change, re-run the audit.
Common hardening mistakes
- Disable password, leave keyboard-interactive enabled: KbdInteractiveAuthentication no. Some PAM modules use keyboard-interactive for password fallback.
- Allow root with password: PermitRootLogin no.
- Forward agents by default:
AllowAgentForwarding noinsshd_config, with aMatch Groupblock for the few who need it. Do not writeForwardAgent nohere: that is the client keyword fromssh_config, sshd rejects it withBad configuration option: ForwardAgent, and the daemon then fails to start on the next reload while agent forwarding stays enabled. - Permit empty passwords: PermitEmptyPasswords no (default).
- Carrying
Protocol 2forward: delete it. TheProtocolkeyword was removed in OpenSSH 7.6 along with SSH-1 support, and sshd silently discards the line — it looks like a control and is not one. - Permit X11 forwarding on servers: X11Forwarding no.
- Permit TCP forwarding on all: AllowTcpForwarding no or selective.
Knowledge check
Knowledge check · 3 questions
Q1. What tool grades an SSH server against best-practice algorithms?
Q2. fail2ban bans IPs that attempt too many SSH logins.
Q3. Which of the following are valid SSH hardening steps? Select all that apply.
Passing score: 75%. Answers are checked in this browser.