Stop the evil doers in their tracks!


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 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

See below for install instructions:

Version 0.3 Released!

Update March 2020:

To update pf-badhost to the latest version:
click here for upgrade instructions

pf-badhost has been downloaded thousands of times since I first released it 18 months ago. The BSD Now guys even did a spot on pf-badhost. Considering the attention pf-badhost has received, I figured I would continue to improve it.

This version makes a number of important improvements. The big name feature for this release is subnet aggregation. Subnet aggregation is used to take the address list and "aggregate" the addresses into the smallest possible representation using CIDR blocks. For example, two /24 CIDR blocks could be merged into a /23, seqential addresses could be merged into a /31 or /30 etc, or a /24 could be eliminated entirely if it overlaps with a /20 somewhere else in the list, thus removing duplicate entries. The reason this is desirable is that it provides maximum lookup efficiency while minimizing memory usage. Depending on which block lists you have enabled, you could have anywhere from a 10%-35% overhead reduction by enabling this feature. This feature is especially powerful if using GeoIP/Country blacklisting

I wanted to avoid pf-badhost having any dependencies outside the OpenBSD base system, so I implemented a subnet aggregation function in Perl, but I highly recommend installing the "aggregate" utility (availble in the OpenBSD package repository). The aggregate utility has a very mature, stable codebase thats been used and abused for decades. If for some reason you cannot install any outside dependencies, you can use the built in aggregate function that's written in pure Perl. It is however at least 10x slower than the aggregate utility which is written in C.

The IPv6 handling has been completely redone. I discovered a nasty issue with regards to IPv6 handling that has since been rectified. As it turns out, IPv6 regex matching is significantly more difficult than IPv4 regex matching.

With IPv4, the regex logic is simple, and we can easily validate and pull an IPv4 address from arbitrarily formatted text with ease. Doing the same thing with IPv6 has proven difficult, if not impossible to do properly and correctly.

Due to IPv6 being alpha numeric as well as having many different valid ways to represent an address (click here for an example of just a few of the many ways to fomat an IPv6 address) there was no way to support every valid IPv6 address representation while still accepting arbitrary input formats. The only feasible solution seemed to be requiring preformatting of the IPv6 list data.

Without preformatted data (1 address per line) we could make accidental matches by pulling valid addresses out of otherwise invalid addresses. Take for example the invalid IPv6 address "123:54H:19ff::1", due to double colon notation, there are at least 3 valid addresses that could be extracted from within that otherwise invalid address. You could remove the first half of the address and get a valid address (19ff::1) or the final quarter of the address (::1) or even just "::" is a valid address in IPv6 land.

Addtionally, there's been improvements made to error checking and abort handling, as well as an effort to maximize robustness. I removed all reliance on shell globbing and other potentially unsafe path handling which had the added benefit of fixing a bug where the script would fail when using many lists (hundreds) due to exceeding the shells maximum argument length.


• Add support for subnet aggregation

• Improved error checking

• Improved robustness

• Rewritten IPv6 handling

To update pf-badhost to the latest version, click here for upgrade instructions

Upgrade Quick Start:

If you already have pf-badhost version 0.2 installed all you need to do is:
Download the updated script, and replace the old one in /usr/local/bin/

Old version 0.2 release page: Link

Install Guide

• 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 into /usr/local/bin/

	# ftp
	# mv /usr/local/bin/
	# chown root:bin /usr/local/bin/
	# chmod 644 /usr/local/bin/
• 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

• Edit _pfbadhost crontab to run every night at midnight

      # crontab -u _pfbadhost -e
      @midnight 		/bin/sh /usr/local/bin/


• Create /etc/pf-badhost.txt and set appropriate permissions

      # install -m 600 -o _pfbadhost /dev/null /etc/pf-badhost.txt
• Add the following lines to your pf.conf :

      table <pfbadhost> persist file “/etc/pf-badhost.txt”
      block in quick on egress from <pfbadhost>
      block out quick on egress to <pfbadhost>
• Run the script as user "_pfbadhost" to create the required files

      $ doas -u _pfbadhost sh /usr/local/bin/

• It should say about ~50,000 addresses were added to the <pfbadhost> table

• Now reload your pf rule set:

      # pfctl -f /etc/pf.conf

• For good measure, we'll run the script once more

      $ doas -u _pfbadhost sh /usr/local/bin/

Yay! pf-badhost is now installed!

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/

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
	pass out quick on egress to

   	table <pfbadhost> persist file “/etc/pf-badhost.txt”
     	block in quick on egress from <pfbadhost>
     	block out quick on egress to <pfbadhost>

Conversely, adding a custom rule to that negates your subnet range from the <pfbadhost> table will also work.

	# vi /usr/local/bin/

	# User Defined Rules (add or negate addresses from list)
	printf "\n# User Defined Rules:\n\n" >> $outdir/$finout
	echo "!" >> $outdir/$finout