This post is a deep-dive companion to Linux Server Security Baseline. That post covers seven hardening steps including a simplified Fail2ban setup. This one covers everything that post intentionally left out.
A basic fail2ban set up on AWS EC2 - Debian 13, configure, and we will have a live test — using a wrong key to trigger a ban on a real EC2 instance.
How Fail2ban Works
Fail2ban watches a log source — a file or the systemd journal — for lines matching a filter regex. When the same IP matches the filter maxretry times within findtime, it triggers a ban action — typically a UFW or nftables rule that drops traffic from that IP for bantime.
SSH attempt (wrong key)
│
▼
systemd journal logs "Failed publickey"
│
▼
Filter match? ───No──▶ ❌ Missed (silent failure)
│
Yes
│
▼
Count failure for IP
│
▼
Failures ≥ maxretry? ───No──▶ Wait for next attempt
│
Yes
│
▼
🚫 Ban IP (UFW DENY rule)
On Debian 13, the default config hits the “No” path for three different reasons. Each one silently drops the event — Fail2ban runs, the jail is active, and the count stays at zero.
Prerequisites
- Debian 13 server hardened per the Linux Server Security Baseline
- Custom SSH port configured (this guide uses
2233) systemdis running — default on Debian 13
Step 1: Install Fail2ban
sudo apt install -y fail2ban
Do not start it yet. Configure it first.
Step 2: Create a Minimal jail.local
The common advice is to copy jail.conf to jail.local and edit it.
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 <your-ip>
[sshd]
enabled = true
port = 2233
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service
filter = sshd[mode=aggressive]
bantime = 1h
findtime = 10m
maxretry = 5
| Setting | What It Does |
|---|---|
ignoreip | IPs that will never be banned — add your own IP or CIDR |
port | Must match your SSH port |
backend | systemd — reads from the journal, not a file |
journalmatch | Which journal entries to watch — see Step 3 |
filter | sshd[mode=aggressive] — counts publickey failures, not just passwords |
bantime | How long a banned IP stays blocked |
findtime | The window for counting failures |
maxretry | Failed attempts within findtime that trigger a ban |
Add your IP or network to ignoreip so you can never accidentally ban yourself. Supports multiple entries space-separated: ignoreip = 127.0.0.1/8 ::1 203.0.113.10 10.0.0.0/8
Step 3: Enable LogLevel VERBOSE in sshd
This is the second non-obvious issue.
At the default INFO log level, a wrong-key attempt logs as:
Connection closed by authenticating user admin x.x.x.32 port 60869 [preauth]
Fail2ban’s sshd filter does not match Connection closed. The failure goes undetected.
With LogLevel VERBOSE, the same event produces:
Failed publickey for admin from x.x.x.32 port 27370 ssh2: ED25519 SHA256:pjC7...
Connection closed by authenticating user admin x.x.x.32 port 27370 [preauth]
The Failed publickey line is matched by the filter.
Add this to /etc/ssh/sshd_config:
LogLevel VERBOSE
Restart sshd:
sudo systemctl restart sshd
Keep your existing SSH session open while restarting sshd. Open a second terminal and verify you can still connect before closing anything.
Step 6: Start and Enable Fail2ban
sudo systemctl enable --now fail2ban
Verify the SSH jail is active:
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=ssh.service
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Two things to confirm:
Journal matchesshows_SYSTEMD_UNIT=ssh.service- Jail is active with 0 errors
Step 7: Test — Wrong Key Attempt
Use a different key pair (or generate a throwaway one) and try to connect repeatedly:
ssh -i wrong-key.pem admin@<ec2-public-ip> -p 2233
The first attempts return the expected auth failure:
admin@<ec2-public-ip>: Permission denied (publickey).
Check the journal — each attempt now produces a Failed publickey line from sshd-session:
sudo journalctl -u ssh -n 10
Mar 16 11:07:43 hostname sshd-session[10804]: Connection from 203.0.113.50 port 59262 on 10.0.1.100 port 2233 rdomain ""
Mar 16 11:07:44 hostname sshd-session[10804]: Failed publickey for admin from 203.0.113.50 port 59262 ssh2: ED25519 SHA256:pjC7...
Mar 16 11:07:44 hostname sshd-session[10804]: Connection closed by authenticating user admin 203.0.113.50 port 59262 [preauth]
Mar 16 11:07:44 hostname sshd[5634]: srclimit_penalise: ipv4: new 203.0.113.50/32 deferred penalty of 5 seconds
Mar 16 11:07:46 hostname sshd-session[10806]: Failed publickey for admin from 203.0.113.50 port 7397 ssh2: ED25519 SHA256:pjC7...
Mar 16 11:07:48 hostname sshd-session[10808]: Failed publickey for admin from 203.0.113.50 port 26010 ssh2: ED25519 SHA256:pjC7...
Mar 16 11:07:51 hostname sshd-session[10810]: Failed publickey for admin from 203.0.113.50 port 16599 ssh2: ED25519 SHA256:pjC7...
Keep trying. On the 5th attempt, the ban triggers. The 6th attempt looks different — SSH does not even respond with Permission denied:
Connection closed by 198.51.100.10 port 2233
That is the ban in action — Fail2ban dropped the connection before SSH could respond.
Step 8: Verify the Ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 5
| `- Journal matches: _SYSTEMD_UNIT=ssh.service
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 203.0.113.50
Total failed: 5 — all five wrong-key attempts were counted. Currently banned: 1 — the ban triggered after the 5th failure. Currently failed: 0 — the counter resets after a ban.
Verify the UFW rule was added:
sudo ufw status
Anywhere DENY IN 203.0.113.50
To manually unban an IP (your own, if you locked yourself out):
sudo fail2ban-client set sshd unbanip <ip>
Tuning for Production
| Setting | Development | Production |
|---|---|---|
bantime | 1h | 24h or 1w |
findtime | 10m | 10m |
maxretry | 5 | 3 |
ignoreip | Your home IP | Your team’s IPs or VPN CIDR |
For production servers with real exposure, a 24-hour ban and 3 retries is more appropriate. Bots retry thousands of times — a short ban just means they come back in an hour.
Fail2ban has a built-in recidive jail that permanently bans IPs that keep returning after their ban expires. Add it to jail.local for persistent abusers:
[recidive]
enabled = true
bantime = 1w
findtime = 1d
maxretry = 5 Useful Commands
# Check which jails are active
sudo fail2ban-client status
# Status of the SSH jail
sudo fail2ban-client status sshd
# Unban an IP
sudo fail2ban-client set sshd unbanip <ip>
# Reload config after editing jail.local
sudo fail2ban-client reload
# View Fail2ban logs
sudo journalctl -u fail2ban -n 50
# Test the sshd filter against the journal manually
sudo fail2ban-regex systemd-journal "/etc/fail2ban/filter.d/sshd.conf[mode=aggressive]" --journalmatch="_SYSTEMD_UNIT=ssh.service"
Wrapping Up
I spent a good while staring at a Fail2ban jail that looked perfectly healthy — running, no errors, zero failures — while wrong-key attempts piled up in the journal.
If you followed along, your server now has a working Fail2ban setup that actually bans wrong-key attempts. Test it yourself — use a wrong key, watch the counter go up, and see the ban drop the connection.
The full config is eight lines in jail.local and one line in sshd_config. Copy it, verify it, and move on.
With SSH locked down and Fail2ban running, the next layer is UFW — controlling exactly which ports are reachable at the OS level. See Linux Server Security Baseline for the full setup.