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...
One owner, one affected system, and the next buyer or recovery deadline mapped.
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_legacyWhy Ed25519:
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 AUTHAfter editing, validate and restart:
# Validate config before restarting (prevents lockout!)
sshd -t
# Restart SSH
sudo systemctl restart sshdSSH 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:6379SSH 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.pubConfigure the server to trust the CA:
# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/ca_key.pubConfigure 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">🔑</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">🔑</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/24Automated 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 = synAudit 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
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].
Need the next owner and evidence step mapped?
Send the current system and deadline. Yash replies with the service path, first proof artifact, and handoff owner.