Why it matters
SSH is the main attack vector on Linux servers. Botnets brute-force
it around the clock: on any publicly routable IP with port 22 open,
within a day you will see thousands of attempts in auth.log. The
default OpenSSH configuration is too open for prod:
- Root login over a password is allowed
- Passwords are allowed at all
- All users can connect
- There is no rate limit on attempts
- X11 forwarding is on
This document is a set of "disable by default" recipes.
Minimal hardening: what to change in sshd_config
/etc/ssh/sshd_config (or the drop-in /etc/ssh/sshd_config.d/00-hardening.conf):
# ===== Authentication =====
PermitRootLogin no # never root directly
PasswordAuthentication no # keys only
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey # key only; for 2FA: publickey,keyboard-interactive
# ===== Who is allowed in =====
AllowUsers alice bob carol # whitelist
# OR by group
AllowGroups ssh-users
# ===== Limiting attempts =====
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30 # disconnect if not authenticated within 30s
# ===== Server identity =====
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# ===== Cipher / KEX =====
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
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,umac-128-etm@openssh.com
# ===== Forwarding =====
X11Forwarding no
AllowAgentForwarding no # enable only where needed
AllowTcpForwarding no # yes for bastion hosts
PermitTunnel no
GatewayPorts no
# ===== Other =====
PermitEmptyPasswords no
ClientAliveInterval 300
ClientAliveCountMax 2 # 10 minutes idle = disconnect
Banner /etc/issue.net # legal warning
PrintLastLog yes
UseDNS no # speeds up login (no reverse DNS)
Apply:
sshd -t # syntax check
systemctl reload sshd
Do not close your active session right away! Open a second SSH terminal and confirm that new connections go through.
Keys only
Generate a strong key:
ssh-keygen -t ed25519 -a 100 -C "alice@laptop"
# ed25519 is modern, short, and fast
# -a 100 is 100 KDF rounds to protect the passphrase
Copy it to the server:
ssh-copy-id alice@server # automatically
# or by hand:
cat ~/.ssh/id_ed25519.pub | ssh alice@server 'cat >> ~/.ssh/authorized_keys'
Permissions are mandatory:
ssh -o LogLevel=DEBUG alice@server # shows if authorized_keys mode is 644 + group/other = drop
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
SSH Certificate Authority: scaling keys
Handing out keys to 100 servers for every user does not scale. An SSH CA signs short-lived certificates:
# On the CA machine (isolated)
ssh-keygen -t ed25519 -f ssh_ca_key
# Distribute the CA public key to all servers in /etc/ssh/ca.pub
# In sshd_config:
TrustedUserCAKeys /etc/ssh/ca.pub
# Sign the user's key
ssh-keygen -s ssh_ca_key -I "alice-2026-05-01" -n alice -V +1d ~/.ssh/id_ed25519.pub
▸creates id_ed25519-cert.pub
Servers accept anyone whose key is signed by the CA. The validity
period is built into the certificate. Revocation goes through RevokedKeys.
It is used at Facebook, Netflix, and Uber. For 1 or 2 servers it is overkill; for 100 or more it is essential.
Match blocks
Conditional rules:
# SFTP-only users
Match Group sftponly
ChrootDirectory /var/sftp/%u
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
# Bastion: allow TCP forwarding only from a specific network
Match Address 10.0.0.0/24
AllowTcpForwarding yes
PermitTunnel yes
# Deny login from the deploy user outside the corp network
Match User deploy Address !10.0.0.0/8,!172.16.0.0/12
DenyUsers *
Custom port: security through obscurity?
Changing Port 22 → Port 2222 does not make SSH more secure.
Bots scan all 65535 ports. But it reduces noise in the logs:
Port 2222
Do not forget to:
- Open 2222 in the firewall
- Close 22
- Update SELinux on RHEL:
semanage port -a -t ssh_port_t -p tcp 2222 - Update the fail2ban jail
- Tell clients (
~/.ssh/configorPort 2222on the command line)
I would keep 22 plus fail2ban, and use a custom port for bastion hosts that carry a lot of automation.
A fail2ban jail for sshd
fail2ban reads auth.log and, after N failed attempts, bans the IP in iptables/nftables.
/etc/fail2ban/jail.local:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = nftables
backend = systemd
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 3
systemctl restart fail2ban
fail2ban-client status sshd
fail2ban-client unban 1.2.3.4 # unban by hand
2FA through PAM
Make sshd require Google Authenticator or a Yubikey on top of the key.
Note: the baseline config above disables KbdInteractiveAuthentication
and ChallengeResponseAuthentication. For 2FA you have to remove these
lines (or set them to yes), otherwise the keyboard-interactive step
will not fire:
# sshd_config
AuthenticationMethods publickey,keyboard-interactive
KbdInteractiveAuthentication yes # overrides the baseline ban
ChallengeResponseAuthentication yes
UsePAM yes
In /etc/pam.d/sshd:
auth required pam_google_authenticator.so nullok
And each user runs google-authenticator to generate a QR code.
Auditing an existing config
Tools:
ssh-audit example.com # external check of cipher/kex/host-key
sshd -T # effective config (after Match)
sshd -T | grep -iE 'permitroot|password|kex|cipher|mac|maxauth'
When things go wrong
Permission denied (publickey): the key is not inauthorized_keys, or the mode on~/.ssh(must be 700) andauthorized_keys(600) is wrong.- Locked yourself out of SSH while changing the config: open a second active SSH session BEFORE the reload. If it is already too late, use the console/IPMI/ILO/cloud console. Clouds offer a serial console through the CLI.
no matching host key type found: you disabled RSA, but the client has only RSA. AddHostKey /etc/ssh/ssh_host_rsa_keyback, or ask the client to update openssh.no matching cipher found: client offers: an old client, the cipher is not in your whitelist. Checksshd -T | grep cipher.fail2banbans itself: your IP landed in a jail. Whitelist it withignoreip = 127.0.0.1/8 10.0.0.0/24 my.public.ip/32.- GSSAPI/Kerberos handshake slows down login: turn it off with
GSSAPIAuthentication noif you do not use kerberos. AlsoUseDNS no. - A Match block does not apply: Match is parsed in order, and the first match wins. A Match block ends at the next Match or at EOF.
Checklist
- PermitRootLogin no
- PasswordAuthentication no
- AllowUsers/Groups whitelist
- MaxAuthTries ≤ 3
- X11Forwarding no (if not needed)
- fail2ban jail enabled
- ed25519 host key and client key
- Modern KEX/Cipher/MAC, no legacy
- Banner with a legal warning
- sshd -T → check the effective configuration
- SSH CA certificates if 5+ servers
- auditd watch on sshd_config