SSH hardening for Linux servers: Key-based authentication, sshd_config lockdown, Fail2Ban setup, port changes, two-factor authentication, and jump host configuration for Ubuntu and Debian.
SSH hardening on Linux is the single most impactful security measure you can take on a server. SSH is the front door to everything — your applications, your data, your infrastructure. The default OpenSSH configuration on Ubuntu and Debian allows password authentication, permits root login, and doesn't limit connection attempts. That's acceptable for a local dev box, but on an internet-facing server it means automated bots can hammer your login indefinitely. This guide covers every layer of SSH hardening: key generation, sshd_config lockdown, Fail2Ban, port changes, two-factor authentication, and SSH client configuration for managing multiple servers.
Why Default SSH Configuration Is Not Enough
Within minutes of provisioning a new VPS, /var/log/auth.log starts filling with failed login attempts. These are automated scanners probing default credentials: root/root, admin/admin, ubuntu/password. On one server I set up for testing, I logged over 4,000 failed SSH attempts in the first 24 hours — all from different IPs, all targeting port 22 with password guesses.
Password authentication is the root cause. Even a strong password is vulnerable to credential stuffing (where attackers try passwords leaked from other breaches) and slow brute-force attacks that stay under rate limits. Key-based authentication eliminates this entire attack class — there's no password to guess.
Generating and Managing SSH Keys
Which SSH key type should I use in 2026?
Use Ed25519. It's the current best choice: faster than RSA, shorter keys (68 characters vs 400+), and resistant to several classes of side-channel attacks. If you need compatibility with very old systems (OpenSSH before 6.5), fall back to RSA with 4096 bits. Don't use DSA or ECDSA — DSA is deprecated and ECDSA has known implementation concerns.
# Generate an Ed25519 key
ssh-keygen -t ed25519 -C "deploy@myproject" -f ~/.ssh/id_ed25519_prod
# If you need RSA compatibility
ssh-keygen -t rsa -b 4096 -C "deploy@myproject" -f ~/.ssh/id_rsa_prod
The -C comment helps identify the key later. I name keys by purpose (id_ed25519_prod, id_ed25519_staging) rather than using a single default key for everything — if one key is compromised, it only affects one environment.
How do I copy my SSH key to a remote server?
The simplest method is ssh-copy-id, which handles creating the ~/.ssh/authorized_keys file with correct permissions:
ssh-copy-id -i ~/.ssh/id_ed25519_prod.pub [email protected]
If ssh-copy-id isn't available (Windows without WSL, or minimal environments), do it manually:
# On the server, as the target user
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "ssh-ed25519 AAAA... deploy@myproject" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Permissions matter. OpenSSH refuses to use authorized_keys if the file or directory permissions are too open. 700 for .ssh/ and 600 for authorized_keys are the required values.
Critical step: Open a second terminal and test key-based login before changing anything in sshd_config. If the key doesn't work, fix it now while you still have password access. I cannot stress this enough — I've seen teams lock themselves out by skipping this verification.
Production sshd_config — The Complete Lockdown
Once key-based login is confirmed, edit /etc/ssh/sshd_config to disable everything you don't need. Here's the hardened configuration I use on every production server:
# /etc/ssh/sshd_config — production hardened
# Authentication
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 20
# Session limits
MaxSessions 3
ClientAliveInterval 300
ClientAliveCountMax 2
# Restrict features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
# Access control
AllowUsers deploy monitoring
Banner /etc/issue.net
# Crypto hardening
KexAlgorithms curve25519-sha256,[email protected]
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
What does each sshd_config setting actually do?
The settings break into four groups:
Authentication: PermitRootLogin no forces access through a named user — every sudo action maps to a real person. AuthenticationMethods publickey explicitly requires a key (redundant with PasswordAuthentication no, but defense in depth). LoginGraceTime 20 gives 20 seconds to authenticate before the connection drops — bots that open connections without completing auth waste fewer server resources.
Session limits: ClientAliveInterval 300 sends a keep-alive every 5 minutes. After 2 missed replies (ClientAliveCountMax 2), the session is terminated. This cleans up ghost sessions from dropped connections.
Feature restrictions: AllowTcpForwarding no prevents an attacker who compromises an SSH session from tunneling traffic through your server. X11Forwarding no disables graphical forwarding — unnecessary on a headless server and a potential attack vector.
Crypto hardening: The KexAlgorithms, Ciphers, and MACs lines restrict SSH to modern algorithms only. This blocks connections from outdated clients that only support weak ciphers, which is exactly the point — if a client can't use chacha20-poly1305 or aes256-gcm, it shouldn't be connecting to a production server.
Validate the config before restarting:
sudo sshd -t && sudo systemctl restart sshd
If sshd -t returns any error, fix it before restarting. Keep your current session open and test a new connection in a separate terminal.
Fail2Ban Configuration for SSH Protection
How do I configure Fail2Ban to protect SSH?
Even with key-only authentication, Fail2Ban adds value by banning IPs that generate failed authentication attempts. This reduces log noise, saves CPU cycles, and protects against potential zero-day SSH vulnerabilities. Install and configure:
sudo apt install -y fail2ban
Create /etc/fail2ban/jail.local (never edit jail.conf directly — it gets overwritten on updates):
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = ufw
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
Setting banaction = ufw tells Fail2Ban to use UFW commands for banning instead of raw iptables, keeping your firewall rules consistent. For a complete UFW firewall setup, see the dedicated guide.
How do I enable progressive banning for repeat offenders?
Add to the [DEFAULT] section:
[DEFAULT]
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800
First ban: 1 hour. Second ban: 2 hours. Third: 4 hours. This escalates up to 7 days (604800 seconds). Persistent scanners get longer bans each time without manual intervention. Verify and monitor:
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
sudo fail2ban-client get sshd banip --with-time
Changing the SSH Port — Worth It or Security Theater?
Should I change SSH from port 22 to a non-standard port?
It depends. Changing the port doesn't add real security — any serious attacker runs a port scan and finds your SSH service regardless. But it does eliminate 99% of automated bot traffic that only targets port 22. On servers I manage, I see auth.log go from thousands of entries per day to nearly zero after a port change.
If you decide to change it, edit /etc/ssh/sshd_config:
Port 2222
Before restarting SSH, update your firewall — this is where most people lock themselves out:
# Add the new port BEFORE removing the old one
sudo ufw allow 2222/tcp comment 'SSH non-standard'
sudo sshd -t && sudo systemctl restart sshd
# Test the new port in a separate terminal:
# ssh -p 2222 [email protected]
# Only after confirming the new port works:
sudo ufw delete allow 22/tcp
Also update Fail2Ban's [sshd] section: port = 2222. The order matters: add the new firewall rule, restart SSH, verify, then remove the old rule. If you remove port 22 first and the restart fails, you're locked out.
Two-Factor Authentication for SSH
Can I add 2FA to SSH key authentication?
Yes — and it's the strongest SSH protection available. With AuthenticationMethods publickey,keyboard-interactive, a user needs both a valid SSH key and a TOTP code from an authenticator app. Even if someone steals your private key, they can't log in without the second factor.
Install the Google Authenticator PAM module:
sudo apt install -y libpam-google-authenticator
Run the setup as the user who will log in:
google-authenticator -t -d -f -r 3 -R 30 -w 3
This creates a time-based token (-t), disallows reuse (-d), forces writing the config file (-f), rate-limits to 3 attempts per 30 seconds (-r 3 -R 30), and allows a window of 3 codes (-w 3) to account for time drift. Save the QR code or secret key in your authenticator app (Authy, Google Authenticator, or any TOTP app).
Edit /etc/pam.d/sshd — add at the end:
auth required pam_google_authenticator.so
Update /etc/ssh/sshd_config:
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
Restart SSH and test in a separate terminal before closing your current session. The login flow now requires your SSH key first, then prompts for the TOTP code. I use this on servers with sensitive data or regulatory requirements. For most infrastructure servers where only I have access, key-only with Fail2Ban is sufficient.
SSH Client Configuration for Multiple Servers
When you manage multiple servers, typing ssh -i ~/.ssh/id_ed25519_prod -p 2222 [email protected] every time gets old. Create an SSH client config at ~/.ssh/config on your local machine:
# ~/.ssh/config
Host prod
HostName 203.0.113.50
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_prod
IdentitiesOnly yes
Host staging
HostName 198.51.100.25
User deploy
Port 22
IdentityFile ~/.ssh/id_ed25519_staging
IdentitiesOnly yes
Host db-tunnel
HostName 203.0.113.50
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_prod
LocalForward 5432 127.0.0.1:5432
Now ssh prod connects with the right key, port, and user. ssh db-tunnel opens a local port forward to the database. IdentitiesOnly yes prevents the SSH agent from trying every loaded key — it uses only the specified one, which avoids hitting MaxAuthTries on servers with strict limits.
Auditing Your SSH Security
After hardening, verify your configuration covers the essentials:
- Run
sudo sshd -Tto dump the effective configuration — check every value matches your intent - Search auth.log for successful logins:
grep "Accepted" /var/log/auth.log— verify only expected users and IPs appear - Check for unauthorized keys:
cat /home/*/.ssh/authorized_keys— each key should be known and documented - Verify Fail2Ban is running:
sudo fail2ban-client status sshd - Confirm UFW rules match your SSH port:
sudo ufw status numbered
For a broader security audit covering kernel tuning, file system hardening, and audit logging beyond SSH, follow the Linux server hardening checklist. SSH is the front door — but a hardened door on an open house isn't enough.