A small SSH brute-force alerter in Python
Why build this when fail2ban exists
My server already runs fail2ban, and it does its job: three failed SSH attempts and the source IP is banned for 24 hours. But fail2ban is silent. It quietly handles the attack and I never find out it happened. On a box sitting on a campus network, I wanted the opposite: tell me when someone is hammering the door, who they are, and what usernames they're guessing.
So this script doesn't ban anything. It's purely a visibility tool that runs alongside fail2ban, and it reports to the same place my other alerts (drive health, monitoring) already go: a Discord channel via webhook.
How it works
The whole thing is standard library: read auth.log, match failed login lines, aggregate by source IP, and post an alert if any IP crosses a threshold. No dependencies means nothing to install or break when it runs from cron.
The matching and aggregation:
FAILED = re.compile(
r'Failed (?:password|none) for (?:invalid user )?(?P<user>\S+) '
r'from (?P<ip>[0-9a-fA-F.:]+)'
)
def parse(path):
attempts = Counter()
users = defaultdict(set)
with open(path, errors='replace') as log:
for line in log:
m = FAILED.search(line)
if m:
attempts[m['ip']] += 1
users[m['ip']].add(m['user'])
return attempts, users
The interesting detail is invalid user: sshd logs a different line format for usernames that don't exist on the system versus real users with wrong passwords. Bots overwhelmingly guess non-existent users (admin, oracle, test), so capturing both formats and keeping the username set per IP shows at a glance whether something is a dumb scanner or actually targeting a real account.
Alerting is a plain webhook POST, also stdlib:
def alert(ip, count, users):
body = json.dumps({'content':
f'{count} failed SSH attempts from {ip} '
f'(users tried: {", ".join(sorted(users))})'}).encode()
req = urllib.request.Request(
WEBHOOK_URL, data=body,
headers={'Content-Type': 'application/json'})
urllib.request.urlopen(req, timeout=10)
Sample output
Console output is a per-IP breakdown sorted by volume (IPs below are sanitised to documentation ranges):
$ python3 sshwatch.py /var/log/auth.log
203.0.113.47 312 attempts users: admin, oracle, root, test
198.51.100.9 41 attempts users: root
192.0.2.200 3 attempts users: anders
2 IPs over threshold (10): alerts sent
Limitations and next steps
- It reads
auth.logdirectly, which works on Ubuntu but ties it to rsyslog. Reading from journald would make it portable to systems that don't keep the flat file. - Run repeatedly over the same log it will re-alert on the same attacker. The next step is a small state file recording per-IP counts at last run, so alerts only fire on new activity.
- Counts reset when the log rotates, so a slow attacker who stays under the threshold per rotation window goes unnoticed. Tracking across rotated logs would close that.
More write-ups: VLAN discovery and responsible disclosure · Hardened home lab server