Back to articles

Mac Firewall - Persistent Custom Settings

Published: 2026-05-07

macOS uses PF (Packet Filter) as its default firewall.

My use-case was, I wanted to lock a Mac down to only be reachable from my Tailscale network.

This guide covers how to create rules, enable the firewall, and ensure they survive a reboot.


1. Create Your Rule File

PF rules live in .conf files. You can edit the system’s default /etc/pf.conf, but it’s safer to create your own anchor file to avoid being overwritten by macOS updates.

Create a directory for your custom rules:

sudo mkdir -p /etc/pf.anchors
sudo nano /etc/pf.anchors/com.user.custom

Basic Rule Examples

# Block all incoming traffic from a specific IP
block in from 192.168.1.50 to any

# Allow traffic on a specific port (e.g., 8080)
pass in proto tcp from any to any port 8080

# Block everything by default, allow specific traffic (Advanced)
# block all
# pass out all

BONUS my Tailscale settings to allow IN only from tailnet

# Macros
int_if = "utun4"
phys_if = "en0"

# Options
set skip on lo0

# 1. Default Policy: Block everything coming IN, but allow everything OUT
block in all
pass out all

# 2. The VIP Lane: Allow all traffic coming IN from Tailscale
pass in on $int_if all

# 3. Specific LAN exceptions (Optional)
# If you use a physical keyboard/monitor, you don't need this.
# But if you want to SSH from a local computer NOT on Tailscale,
# uncomment the line below:
# pass in on $phys_if proto tcp from any to any port 22

# 4. Tailscale Handshake (Essential)
# Allow Tailscale peers to find you via the physical wire
pass in on $phys_if proto udp from any to any port 41641

2. Test and Enable the Firewall

Always validate your syntax before committing. A single typo in a firewall config can lock you out of your own machine.

Syntax Check

sudo pfctl -vnf /etc/pf.anchors/com.user.custom

If there are no errors, you’ll see the rules printed back out.

Manual Enable

To turn on the firewall and load your rules immediately:

sudo pfctl -e -f /etc/pf.anchors/com.user.custom
  • -e: Enable PF.
  • -f: Load rules from the specified file.

3. Making It Persistent (Auto-Start)

By default, PF resets on reboot. To make it stick, we use a LaunchDaemon.

Create the LaunchDaemon Plist

sudo nano /Library/LaunchDaemons/com.user.pfctl.plist

Paste This Configuration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.pfctl</string>
    <key>ProgramArguments</key>
    <array>
        <string>/sbin/pfctl</string>
        <string>-e</string>
        <string>-f</string>
        <string>/etc/pf.anchors/com.user.custom</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardErrorPath</key>
    <string>/var/log/pfctl.err.log</string>
</dict>
</plist>

Set Permissions and Load

sudo chown root:wheel /Library/LaunchDaemons/com.user.pfctl.plist
sudo chmod 644 /Library/LaunchDaemons/com.user.pfctl.plist
sudo launchctl load /Library/LaunchDaemons/com.user.pfctl.plist

4. Confirming the Status

After a reboot (or loading the daemon), verify that PF is actually filtering traffic.

Check if PF is Running

sudo pfctl -s info

Look for Status: Enabled.

View Active Rules

sudo pfctl -s rules

This lists every rule currently enforced. If you see your custom rules here, you’re done.

Check for Errors

cat /var/log/pfctl.err.log

Note: To disable your custom setup at any time:

sudo launchctl unload /Library/LaunchDaemons/com.user.pfctl.plist

Once again, a how to for myself that I hope others find useful.