BlogCybersecurity
Cybersecurity

SSH Hardening in 2026: The Complete Guide to Securing Your Linux Server Access

Default SSH configurations leave your server vulnerable to brute-force attacks, credential stuffing, and lateral movement. This guide walks through every hardening measure — from Ed25519 keys and certificate-based auth to port knocking, fail2ban tuning, and audit logging — with exact config files you can deploy today.

S

Sarah Chen

Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.

February 9, 2026
22 min read

Every Linux server exposed to the internet receives thousands of SSH brute-force attempts per day. Shodan indexes over 22 million SSH services globally, and automated botnets cycle through lists of default credentials, weak passwords, and known CVEs within minutes of a new server going online. In January 2026, the CVE-2024-6387 (regreSSHion) vulnerability in OpenSSH demonstrated that even the SSH daemon itself can have critical remote code execution bugs. Hardening SSH is not optional — it is the single most important security measure for any Linux server.

This guide covers every layer of SSH hardening, from cryptographic key selection to kernel-level restrictions. Each section includes exact configuration directives you can copy into your /etc/ssh/sshd_config or drop into /etc/ssh/sshd_config.d/ as a modular file. We test every change on Ubuntu 24.04 LTS and AlmaLinux 9, but the directives apply to any modern OpenSSH 9.x installation.

Step 1: Upgrade to OpenSSH 9.x and Verify Your Version

Before configuring anything, make sure you are running a patched version of OpenSSH. The regreSSHion vulnerability (CVE-2024-6387) affected OpenSSH versions 8.5p1 through 9.7p1 on glibc-based Linux systems. The fix was released in OpenSSH 9.8p1. Check your version:

ssh -V
# Expected output: OpenSSH_9.9p1, OpenSSL 3.x.x

# On Ubuntu/Debian:
sudo apt update && sudo apt install openssh-server -y

# On RHEL/AlmaLinux:
sudo dnf update openssh-server -y

# Verify after update:
ssh -V
sshd -T | head -5

The sshd -T command prints the effective runtime configuration after all config files are parsed. This is the ground truth of what your SSH daemon actually uses — always verify with this command after making changes.

Step 2: Generate Ed25519 Keys and Disable RSA

RSA keys are not broken, but Ed25519 keys are objectively superior in every measurable way: they are faster to generate, faster to verify, produce smaller signatures (64 bytes vs 256+ bytes for RSA-2048), and use the Curve25519 elliptic curve which has no known timing side-channel vulnerabilities. There is no reason to use RSA for new deployments in 2026.

# Generate a new Ed25519 key pair (on your LOCAL machine, not the server):
ssh-keygen -t ed25519 -C "yourname@yourdomain.com" -f ~/.ssh/id_ed25519

# If you need a passphrase-protected key with a modern KDF:
ssh-keygen -t ed25519 -C "yourname@yourdomain.com" -a 100 -f ~/.ssh/id_ed25519
# The -a 100 flag sets 100 rounds of the bcrypt KDF for passphrase protection.
# This makes brute-forcing a stolen key file significantly harder.

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

# Verify the key is in authorized_keys:
ssh user@your-server-ip "cat ~/.ssh/authorized_keys"

On the server side, regenerate the host keys to remove DSA and ECDSA, keeping only Ed25519:

# Remove old host keys:
sudo rm /etc/ssh/ssh_host_dsa_key* /etc/ssh/ssh_host_ecdsa_key* /etc/ssh/ssh_host_rsa_key*

# Regenerate only Ed25519 host key:
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

# In sshd_config, specify only this host key:
# HostKey /etc/ssh/ssh_host_ed25519_key

Step 3: The Complete sshd_config Hardening Template

Here is a battle-tested /etc/ssh/sshd_config.d/99-hardening.conf file. On modern OpenSSH, files in sshd_config.d/ are included automatically and override defaults. Using a separate file keeps your changes clean across OS upgrades:

# /etc/ssh/sshd_config.d/99-hardening.conf
# ZeonEdge SSH Hardening — 2026

# === Authentication ===
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 3
LoginGraceTime 20

# === Cryptography ===
HostKey /etc/ssh/ssh_host_ed25519_key
PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com
HostKeyAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

# === Network ===
Port 2222
AddressFamily inet
ListenAddress 0.0.0.0
TCPKeepAlive yes
ClientAliveInterval 300
ClientAliveCountMax 2

# === Restrictions ===
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no
PermitUserEnvironment no
DisableForwarding yes

# === Logging ===
LogLevel VERBOSE
SyslogFacility AUTH

# === Allowed Users (whitelist approach) ===
AllowUsers deploy admin

After saving this file, always validate before restarting the daemon. A syntax error in sshd_config will lock you out of the server:

# Test the configuration:
sudo sshd -t

# If no output, the config is valid. Restart:
sudo systemctl restart sshd

# CRITICAL: Keep your current SSH session open and test a NEW connection
# in a separate terminal before closing the existing one.
ssh -p 2222 deploy@your-server-ip -i ~/.ssh/id_ed25519

Step 4: SSH Certificate-Based Authentication

SSH certificates solve the "first connection" trust problem and eliminate the need to distribute authorized_keys files across servers. Instead of trusting individual public keys, you trust a Certificate Authority (CA) that signs user and host keys. This is the approach used by companies like Facebook, Google, and Netflix for managing SSH access at scale.

# Step 1: Generate a CA key pair (keep the private key EXTREMELY secure):
ssh-keygen -t ed25519 -f /etc/ssh/ca_user_key -C "SSH User CA"

# Step 2: Sign a user's public key with the CA:
ssh-keygen -s /etc/ssh/ca_user_key \
    -I "marcus@zeonedge.com" \
    -n deploy,admin \
    -V +52w \
    -z 1001 \
    ~/.ssh/id_ed25519.pub

# Breakdown:
# -s: signing key (the CA private key)
# -I: certificate identity (for logging, NOT authorization)
# -n: principals (usernames this cert is valid for)
# -V: validity period (+52w = 1 year)
# -z: serial number (for revocation tracking)

# Step 3: On the server, trust the CA public key:
echo "TrustedUserCAKeys /etc/ssh/ca_user_key.pub" | sudo tee -a /etc/ssh/sshd_config.d/99-hardening.conf

# Step 4: Inspect the certificate:
ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub

Certificate advantages over authorized_keys: centralized revocation (revoke one cert without touching servers), expiration (certs auto-expire, keys do not), audit trail (certificate identity is logged), and principals (one cert can authorize multiple usernames). For teams with more than 5 servers, certificates are dramatically easier to manage.

Step 5: Fail2Ban Configuration for SSH

Fail2Ban monitors log files for authentication failures and dynamically creates firewall rules to block offending IPs. The default configuration is too lenient for production servers. Here is an aggressive but practical configuration:

# /etc/fail2ban/jail.d/sshd.conf
[sshd]
enabled = true
port = 2222
filter = sshd
backend = systemd
maxretry = 3
findtime = 600
bantime = 86400
banaction = nftables-multiport
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16

# Aggressive mode: ban after first failure for repeat offenders
[sshd-aggressive]
enabled = true
port = 2222
filter = sshd[mode=aggressive]
backend = systemd
maxretry = 1
findtime = 86400
bantime = 604800
banaction = nftables-multiport
# Install and enable:
sudo apt install fail2ban -y   # Debian/Ubuntu
sudo dnf install fail2ban -y   # RHEL/AlmaLinux

sudo systemctl enable --now fail2ban

# Check status:
sudo fail2ban-client status sshd

# View banned IPs:
sudo fail2ban-client get sshd banned

# Manually unban an IP:
sudo fail2ban-client set sshd unbanip 1.2.3.4

The two-jail approach works well: the first jail bans IPs after 3 failures within 10 minutes for 24 hours. The second jail catches repeat offenders — if an IP that was previously banned tries again, it gets banned for 7 days after just 1 failure. This dramatically reduces noise while keeping legitimate users safe from accidental lockouts (the ignoreip directive whitelists your internal networks).

Step 6: Port Knocking with nftables

Port knocking hides your SSH port from scanners. The SSH port remains closed until a client sends a specific sequence of connection attempts to other ports. While not a replacement for strong authentication, it eliminates 99.9% of automated scanning noise and reduces your attack surface to near zero:

# /etc/nftables.d/port-knocking.nft
table inet port_knocking {
    set knock_stage1 {
        type ipv4_addr
        timeout 10s
    }
    set knock_stage2 {
        type ipv4_addr
        timeout 10s
    }
    set allowed {
        type ipv4_addr
        timeout 30s
    }

    chain input {
        type filter hook input priority -1; policy accept;

        # Stage 1: knock on port 7000
        tcp dport 7000 add @knock_stage1 { ip saddr } drop

        # Stage 2: knock on port 8000 (only if stage 1 completed)
        ip saddr @knock_stage1 tcp dport 8000 add @knock_stage2 { ip saddr } drop

        # Stage 3: knock on port 9000 (only if stage 2 completed)
        ip saddr @knock_stage2 tcp dport 9000 add @allowed { ip saddr } drop

        # Allow SSH only from IPs that completed all 3 knocks:
        tcp dport 2222 ip saddr @allowed accept
        tcp dport 2222 drop
    }
}
# Client-side knock script:
#!/bin/bash
HOST="your-server-ip"
for port in 7000 8000 9000; do
    nmap -Pn --max-retries 0 -p $port $HOST >/dev/null 2>&1
    sleep 0.5
done
ssh -p 2222 deploy@$HOST

Step 7: Audit Logging and Intrusion Detection

With LogLevel VERBOSE in sshd_config, OpenSSH logs the key fingerprint used for each authentication. This lets you track exactly which key was used for each login, even when multiple keys are authorized for the same user:

# View SSH authentication logs:
sudo journalctl -u sshd --since "1 hour ago" | grep -i "accepted\|failed\|invalid"

# Set up auditd rules for SSH-related file changes:
sudo tee /etc/audit/rules.d/ssh.rules <<'AUDIT'
-w /etc/ssh/sshd_config -p wa -k ssh_config_change
-w /etc/ssh/sshd_config.d/ -p wa -k ssh_config_change
-w /root/.ssh/authorized_keys -p wa -k ssh_authorized_keys
-w /home/ -p wa -k ssh_home_changes
AUDIT

sudo systemctl restart auditd

# Search audit logs:
sudo ausearch -k ssh_config_change --interpret

For teams that need real-time alerting, pipe SSH logs to a SIEM (Wazuh, Elastic SIEM, or Grafana Loki) and create alerts for: failed authentication from new source IPs, successful root login (which should never happen if PermitRootLogin is disabled), and authorized_keys file modifications.

Step 8: Two-Factor Authentication with FIDO2/WebAuthn Keys

Hardware security keys (YubiKey 5, SoloKey, Google Titan) provide phishing-resistant two-factor authentication for SSH. OpenSSH 8.2+ supports FIDO2/U2F keys natively using the sk-ssh-ed25519@openssh.com key type (sk = security key):

# Generate a FIDO2-backed SSH key (requires a hardware key plugged in):
ssh-keygen -t ed25519-sk -C "marcus-yubikey@zeonedge.com" -f ~/.ssh/id_ed25519_sk

# The -sk variant requires the physical key to be present for every authentication.
# This means even if someone steals your private key file, they cannot use it
# without the physical hardware key.

# For resident keys (key stored ON the hardware key, not on disk):
ssh-keygen -t ed25519-sk -O resident -C "marcus-yubikey@zeonedge.com"

# Require both a regular key AND a FIDO2 key (true 2FA):
# In sshd_config:
# AuthenticationMethods publickey,publickey
# This requires two separate successful public key authentications.

FIDO2 keys are the gold standard for SSH authentication in 2026. They prevent credential theft, phishing, and key exfiltration. For organizations subject to SOC 2, PCI-DSS, or ISO 27001 compliance, hardware key requirements satisfy the "multi-factor authentication for administrative access" control.

Verification Checklist

After implementing all hardening measures, run this verification script to confirm everything is properly configured:

#!/bin/bash
echo "=== SSH Hardening Verification ==="

echo -n "OpenSSH version: "
ssh -V 2>&1

echo -n "Root login: "
sshd -T 2>/dev/null | grep -i "permitrootlogin" | awk '{print $2}'

echo -n "Password auth: "
sshd -T 2>/dev/null | grep -i "passwordauthentication" | awk '{print $2}'

echo -n "Host key algorithms: "
sshd -T 2>/dev/null | grep -i "hostkeyalgorithms"

echo -n "SSH port: "
sshd -T 2>/dev/null | grep -i "^port " | awk '{print $2}'

echo -n "Fail2Ban status: "
sudo fail2ban-client status sshd 2>/dev/null | grep "Currently banned" || echo "NOT RUNNING"

echo -n "Listening SSH ports: "
sudo ss -tlnp | grep sshd

echo "=== End Verification ==="

SSH hardening is not a one-time task. Subscribe to the OpenSSH release announcements, monitor CVE databases for new vulnerabilities, and rotate your CA-signed certificates before they expire. At ZeonEdge, we implement automated SSH hardening as part of our server provisioning pipeline — every new server is hardened before it receives its first connection. Learn about our infrastructure security services.

S

Sarah Chen

Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.