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:

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.