Hardened SSH with Plink, Fail2ban, and Persistent ipset Bans
7 min read
Hardened SSH + Fail2ban + Persistent Ban List
This guide configures a Linux server for:
- SSH key authentication using an existing PuTTY
.ppkkey 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:
plinkputtygenpscp
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
5. Test Login Using Plink
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:

Takeaways
- Bullet key points.