← back to the work
exhibit 01-b · breach

A small SSH brute-force alerter in Python

2026 · security tooling · python, stdlib only

Scope

The server already runs fail2ban: three failed SSH attempts and the source IP is banned for 24 hours. fail2ban does not report, so attempts are handled without being recorded anywhere visible. This script covers that gap. It reports the source IP, the number of attempts, and the usernames tried.

It does not ban anything. It runs alongside fail2ban and reports to the same Discord webhook as the drive health and monitoring alerts.

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

sshd logs invalid user in a different line format to a real user with a wrong password. Automated scanners overwhelmingly guess non-existent usernames (admin, oracle, test). Capturing both formats and keeping the username set per IP separates untargeted scanning from attempts against 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

More write-ups: VLAN discovery and responsible disclosure · Hardened home lab server