Simple Windows Firewall Bouncer (SWFB)
If you've ever spun up a Windows server with RDP exposed, you already know what the logs look like. Within hours, sometimes minutes, Event ID 4625 starts flooding the Security log. Failed logon. Failed logon. Failed logon. Different usernames, same IP. Same username, different IPs. The internet is constantly knocking.
On Linux, tools like fail2ban and crowdsec have solved this problem for years. On Windows? You're mostly on your own. There are third-party agents, EDR add-ons, and cloud-based solutions, but if you want something lightweight, auditable, and dependency-free that you can just drop on a box and run, the options are slim.
So I built one: Simple Windows Firewall Bouncer (SWFB).
What It Does
SWFB is a PowerShell script that runs in a loop, watches Windows Security Event Logs for failed logon attempts (Event ID 4625), and fires off netsh advfirewall rules to block offending IPs, automatically, with no external software required.
It handles three distinct attack patterns:
- Brute force — one IP hammering the same account repeatedly
- Password spray — one IP trying many different usernames
- User spray / distributed attacks, many IPs all targeting the same username
Each of these has its own configurable threshold, and blocks are automatically expired and cleaned up after a configurable retention period. No stale rules piling up indefinitely.
The Detection Logic
At the core of each scan cycle, SWFB queries the Security event log for Event 4625 events within the lookback window, then builds three data structures:
$failures- a count of failed attempts per source IP$ipUsers- a set of unique usernames tried per source IP$userIps- a set of unique source IPs per targeted username
Blocking decisions flow naturally from those:
# Block IPs for repeated failed logins foreach ($ip in $failures.Keys) { if ($failures[$ip].Count -ge $FailedAttemptThreshold) { Block-IP $ip } } # Block IPs doing password spray (multiple usernames from one IP) foreach ($ip in $ipUsers.Keys) { if ($ipUsers[$ip].Count -ge $MaxUsernamesPerIp) { Block-IP $ip } } # Block IPs for user spray (single user, multiple source IPs) foreach ($user in $userIps.Keys) { if ($userIps[$user].Count -ge $MaxIpsPerUsername) { foreach ($ip in $userIps[$user]) { Block-IP $ip } } }
That last case is worth calling out, if a single username is getting hammered from three or more different source IPs simultaneously, that's a coordinated spray, and every contributing IP gets blocked. It's a simple heuristic that catches a lot of real-world distributed attack patterns.
Destination Port Correlation
One detail I'm particularly happy with: SWFB correlates each failed logon event with the Windows Firewall log (pfirewall.log) to pull the destination port associated with that connection attempt. This gives each blocked IP entry a port tag, so when you're reviewing blocked_ips.json later, you can immediately see whether the attacker was going after RDP (3389), SMB (445), WinRM (5985), or something else entirely.
It uses a ±10-second time window around the event timestamp to find the matching firewall log entry, which is simple but effective for correlating events that happen within the same second or two.
External Blocklists
Beyond reactive blocking, SWFB also supports proactive blocklisting. On startup, it downloads one or more plaintext IP lists from configurable URLs and consolidates them into a single Windows Firewall rule:
netsh advfirewall firewall add rule name="Block_ExternalBlocklist" dir=in action=block remoteip=$ipString netsh advfirewall firewall add rule name="Block_ExternalBlocklist" dir=out action=block remoteip=$ipString
I've wired this up to a few feeds I maintain: known toolshell IPs, C2 infrastructure, and Vultr ASN ranges that I see generating a lot of noise. You can point it at any public blocklist feed that serves a flat list of IPs. DShield's top-10 feed is included as a default.
State Persistence
Blocked IPs are written to a local blocked_ips.json file, which includes the source IP, the destination port (if captured), and the time of the block. The file also stores the host's public and private IP at the time of writing, which is handy for inventory tracking across multiple machines.
On startup, SWFB loads this file and restores the in-memory block list, meaning a service restart or system reboot won't silently drop your active blocks.
What It Isn't
To be clear about the scope: SWFB is a host-based, reactive tool. It only sees what Windows Security Event Log sees (Event 4625 - failed logon). It won't catch exploitation attempts that don't generate logon failures, it doesn't do deep packet inspection, and it's not a replacement for a SIEM or a proper network-layer IDS/IPS.
It also won't help if an attacker is moving slowly enough to stay under thresholds indefinitely. Slow, patient brute-force attacks are a problem for detection tools with longer memory than a rolling 5-minute window.
That said, for the majority of the noise that hits an exposed Windows system, automated scanners, credential-stuffing bots, RDP brute-forcers, SWFB handles it cleanly.
Getting Started
Requirements are minimal: Windows 10/11 or Server, PowerShell 5.x+, and Administrator privileges. That's it.
.\swfb.ps1
All the thresholds are parameterized at the top of the script and can be overridden at runtime. The most important one to set before you run it on a machine you access remotely: add your admin IP to the $Whitelist. Otherwise, a few fat-fingered RDP attempts from your own machine could lock you out.
The full script and README are on GitHub at github.com/zach115th/SWFB. Pull requests and feedback are welcome — especially if you've found good public blocklist sources worth adding to the defaults.
Comments
Post a Comment