How to Harden SSH Securely Without Locking Yourself Out
This guide explains why the default SSH configuration is vulnerable, walks through protocol basics, key generation, server hardening settings, step‑by‑step procedures to avoid being locked out, key management best practices, troubleshooting tips, and provides a complete hardening script for Linux systems.
Background and Problem
SSH (Secure Shell) is the primary protocol for remote management of Linux servers. The default configuration is insecure: port 22 receives brute‑force attacks, password authentication can be cracked or leaked, and root login is risky. Over‑aggressive hardening can also lock administrators out.
1. SSH Protocol Basics
1.1 Protocol Versions
SSH has two versions, SSHv1 (deprecated) and SSHv2. Modern systems use SSHv2, which provides stronger ciphers (AES, ChaCha20), a full Diffie‑Hellman key‑exchange, and public‑key authentication.
# Show client version
ssh -V
# Show server version
sshd -V
# Test protocol version (v1 should fail)
ssh -1 user@host
ssh -2 user@host1.2 Connection Process
The connection consists of TCP handshake, protocol version exchange, key exchange (Diffie‑Hellman/ECDH), server authentication, user authentication (password, public key, etc.), and session establishment.
# Detailed debug output
ssh -vvv user@host
# Output sections: debug1 (connection & key exchange), debug2 (authentication), debug3 (session)1.3 Key Types
Supported key types include RSA (commonly 4096‑bit), Ed25519 (modern, fast, secure), and ECDSA (curve‑dependent, may have compatibility issues).
# Generate RSA key (4096‑bit)
ssh-keygen -t rsa -b 4096 -C "[email protected]"
# Generate Ed25519 key (recommended)
ssh-keygen -t ed25519 -C "[email protected]"
# Generate ECDSA key (521‑bit)
ssh-keygen -t ecdsa -b 521 -C "[email protected]"
# Store key at a custom location
ssh-keygen -t ed25519 -f ~/.ssh/my_server_key -C "my_server"
# Add password protection (new OpenSSH format, -a sets KDF rounds)
ssh-keygen -t ed25519 -o -a 100 -C "[email protected]"2. SSH Server Configuration
2.1 Configuration File Locations
The server configuration file is /etc/ssh/sshd_config; the client configuration is ~/.ssh/config. This guide focuses on the server side.
# View default configuration
man sshd_config
# Show effective configuration
sshd -T
# Check syntax without restarting
sshd -t
# Test configuration for a specific user/host
sshd -T -C user=root,host=localhost,addr=127.0.0.12.2 Core Security Settings
Key settings that directly affect security are listed below. Apply each change, verify, then restart the service.
# Edit server config
vim /etc/ssh/sshd_config
# Recommended settings:
Port 2222 # Change default port
Protocol 2 # Disable SSHv1
ListenAddress 0.0.0.0 # Optional: bind to all interfaces
PermitEmptyPasswords no
PasswordAuthentication no
ChallengeResponseAuthentication no
PermitRootLogin no
AllowUsers admin [email protected]/24
AllowGroups sshusers
IgnoreRhosts yes
HostbasedAuthentication no
GSSAPIAuthentication no
PubkeyAuthentication yes
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
MaxSessions 10
LoginGraceTime 30
PermitUserEnvironment no
IgnoreUserKnownHosts yes
PrintMotd no
DisableForwarding yes
X11Forwarding no
Subsystem sftp internal-sftp -l INFO
Match Group sftpusers
ChrootDirectory /var/sftp
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no2.3 Verify Configuration Before Restart
# Check syntax
sshd -t
# If no output, syntax is OK
# Show selected effective options
sshd -T | grep -E "^passwordauthentication|^permitrootlogin|^pubkeyauthentication"
# Restart service
systemctl restart sshd
systemctl status sshd3. Public‑Key Authentication
3.1 Principle
Clients hold a private key; the public key is placed on the server. During login the client signs random data with the private key, the server verifies the signature with the public key. This eliminates password‑based brute‑force attacks.
3.2 Server‑Side Configuration
# Enable public‑key auth
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
# Ensure correct permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys3.3 Client‑Side Setup and Management
# Generate a new key pair
ssh-keygen -t ed25519
# Copy public key to server (simplest)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
# Manual copy
cat ~/.ssh/id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# Verify permissions on server
ssh user@host "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
# List keys loaded in the agent
ssh-add -l3.4 Managing authorized_keys
# View current keys
ssh user@host "cat ~/.ssh/authorized_keys"
# Append a new key
ssh user@host "cat >> ~/.ssh/authorized_keys" < ~/.ssh/new_key.pub
# Remove a specific key (example)
ssh user@host "grep -v 'ssh-rsa AAAAB3...' ~/.ssh/authorized_keys > /tmp/auth_keys && mv /tmp/auth_keys ~/.ssh/authorized_keys"
# Batch management script (example)
#!/bin/bash
AUTH_KEYS_FILE="$HOME/.ssh/authorized_keys"
USER_KEYS_DIR="$HOME/.ssh/user_keys"
mkdir -p "$USER_KEYS_DIR"
for user in alice bob charlie; do
mkdir -p "$USER_KEYS_DIR/$user"
done
{
echo "# Alice's keys"
cat "$USER_KEYS_DIR/alice/"*.pub 2>/dev/null
echo "# Bob's keys"
cat "$USER_KEYS_DIR/bob/"*.pub 2>/dev/null
} > "$AUTH_KEYS_FILE"4. Avoid Locking Yourself Out
4.1 Preparation Before Editing
Keep an active SSH session as an emergency recovery channel, back up the current configuration, and ensure console or out‑of‑band access.
# Backup configuration
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.$(date +%Y%m%d)
cp /etc/ssh/sshd_config /root/sshd_config.backup
# Verify syntax
sshd -t4.2 Incremental Changes
Modify only one setting at a time, test the new configuration, then proceed to the next change.
# Step 1: Change port only
Port 2222
sshd -t && systemctl restart sshd
# Test new port while keeping the old session open
ssh -p 2222 user@host
# After confirming, move to the next setting4.3 Bulk Changes with Ansible
# Change port on all hosts
ansible all -i inventory -m lineinfile \
-a "path=/etc/ssh/sshd_config regexp='^Port' line='Port 2222'"
# Verify syntax on all hosts
ansible all -i inventory -m command -a "sshd -t"
# Restart SSH service
ansible all -i inventory -m systemd -a "name=sshd state=restarted"4.4 Proper Order for Disabling Root Login
# 1. Create a sudo‑capable admin user
useradd -m -s /bin/bash admin
usermod -aG sudo admin
# 2. Set up public‑key login for admin
ssh-copy-id admin@host
# 3. Verify sudo works
ssh admin@host
sudo -i
# 4. After verification, disable root login
PermitRootLogin no
# 5. Keep at least one out‑of‑band access method (console, rescue mode)4.5 Fail2Ban for Brute‑Force Protection
# Install Fail2Ban
apt-get install fail2ban # Debian/Ubuntu
yum install fail2ban # RHEL/CentOS
# Configure jail for SSH on the new port
cat > /etc/fail2ban/jail.local <<'EOF'
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 600
bantime = 3600
EOF
# Enable and start service
systemctl enable fail2ban
systemctl start fail2ban
# Check status
fail2ban-client status sshd4.6 Emergency Recovery Methods
Cloud console login (web UI) to access the instance as root.
Cloud user‑data script executed on next boot to fix the config.
Boot into single‑user mode by editing GRUB (add single or init=/bin/bash), mount the root filesystem read‑write, and edit /etc/ssh/sshd_config.
Provider’s rescue mode: attach the disk to another instance, mount it, edit the config, then reboot.
5. Key Management Best Practices
5.1 Storage and Permissions
# Private key permissions
chmod 600 ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_rsa
# Public key can be 644
chmod 644 ~/.ssh/id_ed25519.pub
# .ssh directory must be 700
chmod 700 ~/.ssh
# Server‑side authorized_keys must be 600
chmod 600 ~/.ssh/authorized_keys
# Home directory should not be group‑writable
chmod go-w ~5.2 Key Rotation
# Create a new key for 2024
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_2024 -C "rotation 2024"
# Add new public key to server
ssh user@host "cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_ed25519_2024.pub
# Test the new key
ssh -i ~/.ssh/id_ed25519_2024 user@host
# After confirming, remove old key from authorized_keys
ssh user@host "grep -v 'old_key_comment' ~/.ssh/authorized_keys > /tmp/auth && mv /tmp/auth ~/.ssh/authorized_keys"
# Delete old private key locally
mv ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.old5.3 Using SSH Agent Forwarding
# Start the agent
eval "$(ssh-agent -s)"
# Add key to the agent
ssh-add ~/.ssh/id_ed25519
# List loaded keys
ssh-add -l
# Enable forwarding in ~/.ssh/config
Host jump-server
HostName jumphost.example.com
User admin
Port 2222
ForwardAgent yes
# Or use -A on the command line
ssh -A [email protected]5.4 Managing Multiple Hosts with Config File
# ~/.ssh/config example
Host *
Port 2222
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 300
ServerAliveCountMax 2
StrictHostKeyChecking ask
Host prod-*
HostName %h.example.com
User admin
ForwardAgent no
LogLevel INFO
Host jump
HostName jumphost.example.com
User admin
Port 2222
ForwardAgent yes
Host db-1
HostName 192.168.1.100
User dbadmin
ProxyJump jump
LocalForward 3306 127.0.0.1:3306
Host github
HostName github.com
User git
IdentityFile ~/.ssh/github_ed255196. Connection Troubleshooting
6.1 Common Errors
Connection refused
# Check if sshd is running
systemctl status sshd
# Verify listening ports
ss -tlnp | grep sshd
netstat -tlnp | grep sshd
# Check firewall rules
iptables -L -n | grep 22
ufw status
firewall-cmd --list-allPermission denied
# Verify username
ssh user@host
# Debug authentication
ssh -vvv user@host
# Inspect server logs
tail -f /var/log/auth.log
journalctl -u sshd -f
# Specify key explicitly
ssh -i ~/.ssh/specific_key user@hostConnection timeout
# Test network reachability
ping -c 4 host
traceroute host # Linux
tracert host # Windows
# Test port accessibility
nc -zv host 22
telnet host 22
# Try binding to a specific source IP
ssh -b bind_ip user@host6.2 Detailed Logging
# Client side verbose output
ssh -vvv user@host
# Server side real‑time logs
tail -f /var/log/auth.log
journalctl -u sshd -f
# Force a specific KEX algorithm for debugging
ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 user@host6.3 Service Status Checks
# Service status
systemctl status sshd
# Listening sockets
ss -tlnp | grep sshd
# Test local connection
ssh localhost
# SELinux context (RHEL/CentOS)
getsebool -a | grep ssh
setsebool -P ssh_sysadm_login on7. Complete SSH Hardening Script
#!/bin/bash
# ssh_hardening.sh - SSH security hardening script
set -euo pipefail
BACKUP_DIR="/root/ssh_backups"
mkdir -p "$BACKUP_DIR"
backup_config() {
cp /etc/ssh/sshd_config "${BACKUP_DIR}/sshd_config.$(date +%Y%m%d_%H%M%S)"
echo "Backup created in $BACKUP_DIR"
}
verify_sudo() {
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root"
exit 1
fi
}
verify_admin_user() {
if ! id admin &>/dev/null; then
echo "Creating admin user..."
useradd -m -s /bin/bash admin
usermod -aG sudo admin
fi
if [ ! -f "/home/admin/.ssh/authorized_keys" ]; then
echo "WARNING: admin user has no SSH keys configured!"
echo "Please add your public key to /home/admin/.ssh/authorized_keys before continuing"
read -p "Press Enter to continue anyway..." dummy
fi
}
apply_config() {
cat > /etc/ssh/sshd_config <<'EOF'
# SSH Server Configuration
Port 2222
Protocol 2
ListenAddress 0.0.0.0
# Authentication
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
PermitEmptyPasswords no
MaxAuthTries 3
LoginGraceTime 30
# Security
IgnoreRhosts yes
HostbasedAuthentication no
PermitUserEnvironment no
PrintMotd no
TCPKeepAlive yes
# Idle timeout
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable unused features
X11Forwarding no
AllowTcpForwarding no
PermitTunnel no
AllowAgentForwarding no
# SFTP configuration
Subsystem sftp internal-sftp -l INFO
# Override for SFTP users
Match Group sftpusers
ChrootDirectory /var/sftp
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
EOF
echo "Configuration applied"
}
set_permissions() {
chmod 644 /etc/ssh/sshd_config
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keys
}
verify_config() {
echo "Verifying configuration..."
sshd -t && echo "Configuration syntax OK"
}
restart_service() {
echo "Restarting SSH service..."
systemctl restart sshd
systemctl status sshd --no-pager
}
main() {
echo "SSH Security Hardening Script"
echo "============================"
verify_sudo
verify_admin_user
backup_config
apply_config
set_permissions
verify_config
echo ""
echo "IMPORTANT: Keep your current SSH session open!"
echo "Test a new connection before closing this session!"
echo ""
read -p "Restart SSH service now? (yes/no): " confirm
if [ "$confirm" = "yes" ]; then
restart_service
echo "SSH service restarted. Please test a new connection."
else
echo "Service restart skipped. Run 'systemctl restart sshd' manually when ready."
fi
}
main "$@"8. Conclusion
SSH hardening must balance security and availability. Over‑restrictive settings can lock administrators out, while insufficient hardening leaves the service exposed. Core recommendations include changing the default port, disabling password authentication in favor of public‑key auth, disabling direct root login, deploying Fail2Ban, and rotating keys regularly. Always back up the configuration, keep an active session for recovery, apply changes incrementally, and have console or rescue‑mode access as a final fallback.
References
man sshd_config man ssh_config man ssh-keygenOpenSSH official security page: https://www.openssh.com/security.html
NSA SSH Hardening Guide
CIS Benchmarks for SSH
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
