################################################################### # pf-badhost 0.5 MacOS Installation Instructions # Copyright 2018-2021 Jordan Geoghegan ################################################################### ################################################################### # Modified from original and updated for MacOS 12.6 'Monterey' # and MacOS 13.5 'Ventura' by gsb ################################################################### ################################################################### # Table of Contents: ################################################################### * Fresh Install Instructions * Post Install Notes ################################################################### # Fresh Installation Guide ################################################################### 1. Create a new user (we'll call ours "_pfbadhost"): The previous instructions used dscl for this. The has been superceded by sysadminctl on MacOS 10.13 and later. a.) Deprecated: The user should be created with default shell of "nologin" and an empty password (disables password logins) NOTE: Make sure you set the "UniqueID" and "PrimaryGroupID" to an unused value! # dscl . -create /Users/_pfbadhost # dscl . -create /Users/_pfbadhost UserShell /sbin/nologin # dscl . -create /Users/_pfbadhost RealName "_pfbadhost" # dscl . -create /Users/_pfbadhost UniqueID 1051 # Pick an unused UID here # dscl . -create /Groups/_pfbadhost # dscl . -create /Groups/_pfbadhost name _pfbadhost # dscl . -create /Groups/_pfbadhost gid 1051 # Pick an unsued number # dscl . -create /Groups/_pfbadhost GroupMembership _pfbadhost add: # dscl . -create /Users/_pfbadhost PrimaryGroupID 1051 If you need to find an unused UID and GID, use 'dscl . -list /Users UniqueID' and 'dscl . -list /groups PrimaryGroupID'. Generally GIDs and UIDs above 1050 are free from use. You could use a script, like so, editing the data as appropriate: # nano setup_pf-badhost_user.zsh #!/bin/zsh dscl . -create /Users/_pfbadhost dscl . -create /Users/_pfbadhost UserShell /sbin/nologin dscl . -create /Users/_pfbadhost RealName "_pfbadhost" dscl . -create /Users/_pfbadhost UniqueID 1051 dscl . -create /Groups/_pfbadhost dscl . -create /Groups/_pfbadhost name _pfbadhost dscl . -create /Groups/_pfbadhost gid 1051 # Pick an unsued number dscl . -create /Groups/_pfbadhost GroupMembership _pfbadhost dscl . -create /Users/_pfbadhost PrimaryGroupID 1051   exit 0 Edit it then run it with "sudo zsh setup_pf-badhoat_user.zsh" as you might want to keep this script around as, ime, the _pfbadhost user was deleted during the update from MacOS 12.6.2 to 12.7. b.) Using sysadminctl for MacOS 10.13 or later: 'nologin' users seen in Unix or Linux are not used. In modern MacOS versions, these are termed 'roleAccount'. These accounts should have UIDs between 200 and 400. As far as the group ID, you can use staff (20) or localaccounts (61). The command to create the _pfbadhost user is: sudo sysadminctl -addUser _pfbadhost -fullName _pfbadhost -UID 301 -GID 61 -roleAccount I have also found that the _pfbadhost user created in the fashion was also deleted during the update from MacOS 12.7 to 12.7.1. I have searched for the reason for this and documentation of why certain users were retained and others deleted to no avail. Perhaps members of a different group are not? So, check if user _pfbadhost is present after an update. The scripts below perform this check and recreate the user if it is absent. 2) Download pf-badhost script: $ curl https://geoghegan.ca/pub/pf-badhost/0.5/pf-badhost.sh -o pf-badhost.sh 3) a. Install script with appropriate permissions: # install -m 755 -o root -g bin pf-badhost.sh /usr/local/bin/pf-badhost b. Edit script (if you want) to change 1st line shebang to #!/bin/zsh 4) Create required files: # install -m 640 -o _pfbadhost -g wheel /dev/null /etc/pf-badhost.txt # install -d -m 755 -o root -g wheel /var/log/pf-badhost # install -m 640 -o _pfbadhost -g wheel /dev/null /var/log/pf-badhost/pf-badhost.log # install -m 640 -o _pfbadhost -g wheel /dev/null /var/log/pf-badhost/pf-badhost.log.0.gz and: # install -m 640 -o _pfbadhost -g wheel /dev/null /var/log/pf-badhost/pf-update.log Or, use a script, like so: nano setup_pf-badhost_files.zsh #!/bin/zsh install -m 640 -o _pfbadhost -g wheel /dev/null /etc/pf-badhost.txt install -d -m 755 -o root -g wheel /var/log/pf-badhost install -m 640 -o _pfbadhost -g wheel /dev/null /var/log/pf-badhost/pf-badhost.log install -m 640 -o _pfbadhost -g wheel /dev/null /var/log/pf-badhost/pf-badhost.log.0.gz install -m 640 -o _pfbadhost -g wheel /dev/null /var/log/pf-badhost/pf-update.log exit 0 Edit, then run it by "sudo zsh setup-pf-badhost_files.zsh" 5.a) Install Homebrew: # /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" b)Install GNU sort and RipGrep: # brew install coreutils ripgrep c) OPTIONAL: Install mawk for improved performance: # brew install mawk 6) Give "_pfbadhost" user permission to use sudo without a password: Run "sudo visudo" and then add the following line near the end of the file: _pfbadhost ALL = (ALL) NOPASSWD: ALL 7) Run pfbadhost as user "_pfbadhost" using the "-O macos" argument: # sudo -u _pfbadhost pf-badhost -O macos 8) Check to see if pf-badhost run successfully a. Did the file /etc/pf-badhost.txt get updated? b. Check the log /var/log/pf-badhost/pf-badhost.log. 9) Create a process to update pf-badhost daily a. cron script (However, this is not the Apple(R) way and cron may well be deprecated in the future): # crontab -u _pfbadhost -e ... @daily pf-badhost -O macos ... Further, this will fail if the _pfbadhost user gets inexplicably deleted Or, b. create a LaunchAgent which runs a wrapper script which triggers pf-badhost every night # nano /Library/LaunchDaemons/nightly.pfbadhost.update.plist Label nightly.pfbadhosts.update Program /usr/local/bin/refresh_pf-badhosts_nightly StandardErrorPath /var/log/pf-badhost/pf-update.log StandardOutPath /var/log/pf-badhost/pf-update.log StartCalendarInterval Hour 2 Minute 15 The wrapper script is: ## BEGIN script ## #! /bin/zsh # refresh_pf-badhosts_nightly # Script (1) checks if _pfbadhost user is present (and hasn't been deleted by macos update) # (2) checks if there is an active internet connection, and if so, # (3) runs '/usr/local/bin/pf-badhost -O macos' as _pfbadhost # But, if there is no connection, (4) the script sleeps for 30 sec then retries x 2 then fails. # redirect stdout/stderr to log file exec >> /var/log/pf-badhost/pf-update.log 2>&1 print "****\nRunning /usr/local/bin/refresh_pf-badhosts_nightly" print $(date) # Check if user _pfbadhost has been deleted by update and re-create if necessary dscl . list /Users | grep -q '_pfbadhost' if [[ $? -ne 0 ]]; then print "User _pfbadhost not present - recreating" sysadminctl -addUser _pfbadhost -fullName _pfbadhost -UID 301 -GID 61 -roleAccount else print "User _pfbadhost present" fi # Check if /etc/pf-badhost.txt is present if [[ ! -e /etc/pf-badhost.txt ]]; then print "/etc/pf-badhost.txt does not exist - exiting\n****\n" exit 1 fi print "\nchecking for internet connection" for n in {0..2}; do nc -w5 -z www.binarydefense.com 443 1&>/dev/null if [[ $? -eq 0 ]]; then print "internet connection active" print "now running '/usr/local/bin/pf-badhost -O macos' as _pfbadhost" sudo -u _pfbadhost /usr/local/bin/pf-badhost -O macos if [[ $? -eq 0 ]]; then print "pfbadhost completed sucessfully\n****\n" exit 0 else print "pf-badhost script did not sucessfully run - exiting\n****\n" exit 1 fi else print "server connection not available - sleeping for 30 seconds ..." sleep 30 fi done print "server connection failed - exiting\n****\n" exit 1 # End ## END script ## Change ownership and permissions on script: # sudo chown root:bin /usr/local/bin/refresh_pf-badhosts_nightly # sudo chmod 755 /usr/local/bin/refresh_pf-badhosts_nightly Now load the LaunchDaemon: # sudo launchctl bootstrap system refresh_pf-badhost_nightly This will work on a computer which is on and not sleeping. If this is a laptop or a host which is turned on and off, launchd behavior is: - If the computer was off at the configured time, the launchd job will not be run on next boot - If the computer was off at the configured time, the launchd job will be run at the next configured provided the computer is on. 10) If this is a laptop or a host which may be is turned on and off and and you want the computer to update the pf-badhost list if one or more updates were missed while the machine was off, we can create another script which runs at boot. This script checks if /etc/pf-badhost.txt has been recently updated (that is, if it is less than 24 hours old) and updates it as necessary. ## BEGIN script ## #! /bin/zsh # refresh_pf-badhosts_on_boot # Script (1) checks date of last update of /etc/pf-badhost.txt and, if more than 24 hours, # (2) checks if user _pfbadhost is present and, if not, re-creates it, # (3) checks if there is an active internet connection, and if so, # (3) runs '/usr/local/bin/pf-badhost -O macos' as _pfbadhost. # But, if there is no connection, (4) the script sleeps for 30 sec then retries x 2 then fails. # If the pf-badhost list is < 24 hours old, nothing is done # (and the regular pf-badhost update LaunchAgent will run as scheduled). # redirect stdout/stderr to log file exec >> /var/log/pf-badhost/pf-update.log 2>&1 print "****\nRunning /usr/local/bin/refresh_pf-badhosts_on_boot" print $(date) # Check if _pfbadhost has been deleted by update and re-create if necessary dscl . list /Users | grep -q '_pfbadhost' if [[ $? -ne 0 ]]; then print "User _pfbadhost not present - recreating" sysadminctl -addUser _pfbadhost -fullName _pfbadhost -UID 301 -GID 61 -roleAccount else print "User _pfbadhost present" fi if [[ ! -e /etc/pf-badhost.txt ]]; then print "/etc/pf-badhost.txt does not exist - exiting\n ****\n" exit 1 fi file_mtime=$(stat -f %m /etc/pf-badhost.txt) current_time=$(date +%s) file_age=$(($current_time - $file_mtime)) if [[ $file_age -lt 86400 ]]; then print "/etc/pf-badhost.txt is less than 24 hours old - exiting\n ****\n" exit 0 fi print "\n/etc/pf-badhost.txt is more than 24 hours old - checking for internet connection" for n in {0..2}; do nc -w5 -z www.binarydefense.com 443 1&>/dev/null if [[ $? -eq 0 ]]; then print "internet connection active" print "now running '/usr/local/bin/pf-badhost -O macos'" sudo -u _pfbadhost /usr/local/bin/pf-badhost -O macos if [[ $? -eq 0 ]]; then print "pfbadhost completed sucessfully\n ****\n" exit 0 else print "pf-badhost script did not sucessfully run - exiting\n ****\n" exit 1 fi else print "server connection not available - sleeping for 30 seconds ..." sleep 30 fi done print "server connection failed - exiting\n ****\n" exit 1 # End ## END script ## Change ownership and permissions on script: # sudo chown root:bin /usr/local/bin/refresh_pf-badhosts_on_boot # sudo chmod 755 /usr/local/bin/refresh_pf-badhosts_on_boot And now, if you want to run the script at login, either a. create a cron job: # crontab -u _pfbadhost -e ... @reboot /usr/local/bin/refresh_pf-badhost_on_boot ... Or, b. create a LaunchAgent which runs the script at boot. # sudo nano /Library/LaunchAgent/refresh_pf-badhost_on_boot.plist Label refresh_pf-badhost_on_boot Program /usr/local/bin/refresh_pf-badhost_on_boot StandardErrorPath /var/log/pf-badhost/pf-update.log StandardOutPath /var/log/pf-badhost/pf-update.log RunAtLoad Now load the LaunchDaemon: # sudo launchctl bootstrap system refresh_pf-badhost_on_boot Or, if you want to run the script at a time of your choice, you can ignore the plist, change the script to your specifications, copy to your home directory, and just run prn: # sudo zsh $HOME/refresh_pf-badhost_prn 11) Setting up a pf firewall in MacOS: There are 3 ways to run pf on MacOS: a. You can start pf directly and not invoke any other firewall in System Preferences > Security & Privacy b. You can start Apple's socketfilterfw by selecting System Preferences > Security & Privacy > Firewall and click "Turn On Firewall." But, do not select "Enable stealth mode" under "Firewall Options." Then you can start your own instance of pf. These two firewalls work fine together operating as separate processes. c. You can turn on Apple's firewall (socketfilterfw) and also click "Enable stealth mode" to start pf. This will start pf but adds additional rules within anchors which block icmp among other things. We will use option a or b here and start our own pf instance. i. Back up the default pf.conf: # sudo cp /etc/pf.conf /etc/pf.conf.bak ii. Create a new pf.conf by editing the existing pf.conf: # sudo nano /etc/pf.conf The final pf.conf should look like this: # # Default PF configuration file. # # This file contains the main ruleset, which gets automatically loaded # at startup. PF will not be automatically enabled, however. Instead, # each component which utilizes PF is responsible for enabling and disabling # PF via -E and -X as documented in pfctl(8). That will ensure that PF # is disabled only when the last enable reference is released. # # Care must be taken to ensure that the main ruleset does not get flushed, # as the nested anchors rely on the anchor point defined here. In addition, # to the anchors loaded by this file, some system services would dynamically # insert anchors into the main ruleset. These anchors will be added only when # the system service is used and would removed on termination of the service. # # See pf.conf(5) for syntax. # table persist file "/private/etc/pf-badhost.txt" # # com.apple anchor point # scrub-anchor "com.apple/*" nat-anchor "com.apple/*" rdr-anchor "com.apple/*" dummynet-anchor "com.apple/*" anchor "com.apple/*" load anchor "com.apple" from "/etc/pf.anchors/com.apple" # Rules for egress interface block in quick on egress from block out quick on egress to The added lines like the table definition and the block commands are overwritten by system updates so backup your working pf.conf so you can easily restore after you update. # sudo cp /etc/pf.conf /etc/pf.conf.modified You can also use commercial software to manage pf, like Murus Firewall (https://www.murusfirewall.com/). Worth a look. iii. Create a LaunchAgent that starts this pf instance on boot: # sudo nano /Library/LaunchDaemons/pf.start.plist Disabled Label pf.start ProgramArguments /sbin/pfctl -Ef /etc/pf.conf RunAtLoad WorkingDirectory /var/run 12) To enable additional features such as IPv6, Subnet Aggregation, Geo-Blocking, Bogon Filtering or Authlog Scanning open "/usr/local/bin/pf-badhost" with your text editor of choice and find the "User Configuration Area" near the top of the file where you can enable features by setting their value to "1" --- See the "Notes" section below for more info. I would recommend you carefully read the part on Bogon Filtering, enable bogon filtering for IPv4 and IPv6, but exclude your local subnets in the section a bit further down. 13) Now reboot and this should start pf and the script to update pf if it /etc/pf-badhost.txt if it is over 24 hours old. Check is these occurred by reading the /var/log/pf-badhost/pf-update.log Yay! pf-badhost is now installed! Please read the man page for information on how to configure pf-badhost. The manpage can be found here: https://www.geoghegan.ca/pub/pf-badhost/0.5/man/man.txt To receive notification of new pf-badhost releases and updates please send an email to 'announce@geoghegan.ca' with a subject line and body of "subscribe pf-badhost" ################################################################### # Post Install Notes: ################################################################### 1) To add custom rules or enable features, or add alternate blocklists, See the "User Configuration Area" located at the top of the script. This area serves as a built in config file, so please feel free to edit it and experiment with all the features available within. --- Note: Most options can also be configured from the command line 2) NOTE: authlog analysis is not supported on MacOS 3) The script is able to detect which (if any) subnet aggregation utilities are installed and will try to "Do The Right Thing(tm)" and fallback to the best available option. If no subnet aggregation utility is found, the script will fallback to using a pure Perl IPv4 aggregator if Perl is installed. Despite its name, "aggregate6" supports both IPv4 and IPv6 addresses and is written in Python, whereas the "aggregate" utility supports only IPv4 addresses and is written in C and uses less memory and runs slightly faster. If both utilities are installed, the C based "aggregate" utility will be preferred for IPv4 aggregation, but the script will happily function if only one or the other is installed (or neither). --- Note: Subnet aggregation can be enabled with the '-A' switch on the commandline. * "aggregate" can be installed via: # brew install aggregate * "aggregate6" can be installed via: # pip install aggregate6 * The experimental aggregator "aggy" can be installed like so: ... # brew install go $ curl https://geoghegan.ca/pub/aggy/0.1/aggy.go $ go build aggy.go # install -m 755 -o root -g bin aggy /usr/local/bin/aggy ... 4) If you intend to run pf-badhost on a LAN or are using NAT etc, you will want to negate your local subnet range from the filter. This can be equally achieved via four different methods: i) Specify rule on command line (requires updating cron job): $ pf-badhost -O macos -r '!192.0.2.0/24' -r '!2001:db8::/64' ii) Specify path to text file containing list of rules (1 per line): $ pf-badhost -O macos -w '/path/to/rules.txt' iii) Edit built-in config file: # vi /usr/local/bin/pf-badhost ... # User Defined Rules: !192.0.2.0/24 !2001:db8::/64 ... iv) Conversely, you can 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.0.2.0/24 pass out quick on egress to 192.0.2.0/24 table persist file "/etc/pf-badhost.txt" block in quick on egress from block out quick on egress to ...