🗝️ SSH Hardening

Security Series: Part 1 — Security Basics  |  Part 2 — SSH Hardening  |  Part 3 — Firewall Management  |  Part 4 — Auditing & Compliance

SSH — Your Most Critical Service

SSH is the front door to every Linux server. It is also the most attacked service on the internet — any server with port 22 open receives thousands of login attempts per day from automated scanners. A poorly configured SSH daemon is one of the most common entry points for unauthorized access.

This page covers hardening SSH from the ground up: key-based authentication, disabling password login, hardening sshd_config, restricting access, setting up SSH multiplexing, and monitoring for suspicious activity.

Examples

1
Generate and Deploy SSH Key Pairs
# Generate a modern Ed25519 key pair (recommended)
ssh-keygen -t ed25519 -C "craig@binghamton.edu"

# Generate RSA 4096-bit (for older systems that don't support Ed25519)
ssh-keygen -t rsa -b 4096 -C "craig@binghamton.edu"

# Generate with a specific filename
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_bambustudio -C "bambustudio key"

# View your public key
cat ~/.ssh/id_ed25519.pub

# Copy public key to a remote server
ssh-copy-id user@server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
ssh-copy-id -p 2222 user@server    # non-standard port

# Manual method if ssh-copy-id not available
cat ~/.ssh/id_ed25519.pub | ssh user@server \
    "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
     cat >> ~/.ssh/authorized_keys && \
     chmod 600 ~/.ssh/authorized_keys"

# Verify key permissions (must be correct or SSH ignores them)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/authorized_keys
Ed25519 vs RSA: Ed25519 keys are shorter, faster, and more secure than RSA. Use Ed25519 for all new keys. RSA 4096 is acceptable for legacy systems that don't support Ed25519. Never use RSA 1024 or DSA — both are considered broken.
2
Harden sshd_config

The most impactful SSH security changes are in /etc/ssh/sshd_config. Always keep a backup and test before disconnecting:

# Backup first
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig

# Edit the config
sudo vi /etc/ssh/sshd_config
# === Hardened sshd_config settings ===

# Change default port (reduces automated scan noise)
Port 2222

# Only use SSH protocol 2 (protocol 1 is broken)
Protocol 2

# Disable root login -- use a named account + sudo instead
PermitRootLogin no

# Disable password authentication -- keys only
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes

# Disable empty passwords
PermitEmptyPasswords no

# Only allow specific users or groups
AllowUsers craig alice
AllowGroups sysadmin developers

# Limit authentication attempts
MaxAuthTries 3
MaxSessions 5

# Set login grace time (seconds to authenticate)
LoginGraceTime 30

# Disconnect idle sessions after 15 minutes
ClientAliveInterval 300
ClientAliveCountMax 3

# Disable X11 forwarding (unless needed)
X11Forwarding no

# Disable agent forwarding (unless needed)
AllowAgentForwarding no

# Restrict to strong ciphers and MACs
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org

# Log level for security monitoring
LogLevel VERBOSE
# Test config before restarting
sudo sshd -t
echo "Config OK: $?"

# Restart SSH (keep existing session open until verified!)
sudo systemctl restart sshd

# Test from another terminal BEFORE closing current session
ssh -p 2222 craig@server
⚠️ Never close your current SSH session until you have verified the new configuration works from a second connection. A mistake in sshd_config can lock you out permanently. Always test with sshd -t first, then verify the new connection works before closing the old one.
3
SSH Client Configuration (~/.ssh/config)

A well-configured client makes connecting to multiple servers faster, safer, and more convenient:

# ~/.ssh/config -- client-side SSH configuration

# Global defaults
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    IdentitiesOnly yes
    AddKeysToAgent yes

# Specific server aliases
Host bambustudio
    HostName 192.168.1.100
    User craig
    Port 22
    IdentityFile ~/.ssh/id_ed25519_bambustudio

Host centosjump
    HostName 192.168.1.50
    User craig
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

Host prod-web
    HostName web01.example.com
    User deploy
    Port 22
    IdentityFile ~/.ssh/id_ed25519_prod
    ForwardAgent no

# Jump host / bastion configuration
Host internal-db
    HostName 10.0.1.20
    User dbadmin
    ProxyJump bastion.example.com

# Now connect with just:
ssh bambustudio
ssh centosjump
ssh internal-db    # auto-tunnels through bastion
💡 SSH config aliases save time and reduce errors. No more typing ssh -p 2222 -i ~/.ssh/id_ed25519_bambustudio craig@192.168.1.100 — just ssh bambustudio. Consistent aliases also mean you never accidentally connect to the wrong server.
4
SSH Key Management
# List keys loaded in SSH agent
ssh-add -l

# Add a key to the agent
ssh-add ~/.ssh/id_ed25519
ssh-add -t 3600 ~/.ssh/id_ed25519   # expire in 1 hour

# Remove all keys from agent
ssh-add -D

# View authorized_keys on a server
cat ~/.ssh/authorized_keys

# Remove a specific key from authorized_keys
# Edit the file and delete the line, or:
ssh-keygen -R hostname    # remove from known_hosts

# Rotate SSH keys -- generate new, deploy, remove old
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_new -C "new key $(date +%Y%m%d)"
ssh-copy-id -i ~/.ssh/id_ed25519_new.pub user@server
# Test login with new key, then remove old key from authorized_keys

# Audit authorized_keys across all users
sudo find /home /root -name "authorized_keys" -exec echo "=== {} ===" \; \
    -exec cat {} \; 2>/dev/null

# Check key fingerprints
ssh-keygen -lf ~/.ssh/authorized_keys
ssh-keygen -lf ~/.ssh/id_ed25519.pub
Rotate keys periodically. SSH keys don't expire automatically. Establish a policy — rotate annually at minimum, immediately when a team member leaves, and whenever a system is decommissioned. Document who has keys to what.
5
Restrict SSH Access by IP
# Method 1: AllowUsers with from restriction in sshd_config
# Only allow craig to login from specific IP range
AllowUsers craig@192.168.1.*
AllowUsers alice@10.0.0.0/8

# Method 2: TCP Wrappers (/etc/hosts.allow and /etc/hosts.deny)
# /etc/hosts.allow:
sudo tee /etc/hosts.allow << 'EOF'
sshd: 192.168.1.0/255.255.255.0
sshd: 10.0.0.0/255.0.0.0
EOF

# /etc/hosts.deny:
sudo tee /etc/hosts.deny << 'EOF'
sshd: ALL
EOF

# Method 3: firewalld -- allow SSH only from management network
sudo firewall-cmd --zone=public --remove-service=ssh --permanent
sudo firewall-cmd --zone=public \
    --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="ssh" accept' \
    --permanent
sudo firewall-cmd --reload

# Method 4: authorized_keys from= restriction
# In authorized_keys, prefix key with from= to restrict by IP:
# from="192.168.1.*" ssh-ed25519 AAAA... craig@workstation
Layer your SSH restrictions. Use multiple methods together — firewall to limit which IPs can reach port 22, AllowUsers in sshd_config to limit which users can connect, and key-only authentication to eliminate password attacks. Defense in depth for your most critical service.
6
SSH Tunneling and Port Forwarding
# Local port forwarding -- access remote service locally
# Access remote MySQL (port 3306) via local port 3307
ssh -L 3307:localhost:3306 user@dbserver
# Now: mysql -h 127.0.0.1 -P 3307 -u root -p

# Remote port forwarding -- expose local service on remote server
# Make local port 8080 accessible on remote server port 9090
ssh -R 9090:localhost:8080 user@remoteserver

# Dynamic forwarding -- SOCKS proxy through SSH
ssh -D 1080 user@remoteserver
# Configure browser to use SOCKS5 proxy at localhost:1080

# Jump host -- reach an internal server through a bastion
ssh -J bastion.example.com user@internal-server

# Background tunnel (persistent)
ssh -fNL 3307:localhost:3306 user@dbserver
# -f = background, -N = no command, -L = local forward

# Disable tunneling in sshd_config if not needed
# AllowTcpForwarding no
# GatewayPorts no
💡 Disable forwarding on servers that don't need it. SSH tunneling can be used to bypass firewalls from the inside. If a server doesn't need to act as a jump host or tunnel endpoint, set AllowTcpForwarding no and GatewayPorts no in sshd_config.
7
Monitor SSH Activity
# Watch live SSH connections
sudo journalctl -u sshd -f

# View all successful logins today
sudo journalctl -u sshd --since today | grep "Accepted"

# Count failed attempts by source IP
sudo journalctl -u sshd --since "7 days ago" | \
    grep "Failed password" | \
    awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20

# View currently connected SSH sessions
sudo ss -tnp | grep sshd
who
w

# List all SSH sessions with PIDs
sudo ps aux | grep sshd | grep -v grep

# Terminate a specific SSH session
sudo kill -HUP PID

# Check for new authorized_keys additions (via auditd)
sudo ausearch -f /root/.ssh/authorized_keys
sudo ausearch -f /home/alice/.ssh/authorized_keys
journalctl sshd successful login example:
Apr 25 10:22:15 server sshd[12345]: Accepted publickey for craig from 192.168.1.5 port 54321 ssh2: ED25519 SHA256:abc123... Apr 25 10:22:15 server sshd[12345]: pam_unix(sshd:session): session opened for user craig by (uid=0)
8
SSH Hardening Verification Script
#!/bin/bash
# ssh-audit.sh -- verify SSH hardening settings

SSHD_CONFIG="/etc/ssh/sshd_config"
PASS=0
FAIL=0

check() {
    local desc=$1
    local expected=$2
    local actual=$3
    if echo "$actual" | grep -qi "$expected"; then
        echo "  [PASS] $desc"
        ((PASS++))
    else
        echo "  [FAIL] $desc (found: $actual)"
        ((FAIL++))
    fi
}

echo "=== SSH Security Audit: $(hostname) ==="
echo ""

check "Root login disabled" \
    "PermitRootLogin no" \
    "$(grep -i PermitRootLogin $SSHD_CONFIG)"

check "Password auth disabled" \
    "PasswordAuthentication no" \
    "$(grep -i PasswordAuthentication $SSHD_CONFIG)"

check "Empty passwords disabled" \
    "PermitEmptyPasswords no" \
    "$(grep -i PermitEmptyPasswords $SSHD_CONFIG)"

check "X11 forwarding disabled" \
    "X11Forwarding no" \
    "$(grep -i X11Forwarding $SSHD_CONFIG)"

check "Max auth tries set" \
    "MaxAuthTries [1-5]" \
    "$(grep -i MaxAuthTries $SSHD_CONFIG)"

check "Client alive interval set" \
    "ClientAliveInterval" \
    "$(grep -i ClientAliveInterval $SSHD_CONFIG)"

check "SSH running" \
    "active" \
    "$(systemctl is-active sshd)"

echo ""
echo "Results: $PASS passed, $FAIL failed"
[ $FAIL -eq 0 ] && echo "All checks passed!" || echo "Fix failing items above."

Quick Reference

Critical sshd_config Settings

SettingRecommended ValueWhy
PermitRootLoginnoForce use of named accounts with sudo
PasswordAuthenticationnoKeys only — eliminates password brute force
PermitEmptyPasswordsnoNever allow empty password login
MaxAuthTries3Limit guessing attempts per connection
LoginGraceTime3030 seconds to authenticate then disconnect
ClientAliveInterval300Disconnect idle sessions after 15 min
X11ForwardingnoDisable unless GUI forwarding needed
AllowAgentForwardingnoDisable unless acting as jump host
AllowTcpForwardingnoDisable unless tunneling needed
LogLevelVERBOSELog key fingerprints for audit trail

Essential SSH Commands

CommandWhat it does
ssh-keygen -t ed25519 -C "comment"Generate Ed25519 key pair
ssh-copy-id user@serverDeploy public key to server
ssh-add -lList keys in SSH agent
sudo sshd -tTest sshd_config syntax
sudo systemctl restart sshdApply config changes
ssh -L local:host:remote user@serverLocal port forward
ssh -J bastion user@internalJump through bastion host
journalctl -u sshd -fMonitor SSH activity live
ssh-keygen -lf ~/.ssh/authorized_keysShow key fingerprints

← Back to Security Index ↑ Back to EXPANDED