A default OpenSSH installation accepts password authentication, root logins, and listens on port 22. the first three things attackers target. This guide hardens SSH systematically: key authentication, cipher hardening, access controls, and automated blocking of brute-force attempts.
Step 1 - Generate Ed25519 Keys (Client Side)
Generate Ed25519 key pair (smaller and faster than RSA-4096)
ssh-keygen -t ed25519 -C "your@email.com" -f ~/.ssh/id_ed25519
Use a strong passphrase. it protects the private key at rest
If you need RSA (legacy compatibility):
ssh-keygen -t rsa -b 4096 -C "your@email.com" -f ~/.ssh/id_rsa
View your public key for upload to servers
cat ~/.ssh/id_ed25519.pub
Step 2 - Deploy the Public Key to the Server
Copy public key to server (preferred method)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
Or manually:
cat ~/.ssh/id_ed25519.pub | ssh user@server \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
cat >> ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys"
Verify you can log in with key before disabling passwords
ssh -i ~/.ssh/id_ed25519 user@server
Step 3 - Harden sshd_config
Edit /etc/ssh/sshd_config:
sudo nano /etc/ssh/sshd_config
Apply these settings:
Port (change from default 22 to reduce automated scan noise)
Choose a port above 1024 that doesn't conflict with other services
Port 2222
Protocol version (OpenSSH 7+ only supports v2 by default, but be explicit)
Protocol 2
Key algorithms. prefer Ed25519 and ECDSA, disable old ones
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_ecdsa_key
Remove or comment out RSA and DSA host keys if not needed
Authentication
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
Public key authentication
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
Key exchange algorithms. modern, strong algorithms only
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
Access control
AllowUsers youruser adminuser
Or restrict to specific IPs:
Match User deploy
AllowedUsers 10.0.0.0/24
Session settings
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 20
Disable features you don't need
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
PrintLastLog yes
Logging
LogLevel VERBOSE
SyslogFacility AUTH
Validate and reload:
Syntax check BEFORE reloading (critical. don't lock yourself out)
sudo sshd -t
Reload
sudo systemctl reload sshd
Keep your current session open and test in a NEW terminal window
ssh -p 2222 youruser@server
Step 4 - Regenerate Host Keys
The default host keys generated at installation may use weak parameters. Regenerate:
Remove old host keys
sudo rm /etc/ssh/ssh_host_*
Generate new strong keys
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
sudo ssh-keygen -t ecdsa -b 521 -f /etc/ssh/ssh_host_ecdsa_key -N ""
RSA 4096 for legacy client compatibility
sudo ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""
sudo systemctl restart sshd
Clients will see a “host key changed” warning. clear known_hosts and reconnect:
ssh-keygen -R server_ip
ssh-keygen -R server_hostname
Step 5 - Install and Configure fail2ban
fail2ban monitors auth.log and blocks IPs that exceed a threshold of failed logins:
sudo apt install fail2ban
Create local override (never edit the main .conf file)
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
backend = systemd
[sshd]
enabled = true
port = 2222
logpath = %(sshd_log)s
maxretry = 3
bantime = 24h
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
Check current bans:
sudo fail2ban-client status sshd
Shows currently banned IPs
Manually unban an IP (if you ban yourself)
sudo fail2ban-client set sshd unbanip YOUR_IP
Step 6 - UFW Rules for SSH
Allow SSH only from a specific IP (strongest option)
sudo ufw allow from YOUR_OFFICE_IP to any port 2222 proto tcp
Or rate-limit if you need access from any IP
sudo ufw limit 2222/tcp comment "SSH rate limited"
sudo ufw enable
sudo ufw status
Step 7 - Two-Factor Authentication (Optional)
Add TOTP 2FA on top of key authentication for additional protection:
sudo apt install libpam-google-authenticator
Run as the user who will log in
google-authenticator
Answer yes to all prompts
Save the emergency scratch codes
Edit /etc/pam.d/sshd
Add BEFORE the @include common-auth line:
auth required pam_google_authenticator.so
Edit /etc/ssh/sshd_config
AuthenticationMethods publickey,keyboard-interactive
ChallengeResponseAuthentication yes
sudo systemctl restart sshd
Now SSH requires both your private key AND the TOTP code.
Verify Your Hardened Configuration
Check what ciphers the server advertises
ssh -vvv user@server 2>&1 | grep -i "kex\|cipher\|hmac"
Audit with ssh-audit
pip3 install ssh-audit
ssh-audit server:2222
Check for common vulnerabilities
ssh-audit grades your configuration and lists specific issues
SSH Client Configuration
Harden your local SSH client too. Edit ~/.ssh/config:
Host *
# Use Ed25519 keys preferentially
IdentityFile ~/.ssh/id_ed25519
# Strong algorithms only
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Security
VerifyHostKeyDNS yes
AddKeysToAgent yes
StrictHostKeyChecking ask
# Connection reuse (faster connections after first)
ControlMaster auto
ControlPath ~/.ssh/controlmasters/%r@%h:%p
ControlPersist 10m
SSH Jump Hosts and Bastion Configuration
For teams that access production servers from different networks, a bastion (jump host) is cleaner than opening SSH to the world on every server. Only the bastion has port 2222 open; production servers accept SSH only from the bastion’s IP.
On production servers. allow SSH only from bastion
sudo ufw allow from BASTION_IP to any port 2222 proto tcp
sudo ufw deny 2222/tcp # deny all other sources
sudo ufw enable
Configure your client ~/.ssh/config to tunnel through the bastion transparently:
~/.ssh/config
Host bastion
HostName bastion.example.com
User admin
Port 2222
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 60
Host prod-*
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion
# ProxyCommand alternative for older OpenSSH:
# ProxyCommand ssh -W %h:%p bastion
Host prod-web-01
HostName 10.0.1.10
Host prod-db-01
HostName 10.0.2.10
With this config, ssh prod-web-01 connects to 10.0.1.10 through the bastion without any manual tunneling. The bastion itself should be hardened identically to production servers. it is a high-value target.
Restrict the bastion’s sshd_config further to prevent it from being used as a pivot:
Additional bastion-specific sshd_config settings
AllowTcpForwarding local # allow local port forwarding, not remote
GatewayPorts no # no remote port forwarding
AllowStreamLocalForwarding no
PermitTTY yes # bastion users need a shell
ForceCommand /usr/bin/bastion-shell # optional: restrict to a menu script
Detecting SSH Brute Force Attempts
fail2ban blocks IPs after repeated failures, but reviewing the raw attack pattern helps tune your configuration. Check auth.log for attack signatures:
Count failed SSH attempts by IP (last 24 hours)
grep "Failed password\|Invalid user" /var/log/auth.log \
| awk '{print $(NF-3)}' \
| sort | uniq -c | sort -rn | head -20
Show currently banned IPs with unban time
sudo fail2ban-client status sshd
Watch live attack attempts
sudo tail -f /var/log/auth.log | grep -E "Failed|Invalid|Accepted"
Count valid logins vs failed attempts
echo "Successful logins:"
grep "Accepted" /var/log/auth.log | wc -l
echo "Failed attempts:"
grep "Failed password" /var/log/auth.log | wc -l
For high-volume servers, consider sshguard as an alternative. it monitors multiple log formats (sshd, nginx, postfix) and uses exponential backoff:
sudo apt install sshguard
sshguard integrates directly with nftables on newer systems
Check blocked hosts:
sudo sshguard -l /var/log/auth.log
sudo nft list set inet sshguard attackers
Set LoginGraceTime 10 in sshd_config to close unauthenticated connections faster. this helps when scanners hold connections open to occupy your MaxStartups slots:
MaxStartups 10:30:60 # start throttling at 10, drop at 60 unauthenticated connections
LoginGraceTime 10 # close unauthenticated connection after 10 seconds
Related Articles