pf-badhost is a simple, easy to use badhost blocker that uses the power of the pf firewall to block many of the internet's biggest irritants. Annoyances such as SSH and SMTP bruteforcers are largely eliminated. Shodan scans and bots looking for webservers to abuse are stopped dead in their tracks. When used to filter outbound traffic, pf-badhost blocks many seedy, spooky malware containing and/or compromised webhosts.
Filtering performance is exceptional, as the badhost list is stored in a pf table. To quote the OpenBSD FAQ page regarding tables: "the lookup time on a table holding 50,000 addresses is only slightly more than for one holding 50 addresses."
pf-badhost is simple and powerful. The blocklists are pulled from quality, trusted sources. The 'Firehol', 'Emerging Threats' and 'Binary Defense' block lists are used as they are popular, regularly updated lists of the internet's most egregious offenders. The pf-badhost.sh script can easily be expanded to use additional or alternate blocklists as well as setting custom rules.
pf-badhost works best when used in conjunction with unbound-adblock for the ultimate badhost blocking.
Download: link
Update November 2019:
To update pf-badhost to the latest version:
click here for upgrade instructions
It's been just over a year since version 0.1 of pf-badhost was released. pf-badhost has been downloaded thousands of times. The BSD Now guys even did a spot on pf-badhost. Considering the attention pf-badhost has received, I figured it deserved some love.
Version 0.2 is a complete rewrite of the script. I've written a basic address parser to allow for proper address verification and error checking. As an added benefit of the new address parsing function, IPv6 lists are now supported as well as mixed lists containting any combination of IPv4, IPv6 and/or CIDR notation.
The parser also supports arbitrary input formatting for address lists. This means it can be fed addresses in HTML, XML, JSON, CSV etc and be able to parse it into a format suitable for feeding into pf.
A number of users have contacted me asking about support for blocking entire countries. Ask and thou shall receive.
Changelog:
• Add support for GeoIP blocking
• Add support for IPv6 blocklists (including mixed address lists)
• Proper address parsing and error checking
• Add support for HTML, XML, JSON, CSV formatted lists
• Add experimental authlog analyzer to mitigate distributed bruteforcing
• Remove dependency on Perl. Now uses only standard grep and awk
• Security improvements (safe file handling to eliminate race conditions)
I've also cleaned up the how-to guide and clarified a few things such as the specifics for creating the _pfbadhost user.
To update pf-badhost to the latest version, click here for upgrade instructions
Upgrade Quick Start:
Download the updated script, and replace the old one in /usr/local/bin/
NOTE: The file permissions and doas.conf settings have changed.
doas.conf must now contain these two lines:
# cat /etc/doas.conf
permit nopass _pfbadhost cmd pfctl args -nf /etc/pf.conf
permit nopass _pfbadhost cmd pfctl args -t pfbadhost -T replace -f /etc/pf-badhost.txt
/etc/pf-badhost.txt must also be owned by user "_pfbadhost" and have chmod 600 permissions
• Create a new user (we’ll call ours “_pfbadhost”)
# useradd -s /sbin/nologin _pfbadhost
The user should be created with default shell of "nologin" and an empty password (disables password logins).
• Download and put pf-badhost.sh into /usr/local/bin/
# ftp https://www.geoghegan.ca/scripts/pf-badhost.sh
# mv pf-badhost.sh /usr/local/bin/
# chown root:bin /usr/local/bin/pf-badhost.sh
# chmod 644 /usr/local/bin/pf-badhost.sh
• Give user ‘_pfbadhost’ strict doas permission for the exact commands the script needs run as superuser:
# cat /etc/doas.conf
permit nopass _pfbadhost cmd pfctl args -nf /etc/pf.conf
permit nopass _pfbadhost cmd pfctl args -t pfbadhost -T replace -f /etc/pf-badhost.txt
# crontab -u _pfbadhost -e
@midnight /bin/sh /usr/local/bin/pf-badhost.sh
# touch /etc/pf-badhost.txt
# chown _pfbadhost /etc/pf-badhost.txt
# chmod 600 /etc/pf-badhost.txt
table <pfbadhost> persist file “/etc/pf-badhost.txt”
block in quick on egress from <pfbadhost>
block out quick on egress to <pfbadhost>
• Run the pfbadhost.sh script as user "_pfbadhost" to create the required files
$ doas -u _pfbadhost sh /usr/local/bin/pf-badhost.sh
# pfctl -f /etc/pf.conf
$ doas -u _pfbadhost sh /usr/local/bin/pf-badhost.sh
With the nightly cron job, the list will be be regularly updated with the latest known bad hosts.
To enable geo-blocking, you can uncomment the lines under the country blacklisting section of the script. There are instructions and examples provided in comments in the script
To add custom rules or enable or add alternate blocklists, add them to the appropriate section of the script. It is well commented so it should be pretty straightforward.
The address parser is written using mostly POSIX regular expressions with grep and a bit of awk. If you are unsatisfied with the regex performance, you can substitute the use of the system grep with something like ripgrep or GNU grep and/or GNU awk if your platform supports them. I've found ripgrep to be considerably faster than the default grep on OpenBSD.
To convert the script to use ripgrep instead of grep, install ripgrep and run this command as root:
# sed -i 's/grep -E/rg/g' /usr/local/bin/pf-badhost.sh
Note: If you are trying to run pf-badhost on a LAN or are using NAT, you will want to add a pass quick rule to your pf.conf appearing BEFORE the pf-badhost rules allowing traffic to and from your local subnet so that you can still access your gateway and any DNS servers.
Something like this should do:
# vi /etc/pf.conf
pass in quick on egress from 192.168.17.0/24
pass out quick on egress to 192.168.17.0/24
table <pfbadhost> persist file “/etc/pf-badhost.txt”
block in quick on egress from <pfbadhost>
block out quick on egress to <pfbadhost>
# vi /usr/local/bin/pf-badhost.sh
# User Defined Rules (add or negate addresses from list)
printf "\n# User Defined Rules:\n\n" >> $outdir/$finout
echo "!192.168.17.0/24" >> $outdir/$finout