Skip to main content
RunBook Academy

LinuxXXVI · SSHHardening

SSH hardening - production checklist

Intermediate⏱ ~12 minsshsshd

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

Not yet marked complete on this device.

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

LayerControlReason
CryptographyModern KexAlgorithms, Ciphers, MACsAvoid weak algorithms
AuthenticationPublickey only, disable passwordBrute-force resistance
AuthorisationAllowGroups, no root loginLimit who can log in
NetworkDisable DNS lookups, restrict ciphersReduce attack surface
SessionsClientAliveInterval, MaxSessionsTimeouts and limits
ForwardsDisable agent forwarding, restrict TCP forwardingReduce attack pivots
LoggingVERBOSE log level, syslog forwardingVisibility
Rate limitfail2ban or similarSlow down brute force
AuditExternal scans, ssh-auditContinuous 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 no in sshd_config, with a Match Group block for the few who need it. Do not write ForwardAgent no here: that is the client keyword from ssh_config, sshd rejects it with Bad 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 2 forward: delete it. The Protocol keyword 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

  1. Q1. What tool grades an SSH server against best-practice algorithms?

  2. Q2. fail2ban bans IPs that attempt too many SSH logins.

  3. Q3. Which of the following are valid SSH hardening steps? Select all that apply.

Passing score: 75%. Answers are checked in this browser.