Blocking incoming hostile traffic dynamically with PF and fail2ban on OS X

The bigger picture is that I am trying to dynamically block/unblock IPs using fail2ban 0.8.14. I have been following an old guide for OS 10.5 with doesn’t include PF.

I think I’ve configured everything correctly, but OS X’s PF is causing me some issues.

Talaninc has the IP 192.168.1.68 and is the “local”

Celebgul has the IP 192.168.1.50 and is the “remote”


From what I understand, I should be able to issue the following command:

user@celebgul:~$ sudo /sbin/pfctl -t fail2ban -T add 192.168.1.68/32

This will add the IP address 192.168.1.68 to the table fail2ban, and will block incoming traffic from Talantinc, thus the following should fail:

forquare@talantinc:~$ ssh [email protected]
Password:
Last login: Mon Aug  3 12:09:05 2015 from talantinc.local
user@celebgul:~$ 

But as you can see, it succeeds.


From what I can see, my understanding is wrong. I’ve added 192.168.1.68, and yet can still SSH from it to Celebgul.

I can query the table and see 192.168.1.68 in it:

user@celebgul:~$ sudo pfctl -t fail2ban -T show
No ALTQ support in kernel
ALTQ related functions disabled
   192.168.1.68

I haven’t changed any of Apple’s default configuration files for PF, my understanding was that I’d only need to edit/create my own if I wanted rules that were longstanding/persisted across reboots.

How can I use PF to block incoming connections on OS X Client (not Server)?

I’m specifically looking at El Capitan, however I cannot get this to work on Mavericks either.


Solution 1:

Here is an improved walkthrough to install fail2ban on OS X 10.10 (it probably works on 10.9 also) based on the (somehow faulty) guide at forgetcomputers.zendisk.com.

The automated installer didn't work at all for me so I did it manually.

  1. cd to ~/Downloads and download fail2ban-0.8.10

    cd ~/Downloads
    curl -O https://forgetcomputers.zendesk.com/hc/en-us/article_attachments/200287990/fail2ban-0.8.10.tar.gz
    
  2. Unpack the tar package:

    tar xzf fail2ban-0.8.10.tar.gz
    
  3. cd to fail2ban-0.8.10 and install the software:

    cd fail2ban-0.8.10/
    sudo python setup.py install
    
  4. Make a file for the log:

    sudo touch /var/log/fail2ban.log
    
  5. cd back and download the modifications package:

    cd ~/Downloads/
    curl -O https://forgetcomputers.zendesk.com/hc/en-us/article_attachments/200287980/install_fail2ban_mods.tar.gz
    
  6. Unpack this package:

    tar xzf install_fail2ban_mods.tar.gz
    
  7. Run the install script from the modifications package:

    sudo ./fail2ban_mods/install_fail2ban_mod.sh
    
  8. Make yourself sudo and rename /etc/fail2ban/jail.local (the file jail.local is superior to jail.conf and might break everything because the installed file contains a totally useless configuration):

    sudo bash
    mv /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak
    
  9. Add the following two lines to /etc/pf.conf with nano /etc/pf.conf:

    table <fail2ban> persist  
    block drop log quick from <fail2ban> to any
    
  10. In /etc/fail2ban/jail.conf, modify the [ssh-pf] section at the end with nano as follows:

    [ssh-pf]  
    
    enabled  = true  
    filter   = sshd  
    action   = pf  
    logpath  = /var/log/system.log  
    maxretry = 3  
    

    You may enter another maxretry count or define an individual bantime or findtime.

  11. In /etc/fail2ban/action.d/pf.conf, ensure that the following values are set and modify them if necessary with nano /etc/fail2ban/action.d/pf.conf:

    actionban = /sbin/pfctl -t fail2ban -T add <ip>  
    actionunban = pfctl -t fail2ban -T delete `pfctl -t fail2ban -T show 2>/dev/null | grep <ip>`  
    [Init] 
    tablename = fail2ban
    localhost = 127.0.0.1
    
  12. Shutdown pf, tell it to reload its configuration, and start it again:

    pfctl -d
    pfctl -f /etc/pf.conf
    pfctl -e
    
  13. Stop the fail2ban daemon if it is already running, and start it with launchctl:

    fail2ban-client stop
    launchctl load -w /Library/LaunchDaemons/org.fail2ban.init.plist 
    launchctl load -w /Library/LaunchDaemons/org.fail2ban.redo.plist 
    launchctl load -w /Library/LaunchDaemons/org.fail2ban.reset.plist
    

Testing the System

  1. Open a terminal window and watch fail2ban's log (live-update):

    sudo tail -f /var/log/fail2ban.log
    
  2. While keeping this terminal active on the server, SSH into the server from a client and watch the server's terminal output (username is arbitrary, since we are testing what will happen when an incorrect login is attempted; replace server_ip with the IP address or hostname of the server):

     ssh username@server_ip
    
  3. On the client machine, type the wrong password several times until you see a message in fail2ban's log that indicates that the client has been banned. This message will look something like this:

    2015-08-04 18:56:25,001 fail2ban.actions [216]: NOTICE [ssh-pf] Ban 192.168.8.15

    When you see this message, the client machine's IP has been banned. At this point, any future SSH attempts from this IP (within fail2ban's bantime period) should time-out and be unsuccessful.

  4. To stop tail just enter ctrlC

If you want to install the latest fail2ban 0.9.1

  1. Download manually fail2ban to your ~/Downloads folder

  2. cd to ~/Downloads and unpack the tar package:

    cd ~/Downloads
    tar xzf fail2ban-0.9.1.tar.gz
    
  3. cd to fail2ban-0.9.1 and install the software:

    cd fail2ban-0.9.1/
    sudo python setup.py install
    
  4. Make a file for the log:

    sudo touch /var/log/fail2ban.log
    
  5. cd back and download the modifications package:

    cd ~/Downloads/
    curl -O https://forgetcomputers.zendesk.com/hc/en-us/article_attachments/200287980/install_fail2ban_mods.tar.gz
    
  6. Unpack this package:

    tar xzf install_fail2ban_mods.tar.gz
    
  7. Make a backup of /etc/fail2ban/filter.d/sshd.conf and copy the file from the mod_pack to the fail2ban/filter.d directory. You may copy the other filter.conf but better make a backup of the original files. I didn't test these though.

    sudo bash
    mv /etc/fail2ban/filter.d/sshd.conf /etc/fail2ban/filter.d/sshd.conf.old
    cp ~/Downloads/fail2ban_mods/filter.d/sshd.conf /etc/fail2ban/filter.d/
    cp ~/Downloads/fail2ban_mods/fail2ban_reset.sh /private/etc/fail2ban
    cp ~/Downloads/fail2ban_mods/lib-launchdaemons/org.fail2ban* /Library/LaunchDaemons
    
  8. Add the following two lines to /etc/pf.conf with nano /etc/pf.conf:

    table <fail2ban> persist  
    block drop log quick from <fail2ban> to any
    
  9. In /etc/fail2ban/jail.conf, modify the [ssh-pf] section at the end with nano as follows:

    [ssh-pf]  
    
    enabled  = true  
    filter   = sshd  
    action   = pf  
    logpath  = /var/log/system.log  
    maxretry = 3  
    

    You may enter another maxretry count or define an individual bantime or findtime.

  10. In /etc/fail2ban/action.d/pf.conf, ensure that the following values are set and modify them if necessary with nano /etc/fail2ban/action.d/pf.conf:

    actionban = /sbin/pfctl -t fail2ban -T add <ip>/32
    actionunban = /sbin/pfctl -t fail2ban -T delete <ip>/32 
    [Init] 
    tablename = fail2ban
    
  11. Shutdown pf, tell it to reload its configuration, and start it again:

    pfctl -d
    pfctl -f /etc/pf.conf
    pfctl -e
    
  12. Stop the fail2ban daemon if it is already running and start it again:

    fail2ban-client stop
    launchctl load -w /Library/LaunchDaemons/org.fail2ban.init.plist 
    launchctl load -w /Library/LaunchDaemons/org.fail2ban.redo.plist 
    launchctl load -w /Library/LaunchDaemons/org.fail2ban.reset.plist
    

Now test your system again like described above.

Improvements

Since the implementation of the fail2ban table and rule is non-standard and real fail2ban-blocking doesn't survive a reboot, I reworked the whole pf-mechanism, created a separate anchor (inspired by IceFloor) to remove any dependency on Apple's /etc/pf.conf file and modified the ban action.

  1. Starting from the fail2ban 0.9.1 install described above enter:

    sudo bash
    nano /etc/fail2ban/action.d/pf.conf 
    

    and change in the file /etc/fail2ban/action.d/pf.conf the lines starting with

    actionban = .... 
    

    to

    actionban = /sbin/pfctl -a fail2ban.anchor -t fail2ban -T add <ip>/32 && /sbin/pfctl -k <ip>/32 && /sbin/pfctl -Ef /etc/fail2ban/pf/fail2ban.conf
    

    and

    actionunban = ....
    

    to

    actionunban = /sbin/pfctl -a fail2ban.anchor -t fail2ban -T delete <ip>/32
    
  2. Create a folder pf in /etc/fail2ban/

    mkdir /etc/fail2ban/pf
    
  3. Create three files fail2ban, fail2ban.conf and fail2ban.sh in the previously made folder with the following content; afterwards make fail2ban.sh executable with chmod:

    fail2ban:

    table <fail2ban> persist
    block drop log quick from <fail2ban> to any
    

    fail2ban.conf:

    ############### LOOPBACK ###############
    #
    # skip loopback (no filtering on loopback interface)
    set skip on lo0
    
    scrub-anchor "com.apple/*"
    
    ############### INBOUND ###############
    #
    anchor "fail2ban.anchor"
    load anchor "fail2ban.anchor" from "/etc/fail2ban/pf/fail2ban"
    

    fail2ban.sh:

    #!/bin/sh
    
    # start
    #
    # We need to trap on TERM signals, according to Apple's launchd docs:
    #
    trap 'exit 1' 15
    
    #
    # Use the "ipconfig waitall" command to wait for all the interfaces to come up:
    #
    ipconfig waitall
    sleep 5
    #
    # System sysctl 
    #
    sysctl -w net.inet6.ip6.fw.verbose=0
    sysctl -w net.inet.ip.fw.verbose=0
    sysctl -w net.inet.ip.fw.verbose_limit=0
    
    #
    # interface forwarding enabled by default
    #
    sysctl -w net.inet.ip.forwarding=1
    
    # enable PF and load rules from default fail2ban configuration file using tokens (apple specific PF options -E and -X)
    #
    /sbin/pfctl -e
    /sbin/pfctl -Ef /etc/fail2ban/pf/fail2ban.conf
    
    
    # Exit with a clean status
    exit 0
    
    # this file is public domain and is available to everyone with no exceptions.
    
  4. Create the file org.fail2ban.plist in /Library/LaunchDaemon with the following content:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>Disabled</key>
        <false/>
        <key>ExitTimeOut</key>
        <integer>1</integer>
        <key>Label</key>
        <string>org.fail2ban</string>
        <key>Program</key>
        <string>/etc/fail2ban/pf/fail2ban.sh</string>
        <key>RunAtLoad</key>
        <true/>
    </dict>
    </plist>
    
  5. Remove the following two lines from /etc/pf.conf with nano /etc/pf.conf:

    table <fail2ban> persist  
    block drop log quick from <fail2ban> to any
    
  6. Load the file /Library/LaunchDaemons/org.fail2ban.plist with launchctl after stopping fail2ban and pf:

    fail2ban-client stop
    pfctl -d
    pfctl -f /etc/pf.conf
    launchctl load -w /Library/LaunchDaemons/org.fail2ban.plist
    fail2ban-client start
    

    or reboot your Mac after loading org.fail2ban.plist with launchctl.

Now banning works properly even after rebooting your system. If you want to add an IP-address manually to block it, just enter:

sudo /sbin/pfctl -a fail2ban.anchor -t fail2ban -T add <ip>/32 && /sbin/pfctl -k <ip>/32 && /sbin/pfctl -Ef /etc/fail2ban/pf/fail2ban.conf

To un-ban it (it will not automatically be un-banned after the fail2ban bantime!) enter:

sudo /sbin/pfctl -a fail2ban.anchor -t fail2ban -T delete <ip>/32

So to answer your question:

If you want to use the fail2ban table to ban an IP manually after applying the improvements, you have to enter the command above. The reason for the additional parts:

  • /sbin/pfctl -k <ip>/32 is needed to kill all of the state entries originating from the specified host.
  • /sbin/pfctl -Ef /etc/fail2ban/pf/fail2ban.conf to reload the fail2ban.conf gracefully and reflect the changes you made in the table fail2ban.

Without the above improvements you may use:

sudo /sbin/pfctl -t fail2ban -T add <ip>/32 && /sbin/pfctl -k <ip>/32 && /sbin/pfctl -Ef /etc/pf.conf

Installation of fail2ban 0.9.1 on El Capitan

I got it installed on El Capitan in rootless mode (sudo nvram boot-args="rootless=0" and reboot) after removing the doc-install part to /usr/share/docs/fail2ban (= the lines 140-143) in the setup.py of fail2ban 0.9.1.

Use the improved 0.9.1 install and config method. If the command pfctl -sA doesn't reveal fail2ban.anchor check for the correct double-quotes (no smart-quotes!) in the file /etc/fail2ban/pf/fail2ban.conf.

Solution 2:

Installing Server.app has several benefits for traditional UNIX hands since OS X makes configuration choices on the client OS that don't work well for people that live in the command line world. (Max files per process, VM tuning, other various kernel tuning changes)

Barring an easy solution, I would recommend installing Server and then enabling OS X adaptive firewall to ensure you have the proper launchd services enabled.

  • https://support.apple.com/en-us/HT200259

In a nutshell, the above article asks for:

sudo pfctl -f /etc/pf.conf
sudo /Applications/Server.app/Contents/ServerRoot/usr/sbin/serverctl enable service=com.apple.afctl
sudo /Applications/Server.app/Contents/ServerRoot/usr/libexec/afctl -c
sudo /Applications/Server.app/Contents/ServerRoot/usr/libexec/afctl -f

Followed by step 2:

sudo defaults write /System/Library/LaunchDaemons/com.apple.pfctl ProgramArguments '(pfctl, -f, /etc/pf.conf, -e)'
sudo chmod 644 /System/Library/LaunchDaemons/com.apple.pfctl.plist
sudo plutil -convert xml1 /System/Library/LaunchDaemons/com.apple.pfctl.plist

From there, you can then use the pfctl / afctl framework to block hosts and ports as needed. You can test things by trying ssh brute force attacks and seeing that the firewall is working (or is not) before trying to customize the firewall further.

Solution 3:

I was missing a piece of the puzzle that klanomath kindly filled by linking to this “Setting up Fail2ban on Mac OS X 10.7+” post, and Buscar웃 reinforced with this “Integrating PF with Fail2ban 0.9” post.

While PF was adding the IP address to the fail2ban table, it didn’t know what to do with it. There are a couple of ways to achieve this:

The first post linked above suggests modifying /etc/pf.conf and adding the following:

table <fail2ban> persist
block drop log quick from <fail2ban> to any

The second post suggests a more correct route, creating a new “anchor” file /etc/fail2ban/pf-anchor.conf and inserting the following:

table <fail2ban> counters
block drop log quick from <fail2ban> to any

It then references this anchor in /etc/pf.conf by inserting:

anchor fail2ban

I haven’t verified the second method, but my initial testing with the first method has solved my issue with PF. Thank you to klanomoath and Buscar웃 for posting links to relevant guides, either my search queries were way off or it’s the first time DuckDuckGo has let me down.