Tools: SSH Hardening — The Ultimate Guide for 2026

Tools: SSH Hardening — The Ultimate Guide for 2026

SSH Hardening — The Ultimate Guide for 2026

Prerequisites

1. Switch to Key-Based SSH Authentication

Generate a Key Pair (on your local machine)

Copy the Public Key to Your Server

Test Key-Based Login

2. Harden the SSH Configuration

Disable Password Authentication

Disable Root Login

Change the Default Port

Disable Empty Passwords

Limit Authentication Attempts

Set a Login Grace Time

Disable X11 Forwarding

Disable TCP Forwarding (if not needed)

Restrict SSH to Specific Users

Use Strong Ciphers and Key Exchange Algorithms

3. Complete Hardened sshd_config

4. Set Up Fail2Ban for SSH

5. Add Two-Factor Authentication (Optional)

6. Monitor SSH Access

Check Active Sessions

Review Recent Logins

Watch Failed Attempts in Real Time

Set Up Login Notifications

Security Considerations

Troubleshooting

Conclusion If your server has a public IP, it's getting SSH brute-force attempts right now. Not maybe. Not eventually. Right now. Check your auth log: You'll see hundreds — sometimes thousands — of failed login attempts from IPs you've never seen. Botnets scan the entire IPv4 space and hammer port 22 with common username/password combinations 24/7. My basic VPS hardening guide covers the essentials. This SSH hardening guide goes deeper — every sshd_config setting that matters, key-based authentication, two-factor auth, and monitoring. By the end, your SSH setup will be hardened against everything from automated bots to targeted attacks. Warning: Always keep a second SSH session open when modifying SSH config. If you make a mistake, the existing session stays connected. Test your new config in a new connection before closing the old one. Password authentication is the weakest link. Even a strong password can be brute-forced given enough time. SSH keys are cryptographically stronger and immune to dictionary attacks. Ed25519 is the modern default — faster and more secure than RSA. If you need RSA compatibility (some older systems), use: You'll be prompted for a passphrase. Use one. The passphrase encrypts the private key on disk. If someone steals your key file, the passphrase is the last line of defense. Open a new terminal (keep the old one open) and test: If it logs in without asking for a password (or asks for your key passphrase instead), key auth is working. The main SSH server config lives at /etc/ssh/sshd_config. Every change below goes in this file. After editing, restart SSH to apply: Once key-based auth works, disable passwords entirely: This single change eliminates brute-force password attacks completely. Bots can hammer port 22 all day — without the private key, they're not getting in. Even with key auth, there's no reason to allow direct root login. Use a regular user and sudo for privilege escalation. This adds a layer — an attacker needs both the SSH key and knowledge of which user has sudo access. Changing from port 22 won't stop a determined attacker, but it eliminates 99% of automated bot traffic. Most botnets only scan port 22. This is security through obscurity — not a defense by itself, but it reduces noise in your logs dramatically. After changing the port, update your firewall: This should already be the default, but explicitly set it. After 3 failed attempts, the connection is dropped. Combined with Fail2Ban, this makes brute-force attacks impractical. The server waits 30 seconds for authentication before disconnecting. The default is 120 seconds — that's too generous. Reduce it to limit resource consumption from idle connections. Unless you're running graphical applications over SSH (unlikely on a server), disable this. It's an unnecessary attack surface. If you don't use SSH tunnels, disable forwarding. If you use SSH tunnels for things like database access, leave it enabled or restrict to specific users. This is a whitelist. Only the listed users can log in via SSH. Everyone else is rejected before authentication even starts. This disables weak algorithms (3DES, SHA1, diffie-hellman-group1). Modern clients support all of these. If you have very old clients that can't connect after this change, they need upgrading — not your server weakening its crypto. Here's the full set of changes in one block. Add or modify these lines in /etc/ssh/sshd_config: Validate the config before restarting: If there are no errors, restart: Test in a new terminal before closing your current session. Fail2Ban monitors your auth logs and temporarily bans IPs that fail authentication too many times. Even with key-based auth, it reduces log noise and blocks scanners. Create a local config (don't edit the main config — it gets overwritten on updates): This bans an IP for 1 hour after 3 failed attempts within 10 minutes. Start and enable Fail2Ban: You'll see the number of currently banned IPs and total bans since startup. For maximum security, add TOTP (Time-based One-Time Password) as a second factor. You'll need both your SSH key and a code from your authenticator app. Install the Google Authenticator PAM module: Run the setup as your regular user (not root): Scan the QR code with your authenticator app (Google Authenticator, Authy, or any TOTP app). Save the emergency codes in a secure location. Configure PAM. Edit /etc/pam.d/sshd: Update sshd_config to require both key and TOTP: Now login requires: SSH key → TOTP code. Two factors, both under your control. Warning: If you enable 2FA, make sure you have your emergency codes saved somewhere physically secure. Losing access to your authenticator app AND your emergency codes means you're locked out. Hardening is only half the job. You also need to know who's connecting and when. Add this to /etc/profile.d/ssh-notify.sh to get notified on every successful login: This sends you an email every time someone logs in via SSH. If you see a login you don't recognize, investigate immediately. Then connect with ssh byteguard. Problem: Locked out after disabling password auth.

Cause: Key-based auth wasn't properly configured before disabling passwords.Fix: Access your server through your hosting provider's console (Hetzner has a web console). Re-enable PasswordAuthentication yes, restart SSH, and fix your key setup. Problem: Connection refused after changing the port.Cause: Firewall doesn't allow the new port, or you forgot to update the SSH config.Fix: Connect via console, check ufw status, and ensure the new port is allowed. Verify sshd_config has the right port. Problem: "Too many authentication failures" error.Cause: SSH agent is offering multiple keys before the right one.Fix: Specify the key explicitly: ssh -i ~/.ssh/id_ed25519 -p 2222 user@server. Or set IdentitiesOnly yes in your SSH client config. Problem: 2FA prompt doesn't appear.Cause: PAM module not loaded or sshd_config not updated.Fix: Verify auth required pam_google_authenticator.so is in /etc/pam.d/sshd. Verify ChallengeResponseAuthentication yes and AuthenticationMethods publickey,keyboard-interactive in sshd_config. Restart SSH. Problem: Fail2Ban not banning IPs.Cause: Wrong log path or port in jail config.

Fix: Check sudo fail2ban-client status sshd for errors. Verify logpath matches your system (/var/log/auth.log on Ubuntu/Debian). Verify the port matches your custom SSH port. SSH is the front door to your server. Every hardening step here stacks — key-based auth eliminates password attacks, Fail2Ban blocks scanners, a custom port reduces noise, and 2FA adds a second factor even if your key is compromised. If you haven't done the basics yet, start with my VPS hardening guide — it covers UFW, unattended upgrades, and user setup alongside basic SSH config. For protecting other services, check out the Fail2Ban setup guide and Docker security best practices. Your server is only as secure as its weakest entry point. Make SSH a strong one. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

grep "Failed password" /var/log/auth.log | tail -20 grep "Failed password" /var/log/auth.log | tail -20 grep "Failed password" /var/log/auth.log | tail -20 ssh-keygen -t ed25519 -C "[email protected]" ssh-keygen -t ed25519 -C "[email protected]" ssh-keygen -t ed25519 -C "[email protected]" ssh-keygen -t rsa -b 4096 -C "[email protected]" ssh-keygen -t rsa -b 4096 -C "[email protected]" ssh-keygen -t rsa -b 4096 -C "[email protected]" ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-server-ip ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-server-ip ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-server-ip cat ~/.ssh/id_ed25519.pub | ssh user@your-server-ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" cat ~/.ssh/id_ed25519.pub | ssh user@your-server-ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" cat ~/.ssh/id_ed25519.pub | ssh user@your-server-ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" ssh user@your-server-ip ssh user@your-server-ip ssh user@your-server-ip sudo systemctl restart sshd sudo systemctl restart sshd sudo systemctl restart sshd PasswordAuthentication no PasswordAuthentication no PasswordAuthentication no PermitRootLogin no PermitRootLogin no PermitRootLogin no sudo ufw allow 2222/tcp comment "SSH custom port" sudo ufw delete allow 22/tcp sudo ufw allow 2222/tcp comment "SSH custom port" sudo ufw delete allow 22/tcp sudo ufw allow 2222/tcp comment "SSH custom port" sudo ufw delete allow 22/tcp ssh -p 2222 user@your-server-ip ssh -p 2222 user@your-server-ip ssh -p 2222 user@your-server-ip PermitEmptyPasswords no PermitEmptyPasswords no PermitEmptyPasswords no MaxAuthTries 3 MaxAuthTries 3 MaxAuthTries 3 LoginGraceTime 30 LoginGraceTime 30 LoginGraceTime 30 X11Forwarding no X11Forwarding no X11Forwarding no AllowTcpForwarding no AllowTcpForwarding no AllowTcpForwarding no AllowUsers yourusername AllowUsers yourusername AllowUsers yourusername KexAlgorithms curve25519-sha256,[email protected] Ciphers [email protected],[email protected],[email protected] MACs [email protected],[email protected] HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 KexAlgorithms curve25519-sha256,[email protected] Ciphers [email protected],[email protected],[email protected] MACs [email protected],[email protected] HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 KexAlgorithms curve25519-sha256,[email protected] Ciphers [email protected],[email protected],[email protected] MACs [email protected],[email protected] HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 # Network Port 2222 AddressFamily inet # IPv4 only (set to 'any' if you use IPv6) ListenAddress 0.0.0.0 # Authentication PermitRootLogin no PasswordAuthentication no PermitEmptyPasswords no PubkeyAuthentication yes AuthenticationMethods publickey MaxAuthTries 3 LoginGraceTime 30 # Access control AllowUsers yourusername # Security X11Forwarding no AllowTcpForwarding no AllowAgentForwarding no PermitTunnel no # Crypto KexAlgorithms curve25519-sha256,[email protected] Ciphers [email protected],[email protected],[email protected] MACs [email protected],[email protected] HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 # Logging LogLevel VERBOSE # Misc ClientAliveInterval 300 ClientAliveCountMax 2 MaxSessions 3 Banner none # Network Port 2222 AddressFamily inet # IPv4 only (set to 'any' if you use IPv6) ListenAddress 0.0.0.0 # Authentication PermitRootLogin no PasswordAuthentication no PermitEmptyPasswords no PubkeyAuthentication yes AuthenticationMethods publickey MaxAuthTries 3 LoginGraceTime 30 # Access control AllowUsers yourusername # Security X11Forwarding no AllowTcpForwarding no AllowAgentForwarding no PermitTunnel no # Crypto KexAlgorithms curve25519-sha256,[email protected] Ciphers [email protected],[email protected],[email protected] MACs [email protected],[email protected] HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 # Logging LogLevel VERBOSE # Misc ClientAliveInterval 300 ClientAliveCountMax 2 MaxSessions 3 Banner none # Network Port 2222 AddressFamily inet # IPv4 only (set to 'any' if you use IPv6) ListenAddress 0.0.0.0 # Authentication PermitRootLogin no PasswordAuthentication no PermitEmptyPasswords no PubkeyAuthentication yes AuthenticationMethods publickey MaxAuthTries 3 LoginGraceTime 30 # Access control AllowUsers yourusername # Security X11Forwarding no AllowTcpForwarding no AllowAgentForwarding no PermitTunnel no # Crypto KexAlgorithms curve25519-sha256,[email protected] Ciphers [email protected],[email protected],[email protected] MACs [email protected],[email protected] HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 # Logging LogLevel VERBOSE # Misc ClientAliveInterval 300 ClientAliveCountMax 2 MaxSessions 3 Banner none sudo sshd -t sudo sshd -t sudo sshd -t sudo systemctl restart sshd sudo systemctl restart sshd sudo systemctl restart sshd sudo apt update && sudo apt install fail2ban -y sudo apt update && sudo apt install fail2ban -y sudo apt update && sudo apt install fail2ban -y sudo tee /etc/fail2ban/jail.local << 'EOF' [sshd] enabled = true port = 2222 filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600 findtime = 600 banaction = ufw EOF sudo tee /etc/fail2ban/jail.local << 'EOF' [sshd] enabled = true port = 2222 filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600 findtime = 600 banaction = ufw EOF sudo tee /etc/fail2ban/jail.local << 'EOF' [sshd] enabled = true port = 2222 filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600 findtime = 600 banaction = ufw EOF sudo systemctl enable fail2ban sudo systemctl start fail2ban sudo systemctl enable fail2ban sudo systemctl start fail2ban sudo systemctl enable fail2ban sudo systemctl start fail2ban sudo fail2ban-client status sshd sudo fail2ban-client status sshd sudo fail2ban-client status sshd sudo apt install libpam-google-authenticator -y sudo apt install libpam-google-authenticator -y sudo apt install libpam-google-authenticator -y google-authenticator google-authenticator google-authenticator # Add at the end: auth required pam_google_authenticator.so # Add at the end: auth required pam_google_authenticator.so # Add at the end: auth required pam_google_authenticator.so AuthenticationMethods publickey,keyboard-interactive ChallengeResponseAuthentication yes AuthenticationMethods publickey,keyboard-interactive ChallengeResponseAuthentication yes AuthenticationMethods publickey,keyboard-interactive ChallengeResponseAuthentication yes sudo systemctl restart sshd sudo systemctl restart sshd sudo systemctl restart sshd sudo tail -f /var/log/auth.log | grep "Failed\|Accepted" sudo tail -f /var/log/auth.log | grep "Failed\|Accepted" sudo tail -f /var/log/auth.log | grep "Failed\|Accepted" #!/bin/bash if [ -n "$SSH_CONNECTION" ]; then IP=$(echo "$SSH_CONNECTION" | awk '{print $1}') echo "SSH login: $(whoami) from $IP at $(date)" | \ mail -s "SSH Login Alert: $(hostname)" [email protected] 2>/dev/null fi #!/bin/bash if [ -n "$SSH_CONNECTION" ]; then IP=$(echo "$SSH_CONNECTION" | awk '{print $1}') echo "SSH login: $(whoami) from $IP at $(date)" | \ mail -s "SSH Login Alert: $(hostname)" [email protected] 2>/dev/null fi #!/bin/bash if [ -n "$SSH_CONNECTION" ]; then IP=$(echo "$SSH_CONNECTION" | awk '{print $1}') echo "SSH login: $(whoami) from $IP at $(date)" | \ mail -s "SSH Login Alert: $(hostname)" [email protected] 2>/dev/null fi sudo chmod +x /etc/profile.d/ssh-notify.sh sudo chmod +x /etc/profile.d/ssh-notify.sh sudo chmod +x /etc/profile.d/ssh-notify.sh Host byteguard HostName your-server-ip User yourusername Port 2222 IdentityFile ~/.ssh/id_ed25519 Host byteguard HostName your-server-ip User yourusername Port 2222 IdentityFile ~/.ssh/id_ed25519 Host byteguard HostName your-server-ip User yourusername Port 2222 IdentityFile ~/.ssh/id_ed25519 - A Linux VPS (Ubuntu 22.04/24.04 or Debian 12) with root or sudo access - SSH access already working (don't lock yourself out before setting up alternatives) - A second terminal/session open as a safety net while making changes - Time-based tokens: yes - Update .google_authenticator file: yes - Disallow multiple uses: yes - Rate limiting: yes - Key rotation. Rotate your SSH keys annually. Generate a new pair, add the new public key, verify it works, then remove the old one from authorized_keys. - Agent forwarding risks. If you enable SSH agent forwarding (-A), anyone with root on the intermediate server can use your agent to authenticate to other servers. Use ProxyJump instead of agent forwarding where possible. - Authorized keys audit. Periodically check ~/.ssh/authorized_keys on all users. Remove any keys you don't recognize. - SSH config on the client side. Create ~/.ssh/config entries for your servers to avoid typing ports and usernames: