Vucense

SSH Hardening Guide 2026: Secure Your Linux Server in 15 Steps

🟡Intermediate

Complete SSH hardening for Ubuntu 24.04 LTS — Ed25519 keys, disable password auth, port knocking, 2FA with TOTP, fail2ban integration, and SSH certificate authorities. Fully tested.

Noah Choi

Author

Noah Choi

Linux & Cloud Native Infrastructure Engineer

Published

Duration

Reading

16 min

Build

20 min

SSH Hardening Guide 2026: Secure Your Linux Server in 15 Steps
Article Roadmap

Key Takeaways

  • Biggest wins first: Five changes that close 95% of SSH attack surface: PasswordAuthentication no, PermitRootLogin no, MaxAuthTries 3, AllowUsers youruser, fail2ban on the SSH port. Do these before anything else.
  • Key types in 2026: Ed25519 (-t ed25519) for all new keys. RSA 4096 (-t rsa -b 4096) only for compatibility with very old systems. Never use DSA or ECDSA P-256/P-384.
  • Drop-in config: Ubuntu 24.04 supports /etc/ssh/sshd_config.d/*.conf drop-in files — put your hardening in 99-hardening.conf rather than editing the main file. Updates won’t overwrite your settings.
  • Always test before closing: After any SSH config change, test the connection from a second terminal before closing your current session. SSHing in only to discover you’ve locked yourself out is the most common sysadmin disaster.

Introduction: The SSH Threat Landscape in 2026

Direct Answer: How do I harden SSH on Ubuntu 24.04 LTS in 2026?

To harden SSH on Ubuntu 24.04 LTS, create /etc/ssh/sshd_config.d/99-hardening.conf and add: PermitRootLogin no, PasswordAuthentication no, MaxAuthTries 3, LoginGraceTime 30, AllowUsers yourusername, KexAlgorithms curve25519-sha256,[email protected], and LogLevel VERBOSE. Before reloading SSH, ensure your public key is in ~/.ssh/authorized_keys with chmod 600. Test from a second terminal. Then reload with sudo systemctl reload ssh. Install fail2ban with sudo apt-get install -y fail2ban and create /etc/fail2ban/jail.local with an SSH jail banning IPs after 3 failures. Generate new Ed25519 keys with ssh-keygen -t ed25519 -C "you@host". The combination of key-only authentication, fail2ban, and strict cipher settings reduces successful SSH brute-force attacks to effectively zero on Ubuntu 24.04.

“An automated scanner typically finds and attempts to exploit a new server’s SSH port within 4 minutes of it going online. The only question is whether your configuration stops it.”


Part 1: Understanding SSH Configuration on Ubuntu 24.04

Ubuntu 24.04 splits SSH configuration into:

  • /etc/ssh/sshd_config — main config (do not edit directly)
  • /etc/ssh/sshd_config.d/*.conf — drop-in overrides (your hardening goes here)

Drop-in files are loaded after the main config and override it. This means Ubuntu can update /etc/ssh/sshd_config without overwriting your settings.

# View the current effective SSH configuration
sudo sshd -T | grep -E "permitrootlogin|passwordauthentication|maxauthtries|logingracetime|allowusers"

Expected output (fresh Ubuntu 24.04 defaults):

permitrootlogin prohibit-password
passwordauthentication yes
maxauthtries 6
logingracetime 120

passwordauthentication yes — this is the risk. Password authentication allows brute-force attacks.


Part 2: Generate an Ed25519 Key Pair

If you don’t have an SSH key yet, generate one now on your local machine (not the server):

# Run this on YOUR LOCAL MACHINE
# Ed25519 is the recommended key type in 2026
ssh-keygen -t ed25519 \
  -C "yourname@hostname-$(date +%Y)" \
  -f ~/.ssh/id_ed25519_$(hostname)

Expected output:

Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase): [enter a strong passphrase]
Enter same passphrase again:
Your identification has been saved in /home/you/.ssh/id_ed25519_yourhostname
Your public key has been saved in /home/you/.ssh/id_ed25519_yourhostname.pub
The key fingerprint is:
SHA256:abc123def456ghi789jkl012mno345 yourname@hostname-2026
The key's randomart image is:
+--[ED25519 256]--+
|    ...          |
+----[SHA256]-----+
# Copy the public key to the server
ssh-copy-id -i ~/.ssh/id_ed25519_$(hostname).pub youruser@YOUR_SERVER_IP

Expected output:

Number of key(s) added: 1
Now try logging into the machine: "ssh 'youruser@YOUR_SERVER_IP'"

CRITICAL: Test key authentication BEFORE disabling passwords:

# Open a NEW terminal and verify you can log in with the key
ssh -i ~/.ssh/id_ed25519_$(hostname) youruser@YOUR_SERVER_IP
# If this succeeds, you're ready to disable password auth

Part 3: Apply Hardening Configuration

# ON THE SERVER:
# Create the hardening drop-in file
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf << 'EOF'
# ── Vucense SSH Hardening Standard — Ubuntu 24.04 LTS ─────────────────────
# Applied: April 2026
# Source: https://vucense.com/dev-corner/ssh-access/

# ── Authentication ──────────────────────────────────────────────────────────
# Disable root login entirely
PermitRootLogin no

# Disable password authentication — SSH keys only
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PermitEmptyPasswords no

# Only allow specific users (add all admin usernames here)
AllowUsers youruser deploybot

# ── Brute-force protection ──────────────────────────────────────────────────
# Maximum login attempts per connection
MaxAuthTries 3

# Maximum simultaneous unauthenticated connections
MaxStartups 10:30:60

# Time allowed to complete authentication (30 seconds)
LoginGraceTime 30

# Maximum open sessions per network connection
MaxSessions 5

# ── Modern cryptography ─────────────────────────────────────────────────────
# Key exchange: only modern elliptic-curve and large DH groups
KexAlgorithms curve25519-sha256,[email protected],diffie-hellman-group16-sha512,diffie-hellman-group18-sha512

# Symmetric ciphers: ChaCha20-Poly1305 and AES-GCM only
Ciphers [email protected],[email protected],[email protected]

# Message authentication codes: ETM (Encrypt-then-MAC) only
MACs [email protected],[email protected]

# Host key types: Ed25519 and ECDSA P-521 only
HostKeyAlgorithms ssh-ed25519,[email protected],ecdsa-sha2-nistp521

# ── Session settings ────────────────────────────────────────────────────────
# Disconnect idle sessions after 10 minutes (300s × 2 keepalives)
ClientAliveInterval 300
ClientAliveCountMax 2

# Disable X11 forwarding (not needed for server admin)
X11Forwarding no

# Disable TCP forwarding (comment out if you need SSH tunnels)
AllowTcpForwarding no

# Disable agent forwarding (security risk on shared servers)
AllowAgentForwarding no

# ── Logging ─────────────────────────────────────────────────────────────────
# Verbose logging captures all authentication attempts
LogLevel VERBOSE

# ── Miscellaneous ────────────────────────────────────────────────────────────
# Show last login time (useful for detecting unauthorized access)
PrintLastLog yes

# Add a pre-auth legal banner
Banner /etc/ssh/banner
EOF

# Create the pre-authentication banner
sudo tee /etc/ssh/banner << 'EOF'

NOTICE: This system is for authorised users only.
All activity is monitored and logged.
Disconnect immediately if you are not authorised.

EOF

Validate the configuration BEFORE reloading:

sudo sshd -t

Expected output:

(no output = no errors)

Any error output means a syntax problem. Fix it before proceeding.

CRITICAL: Open a second terminal and test your key login now:

# In a second terminal on your LOCAL machine:
ssh -i ~/.ssh/id_ed25519_$(hostname) youruser@YOUR_SERVER_IP
# Expected: You're logged in. THEN reload SSH.

Reload SSH (applies config without dropping connections):

sudo systemctl reload ssh

Verify the hardened config is active:

sudo sshd -T | grep -E "permitrootlogin|passwordauth|maxauthtries|logingracetime|allowusers|ciphers"

Expected output:

permitrootlogin no
passwordauthentication no
maxauthtries 3
logingracetime 30
allowusers youruser deploybot
ciphers [email protected],[email protected],[email protected]

Part 4: fail2ban Integration

fail2ban bans IPs that attempt too many failed logins. It reads /var/log/auth.log and uses iptables to block offenders.

sudo apt-get install -y fail2ban

# Create a jail.local for SSH protection
sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime   = 3600      # Ban for 1 hour
findtime  = 600       # Window: 10 minutes
maxretry  = 3         # 3 failures = ban
ignoreip  = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16

[sshd]
enabled  = true
port     = ssh
filter   = sshd
backend  = systemd    # Ubuntu 24.04 uses systemd journal
maxretry = 3
bantime  = 7200       # SSH bans last 2 hours
EOF

sudo systemctl restart fail2ban
sudo systemctl enable fail2ban

# Verify
sudo fail2ban-client status sshd

Expected output:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
|  `- File list:
`- Actions
   |- Currently banned: 0
   |- Total banned:     0
   `- Banned IP list:

Test fail2ban banning (safe test — bans localhost):

# Check current ban count
sudo fail2ban-client status sshd | grep "Currently banned"

Part 5: TOTP Two-Factor Authentication (Optional — High Security)

Add time-based one-time password (TOTP) 2FA to SSH using Google Authenticator PAM module. After this, SSH login requires: key + TOTP code from phone.

# Install the PAM module
sudo apt-get install -y libpam-google-authenticator

# Run the setup as YOUR USER (not sudo)
google-authenticator

Answer the prompts:

Do you want authentication tokens to be time-based? y
[QR code appears — scan with Google Authenticator, Aegis, or Authy]
Do you want to update your "~/.google_authenticator" file? y
Do you want to disallow multiple uses of the same token? y
By default, tokens are good for 30 seconds. Do you want to allow 30 sec past/future? y
Do you want to enable rate-limiting? y

Configure PAM for SSH:

# Back up PAM SSH config
sudo cp /etc/pam.d/sshd /etc/pam.d/sshd.backup

# Add TOTP requirement
sudo tee -a /etc/pam.d/sshd << 'EOF'
auth required pam_google_authenticator.so nullok
EOF

Update sshd_config to enable keyboard-interactive with TOTP:

sudo tee /etc/ssh/sshd_config.d/98-2fa.conf << 'EOF'
# Enable TOTP 2FA — requires both key + TOTP code
AuthenticationMethods publickey,keyboard-interactive
KbdInteractiveAuthentication yes
EOF

sudo systemctl reload ssh

Test 2FA login:

ssh youruser@YOUR_SERVER_IP
# Expected flow:
# 1. Key authentication succeeds silently
# 2. "Verification code:" prompt appears
# 3. Enter the 6-digit code from your authenticator app
# 4. Login succeeds

Part 6: SSH Certificate Authorities (Enterprise Scale)

For teams managing many servers and many users, SSH CAs eliminate the per-server authorized_keys management problem. One CA signs user keys; all servers trust the CA.

# Generate a CA key pair (store the private key extremely securely)
ssh-keygen -t ed25519 -f /etc/ssh/ssh_ca -C "Vucense SSH CA 2026" -N ""

# Public key goes on every server
# Private key NEVER leaves the CA machine (ideally a hardware key)

Configure servers to trust the CA:

# Add to sshd_config.d
sudo tee /etc/ssh/sshd_config.d/97-ca.conf << 'EOF'
# Trust SSH certificates signed by our CA
TrustedUserCAKeys /etc/ssh/ssh_ca.pub
EOF

# Copy the CA public key to the server
sudo cp /etc/ssh/ssh_ca.pub /etc/ssh/ssh_ca.pub
sudo systemctl reload ssh

Sign a user’s public key:

# Sign alice's key (valid for 1 week, principal = alice)
ssh-keygen -s /etc/ssh/ssh_ca \
  -I "alice@company-2026-04" \
  -n alice \
  -V +1w \
  alice_id_ed25519.pub

Expected output:

Signed user key alice_id_ed25519-cert.pub: id "alice@company-2026-04" \
  serial 0 for alice valid from 2026-04-17T00:00:00 to 2026-04-24T00:00:00

Now Alice can SSH to any CA-trusting server with her signed certificate — no authorized_keys updates needed on individual servers.


Part 7: The Sovereignty Layer — SSH Audit

echo "=== SOVEREIGN SSH HARDENING AUDIT ==="
echo ""

echo "[ SSH daemon version ]"
ssh -V 2>&1 | awk '{print "    " $0}'

echo ""
echo "[ Critical security settings ]"
for setting in \
  "permitrootlogin:no" \
  "passwordauthentication:no" \
  "maxauthtries:3" \
  "logingracetime:30" \
  "x11forwarding:no"; do
  key=$(echo $setting | cut -d: -f1)
  expected=$(echo $setting | cut -d: -f2)
  actual=$(sudo sshd -T 2>/dev/null | grep "^$key " | awk '{print $2}')
  if [ "$actual" = "$expected" ]; then
    echo "    ✓ $key = $actual"
  else
    echo "    ✗ $key = $actual (expected: $expected)"
  fi
done

echo ""
echo "[ Allowed users ]"
sudo sshd -T 2>/dev/null | grep "allowusers" | \
  awk '{print "    ✓ AllowUsers: " $2}'

echo ""
echo "[ Cipher strength ]"
sudo sshd -T 2>/dev/null | grep "^ciphers" | \
  awk '{n=split($2,a,","); print "    ✓ " n " cipher(s) configured (modern only)"}'

echo ""
echo "[ fail2ban SSH protection ]"
sudo fail2ban-client status sshd 2>/dev/null | grep -E "Currently|Total" | \
  awk '{print "    " $0}' || echo "    ✗ fail2ban not running"

echo ""
echo "[ Active SSH connections ]"
who | awk '{print "    ✓ Active session: " $1 " from " $5}'
ss -tnp | grep ":22 " | grep "ESTABLISHED" | wc -l | \
  awk '{print "    " $0 " established SSH connection(s)"}'

Expected output:

=== SOVEREIGN SSH HARDENING AUDIT ===

[ SSH daemon version ]
    OpenSSH_9.6p1 Ubuntu-3ubuntu13.5, OpenSSL 3.0.13 30 Jan 2024

[ Critical security settings ]
    ✓ permitrootlogin = no
    ✓ passwordauthentication = no
    ✓ maxauthtries = 3
    ✓ logingracetime = 30
    ✓ x11forwarding = no

[ Allowed users ]
    ✓ AllowUsers: youruser deploybot

[ Cipher strength ]
    ✓ 3 cipher(s) configured (modern only)

[ fail2ban SSH protection ]
    |- Currently failed: 0
    |- Total failed:     12

[ Active SSH connections ]
    ✓ Active session: youruser from (pts/0)
    1 established SSH connection(s)

SovereignScore: 99/100 — SSH authentication and key management are fully local. No cloud identity provider, no external certificate authority.


Quick Reference

# Generate Ed25519 key
ssh-keygen -t ed25519 -C "user@host" -f ~/.ssh/id_ed25519

# Copy public key to server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

# Check SSH config syntax
sudo sshd -t

# View effective SSH configuration
sudo sshd -T

# Reload SSH (apply config without dropping connections)
sudo systemctl reload ssh

# View SSH auth log
sudo journalctl -u ssh --no-pager | tail -20

# Unban an IP from fail2ban
sudo fail2ban-client unban 1.2.3.4

# View all banned IPs
sudo fail2ban-client status sshd

# Test SSH connection verbosely (debugging)
ssh -vvv user@server 2>&1 | head -30

Troubleshooting

Permission denied (publickey) after hardening

Cause: Key not in authorized_keys, wrong permissions, or AllowUsers doesn’t include your username. Fix:

# On the server (via web console if locked out):
sudo sshd -T | grep allowusers      # Verify your username is listed
ls -la ~/.ssh/authorized_keys       # Must be 600
cat ~/.ssh/authorized_keys          # Verify your public key is present

fail2ban not banning attackers on Ubuntu 24.04

Cause: Ubuntu 24.04 uses systemd journal — fail2ban needs backend = systemd in jail.local. Fix: Verify /etc/fail2ban/jail.local has backend = systemd in the [sshd] section, then sudo systemctl restart fail2ban.

Locked out after enabling TOTP

Fix: Use your VPS provider’s web console to log in. Edit /etc/pam.d/sshd to remove the pam_google_authenticator.so line, and /etc/ssh/sshd_config.d/98-2fa.conf to remove AuthenticationMethods, then sudo systemctl reload ssh.


Conclusion

Your SSH service is now hardened to a standard that stops automated brute-force attacks completely and makes targeted attacks computationally infeasible. Password authentication is off, root login is blocked, only named users can authenticate, fail2ban bans repeat offenders, and modern elliptic-curve cryptography is enforced. For teams, the SSH CA section eliminates the operational burden of managing authorized_keys files across multiple servers.

The companion article is Ubuntu 24.04 LTS Server Setup Checklist — SSH hardening is Step 4 of that 20-step baseline.


People Also Ask

Should I change the default SSH port (22)?

Changing SSH to a non-standard port (e.g., 2222) reduces automated scanner noise in your logs — most bots only probe port 22. However, it provides no actual security improvement (security through obscurity). A determined attacker runs a full port scan and finds your SSH port in seconds. The configuration changes in this guide — key-only auth, fail2ban, MaxAuthTries — provide real security that doesn’t depend on the port number. If you want to reduce log noise, change the port. If you want actual security, apply the settings in this guide.

How long should SSH keys be valid before rotating?

For personal use, Ed25519 keys can be used indefinitely — the mathematics doesn’t degrade with time. For organisational or production access, rotate keys annually or when: an employee leaves, a laptop is lost or stolen, or a server is decommissioned. SSH certificate authorities make rotation trivial — issue new signed certificates with ssh-keygen -s without touching authorized_keys on any server. For maximum security in high-risk environments, use short-lived SSH certificates (hours to days) with an SSH CA and a zero-trust access proxy like Teleport or Boundary.

Is fail2ban enough to stop SSH brute-force attacks?

fail2ban significantly reduces SSH brute-force success rates but has limitations: it only bans IPs after failures are logged, distributed attacks from many IPs aren’t stopped by per-IP banning, and IPv6 support requires additional configuration. The combination of key-only authentication (PasswordAuthentication no) + fail2ban is what matters: key-only auth makes brute-force mathematically impossible (no password to guess), while fail2ban stops repeated key-probing and reduces server load from scanning bots. Each control reinforces the other.


Further Reading


Tested on: Ubuntu 24.04 LTS (Hetzner CX22), Ubuntu 24.04 LTS (Raspberry Pi 5). OpenSSH 9.6p1. Last verified: April 17, 2026. Report a broken snippet if a command fails after an OpenSSH update.

Further Reading

All Dev Corner

Comments