Hardened SSH + Fail2ban + Persistent Ban List

This guide configures a Linux server for:

  • SSH key authentication using an existing PuTTY .ppk key through Plink
  • SSH hardening
  • Fail2ban protection for SSH
  • Permanent bans using bantime = -1
  • Persistent firewall blocking using ipset
  • Automatic validation checks

Brutal truth: Fail2ban is not a replacement for proper SSH hardening. Key-only SSH is the real security control. Fail2ban is mostly noise control and reactive blocking.


0. Important Concepts

.ppk is client-side only

A .ppk file is used by PuTTY/Plink on the client side.

The Linux server does not use the .ppk directly. The server only needs the matching public key inside:

~/.ssh/authorized_keys

Do not confuse these settings

Wrong:

maxretry = -1

That disables banning behavior because it effectively allows infinite retries.

Correct for permanent bans:

bantime = -1

1. Install Server Packages

Run on the Linux server:

sudo apt update
sudo apt install -y fail2ban ipset iptables-persistent

2. Install PuTTY Tools on Linux Client

Run on the Linux client where your .ppk exists:

sudo apt update
sudo apt install -y putty-tools

This gives you:

  • plink
  • puttygen
  • pscp

3. Existing .ppk: Extract the Public Key Using CLI

On your Linux client:

puttygen yourkey.ppk -O public-openssh

Expected output format:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...

If it starts with this, you copied the wrong format:

---- BEGIN SSH2 PUBLIC KEY ----

4. Install the Public Key on the Server

Log in to the server using your current working method first.

Then run on the server as the target SSH user:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys

Paste the OpenSSH public key from puttygen.

Then fix permissions:

chmod 600 ~/.ssh/authorized_keys

From the Linux client:

plink -i yourkey.ppk user@server-ip

For automation/non-interactive checks:

plink -batch -i yourkey.ppk user@server-ip "hostname"

For debugging:

plink -v -i yourkey.ppk user@server-ip

On the server, watch SSH logs:

sudo tail -f /var/log/auth.log

Do not disable password authentication until key login works.


6. Harden SSH

Edit SSH server config:

sudo nano /etc/ssh/sshd_config

Recommended settings:

PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
MaxAuthTries 3
LoginGraceTime 30
MaxStartups 3:30:10

Restart SSH:

sudo systemctl restart ssh

Validate password login is disabled:

plink -ssh -pw wrongpassword user@server-ip

Expected result:

Access denied

If password login still works, your SSH config is wrong or not being applied.


7. Configure Fail2ban with Permanent Bans

Edit:

sudo nano /etc/fail2ban/jail.local

Use this configuration:

[DEFAULT]
# Permanent bans
bantime = -1

# Detection window
findtime = 10m

# Failure threshold
maxretry = 3

# Backend
backend = systemd

# Prevent self-lockout. Replace YOUR_IP_HERE.
ignoreip = 127.0.0.1/8 ::1 YOUR_IP_HERE

# ipset integration
banaction = iptables-ipset-proto4

[sshd]
enabled = true
port = ssh
mode = aggressive
maxretry = 3
findtime = 10m

Start Fail2ban:

sudo systemctl enable fail2ban
sudo systemctl restart fail2ban

Verify:

sudo fail2ban-client status
sudo fail2ban-client status sshd

8. Create Persistent ipset Blacklist

Create the set:

sudo ipset create f2b-blacklist hash:ip timeout 0

Add firewall rule:

sudo iptables -I INPUT -m set --match-set f2b-blacklist src -j DROP

9. Save iptables Rules Correctly

Wrong:

sudo iptables-save > /etc/iptables/rules.v4

Correct:

sudo iptables-save | sudo tee /etc/iptables/rules.v4 > /dev/null

Reason: sudo does not apply to shell redirection. The > is handled by your non-root shell.


10. Save ipset Rules Correctly

Wrong:

sudo ipset save > /etc/ipset.conf

Correct:

sudo ipset save | sudo tee /etc/ipset.conf > /dev/null

11. Restore ipset on Boot

Create a systemd service:

sudo nano /etc/systemd/system/ipset-restore.service

Paste:

[Unit]
Description=Restore ipset
Before=network.target

[Service]
Type=oneshot
ExecStart=/sbin/ipset restore < /etc/ipset.conf

[Install]
WantedBy=multi-user.target

Enable it:

sudo systemctl daemon-reexec
sudo systemctl enable ipset-restore

12. Auto-Save ipset

Fail2ban does not automatically save new ipset entries to /etc/ipset.conf.

Add cron:

sudo crontab -e

Add:

*/5 * * * * /sbin/ipset save | /usr/bin/tee /etc/ipset.conf > /dev/null

13. Validate Everything

Check Fail2ban:

sudo fail2ban-client status sshd

Check ipset:

sudo ipset list f2b-blacklist

Check iptables rule:

sudo iptables -L -n | grep f2b-blacklist

Manual ban test:

sudo fail2ban-client set sshd banip 1.2.3.4
sudo ipset list f2b-blacklist

Unban test IP:

sudo fail2ban-client set sshd unbanip 1.2.3.4
sudo ipset del f2b-blacklist 1.2.3.4 2>/dev/null || true

14. Reboot Validation

Reboot:

sudo reboot

After reboot:

sudo ipset list
sudo iptables -L -n | grep f2b-blacklist
sudo fail2ban-client status sshd

If the ipset list or iptables rule is gone, persistence is broken.


15. Maintenance Commands

View banned IPs:

sudo fail2ban-client status sshd
sudo ipset list f2b-blacklist

Unban an IP:

sudo fail2ban-client set sshd unbanip IP_ADDRESS
sudo ipset del f2b-blacklist IP_ADDRESS 2>/dev/null || true

Count IPv4 entries in ipset:

sudo ipset list f2b-blacklist | grep -cE '^[[:space:]]*[0-9]+\.'

Check SSH logs:

sudo journalctl -u ssh -f
# or
sudo tail -f /var/log/auth.log

16. Security Reality Check

This setup is strong because it gives you:

  • Key-only SSH
  • Root login disabled
  • Fail2ban active
  • Permanent bans
  • Persistent firewall enforcement

But it is still not perfect:

  • SSH is still exposed to the internet
  • Permanent ban lists grow forever
  • False positives can accumulate

The cleaner design is to stop exposing SSH publicly and put it behind Tailscale or WireGuard.

Intro

  • One- to two-sentence summary.

Body

  • Paragraph text here.
  • Link example: Descriptive text
  • Code block:
    echo "hello world"
    
  • Table example: | Column | Detail | | — | — | | Item | Description |

Media

  • Image: Alt text

Takeaways

  • Bullet key points.