SSH Hardening and Key Management: The Complete Security Guide

Secure your SSH infrastructure with modern hardening techniques. Covers key types, agent forwarding, certificate-based auth, jump hosts, and automated key...

Y
Yash Pritwani
14 min read

SSH Is Your Front Door

SSH is the most common entry point into servers. A poorly configured SSH setup is an open invitation for attackers. Credential stuffing, brute force, and stolen keys account for over 60% of server compromises. Let us fix that.

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="220" rx="12" fill="#1a1a2e"/><path d="M300,25 L380,55 L380,120 Q380,170 300,195 Q220,170 220,120 L220,55 Z" fill="none" stroke="#6366f1" stroke-width="2.5"/><path d="M300,40 L365,65 L365,118 Q365,160 300,180 Q235,160 235,118 L235,65 Z" fill="#6366f1" opacity="0.15"/><rect x="280" y="95" width="40" height="30" rx="4" fill="#6366f1" opacity="0.9"/><path d="M288,95 L288,82 Q288,72 300,72 Q312,72 312,82 L312,95" fill="none" stroke="#6366f1" stroke-width="2.5"/><circle cx="300" cy="110" r="4" fill="#ffffff"/><text x="90" y="60" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="system-ui">Firewall</text><line x1="130" y1="57" x2="218" y2="57" stroke="#3b82f6" stroke-width="1" stroke-dasharray="3,3"/><text x="90" y="100" text-anchor="middle" fill="#a855f7" font-size="10" font-family="system-ui">WAF</text><line x1="110" y1="97" x2="220" y2="85" stroke="#a855f7" stroke-width="1" stroke-dasharray="3,3"/><text x="90" y="140" text-anchor="middle" fill="#2dd4bf" font-size="10" font-family="system-ui">SSO / MFA</text><line x1="130" y1="137" x2="222" y2="120" stroke="#2dd4bf" stroke-width="1" stroke-dasharray="3,3"/><text x="510" y="60" text-anchor="middle" fill="#f59e0b" font-size="10" font-family="system-ui">TLS/SSL</text><line x1="470" y1="57" x2="382" y2="57" stroke="#f59e0b" stroke-width="1" stroke-dasharray="3,3"/><text x="510" y="100" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="system-ui">RBAC</text><line x1="490" y1="97" x2="380" y2="85" stroke="#3b82f6" stroke-width="1" stroke-dasharray="3,3"/><text x="510" y="140" text-anchor="middle" fill="#a855f7" font-size="10" font-family="system-ui">Audit Logs</text><line x1="470" y1="137" x2="378" y2="120" stroke="#a855f7" stroke-width="1" stroke-dasharray="3,3"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Defense in depth: multiple security layers protect your infrastructure from threats.</p></div>

Key Generation — Use Ed25519

RSA keys are still common, but Ed25519 is superior in every way:

# Generate an Ed25519 key (recommended)
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/id_ed25519_prod

# If you need RSA compatibility (legacy systems), use 4096 bits minimum
ssh-keygen -t rsa -b 4096 -C "[email protected]" -f ~/.ssh/id_rsa_legacy

Why Ed25519:

Faster: Key generation, signing, and verification are all faster
Smaller keys: 256-bit vs 3072-4096-bit RSA
No weak key risk: RSA keys can be generated poorly; Ed25519 cannot
Constant-time: Resistant to side-channel attacks

SSH Server Hardening

Edit /etc/ssh/sshd_config with these security settings:

# /etc/ssh/sshd_config — Hardened Configuration

# Disable password authentication entirely
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
PubkeyAuthentication yes

# Disable root login (use sudo instead)
PermitRootLogin no

# Restrict to specific users/groups
AllowGroups ssh-users

# Use only protocol 2
Protocol 2

# Strong key exchange algorithms
KexAlgorithms [email protected],curve25519-sha256,[email protected]
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]

# Disable unused features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no

# Rate limiting
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30

# Idle timeout (disconnect after 10 min idle)
ClientAliveInterval 300
ClientAliveCountMax 2

# Logging
LogLevel VERBOSE
SyslogFacility AUTH

After editing, validate and restart:

# Validate config before restarting (prevents lockout!)
sshd -t

# Restart SSH
sudo systemctl restart sshd

SSH Config for Clients

Organize your connections with ~/.ssh/config:

# ~/.ssh/config

# Default settings for all hosts
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentitiesOnly yes

# Production server via jump host
Host prod
    HostName 10.0.1.50
    User deploy
    IdentityFile ~/.ssh/id_ed25519_prod
    ProxyJump jump.example.com

# Jump/Bastion host
Host jump.example.com
    User jumpuser
    IdentityFile ~/.ssh/id_ed25519_jump
    ForwardAgent no

# Development server
Host dev
    HostName dev.example.com
    User developer
    IdentityFile ~/.ssh/id_ed25519_dev
    LocalForward 5432 localhost:5432
    LocalForward 6379 localhost:6379

SSH Certificate Authority

For teams larger than 3 people, SSH certificates are far superior to distributing public keys:

# Create a CA key pair (do this once, guard the private key)
ssh-keygen -t ed25519 -f /etc/ssh/ca_key -C "SSH CA"

# Sign a user's public key (valid 8 hours, for user "deploy")
ssh-keygen -s /etc/ssh/ca_key \
  -I "deploy@company" \
  -n deploy \
  -V +8h \
  ~/.ssh/id_ed25519.pub

# Sign a host key (valid 1 year)
ssh-keygen -s /etc/ssh/ca_key \
  -I "web1.example.com" \
  -h \
  -n web1.example.com \
  -V +52w \
  /etc/ssh/ssh_host_ed25519_key.pub

Configure the server to trust the CA:

# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/ca_key.pub

Configure clients to trust host certificates:

# ~/.ssh/known_hosts
@cert-authority *.example.com ssh-ed25519 AAAA...

Now you never need to manage authorized_keys files or deal with host key verification warnings again.

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 150" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="150" rx="12" fill="#1a1a2e"/><rect x="30" y="40" width="100" height="55" rx="6" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="80" y="60" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="monospace">Hello World</text><text x="80" y="80" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Plaintext</text><rect x="175" y="30" width="90" height="75" rx="8" fill="#6366f1" opacity="0.85"/><text x="220" y="55" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Encrypt</text><text x="220" y="72" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">AES-256</text><text x="220" y="92" text-anchor="middle" fill="#f59e0b" font-size="20" font-family="system-ui">&#x1f511;</text><rect x="310" y="40" width="100" height="55" rx="6" fill="none" stroke="#a855f7" stroke-width="1.5"/><text x="360" y="60" text-anchor="middle" fill="#a855f7" font-size="10" font-family="monospace">x8f2...k9z</text><text x="360" y="80" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Ciphertext</text><rect x="455" y="30" width="90" height="75" rx="8" fill="#2dd4bf" opacity="0.85"/><text x="500" y="55" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Decrypt</text><text x="500" y="72" text-anchor="middle" fill="#1a1a2e" font-size="9" font-family="system-ui">AES-256</text><text x="500" y="92" text-anchor="middle" fill="#f59e0b" font-size="20" font-family="system-ui">&#x1f511;</text><defs><marker id="arrow6" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="132" y1="67" x2="173" y2="67" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow6)"/><line x1="267" y1="67" x2="308" y2="67" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow6)"/><line x1="412" y1="67" x2="453" y2="67" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow6)"/><text x="300" y="130" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">Symmetric Encryption: same key encrypts and decrypts</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Encryption transforms readable plaintext into unreadable ciphertext, reversible only with the correct key.</p></div>

Fail2Ban Configuration

# /etc/fail2ban/jail.d/sshd.conf
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
ignoreip = 192.168.1.0/24

Automated Key Rotation

Create a script to rotate SSH keys quarterly:

#!/bin/bash
# rotate-ssh-keys.sh
KEY_DIR="\$HOME/.ssh"
OLD_KEY="\$KEY_DIR/id_ed25519_prod"
NEW_KEY="\$KEY_DIR/id_ed25519_prod_new"
SERVERS="web1 web2 db1"

# Generate new key
ssh-keygen -t ed25519 -f "\$NEW_KEY" -N "" -C "deploy@company-\$(date +%Y%m)"

# Deploy new key to all servers
for server in \$SERVERS; do
    ssh-copy-id -i "\$NEW_KEY.pub" "\$server"
    echo "Deployed to \$server"
done

# Test new key works
for server in \$SERVERS; do
    ssh -i "\$NEW_KEY" "\$server" "echo 'New key works'" || {
        echo "FAILED on \$server — aborting rotation"
        exit 1
    }
done

# Remove old key from servers
for server in \$SERVERS; do
    OLD_PUB=\$(cat "\$OLD_KEY.pub")
    ssh -i "\$NEW_KEY" "\$server" "sed -i '\\|\$OLD_PUB|d' ~/.ssh/authorized_keys"
done

# Replace old key locally
mv "\$OLD_KEY" "\$OLD_KEY.bak.\$(date +%Y%m%d)"
mv "\$OLD_KEY.pub" "\$OLD_KEY.pub.bak.\$(date +%Y%m%d)"
mv "\$NEW_KEY" "\$OLD_KEY"
mv "\$NEW_KEY.pub" "\$OLD_KEY.pub"

echo "Key rotation complete"

Port Knocking (Defense in Depth)

Add port knocking as an additional layer:

# Install knockd
sudo apt install knockd

# /etc/knockd.conf
[options]
    UseSyslog

[openSSH]
    sequence    = 7000,8000,9000
    seq_timeout = 5
    command     = ufw allow from %IP% to any port 22
    tcpflags    = syn

[closeSSH]
    sequence    = 9000,8000,7000
    seq_timeout = 5
    command     = ufw delete allow from %IP% to any port 22
    tcpflags    = syn

Audit Logging

Track who logged in, when, and what they did:

# Enable session recording with script command
# Add to /etc/profile
if [ -n "\$SSH_CONNECTION" ]; then
    LOGDIR="/var/log/ssh-sessions"
    mkdir -p "\$LOGDIR"
    LOGFILE="\$LOGDIR/\$(whoami)_\$(date +%Y%m%d_%H%M%S)_\$\$.log"
    script -qf "\$LOGFILE"
fi

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 180" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="180" rx="12" fill="#1a1a2e"/><circle cx="60" cy="90" r="20" fill="none" stroke="#3b82f6" stroke-width="2"/><text x="60" y="94" text-anchor="middle" fill="#3b82f6" font-size="11" font-family="system-ui">User</text><rect x="120" y="65" width="95" height="50" rx="8" fill="#6366f1" opacity="0.85"/><text x="167" y="85" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Identity</text><text x="167" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Verify</text><rect x="250" y="65" width="95" height="50" rx="8" fill="#a855f7" opacity="0.85"/><text x="297" y="85" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Policy</text><text x="297" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Engine</text><rect x="380" y="65" width="95" height="50" rx="8" fill="#2dd4bf" opacity="0.85"/><text x="427" y="85" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Access</text><text x="427" y="100" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Proxy</text><rect x="510" y="65" width="60" height="50" rx="8" fill="#f59e0b" opacity="0.85"/><text x="540" y="94" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">App</text><defs><marker id="arrow5" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="82" y1="90" x2="118" y2="90" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow5)"/><line x1="217" y1="90" x2="248" y2="90" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow5)"/><line x1="347" y1="90" x2="378" y2="90" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow5)"/><line x1="477" y1="90" x2="508" y2="90" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow5)"/><text x="167" y="140" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">MFA + Device</text><text x="297" y="140" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Least Privilege</text><text x="427" y="140" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Encrypted Tunnel</text><text x="300" y="165" text-anchor="middle" fill="#6366f1" font-size="11" font-family="system-ui" font-weight="bold">Never Trust, Always Verify</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Zero Trust architecture: every request is verified through identity, policy, and access proxy layers.</p></div>

Security Checklist

Disable password authentication
Use Ed25519 keys exclusively
Set PermitRootLogin to no
Configure AllowGroups
Install Fail2Ban with strict limits
Enable verbose logging
Use SSH certificates for teams
Rotate keys quarterly
Use jump hosts for internal servers
Monitor auth logs with alerting

At TechSaaS, we follow all of these practices. Our servers are accessible only via Ed25519 keys through a jump host, with CrowdSec providing real-time intrusion prevention. Security is not a feature — it is the foundation.

Need an SSH security audit? Reach out at [email protected].

#ssh#security#hardening#key-management#linux

Need help with security?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.