Last updated: March 22, 2026

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