macOS ships with two independent firewall layers: a user-friendly Application Firewall aimed at desktop users, and pf, the OpenBSD-derived packet filter that powers the same kind of rule-based network control you would find on a BSD router. This guide walks through enabling both, then explains how to write and persist custom pf rules to restrict network access on a Mac.
Before You Begin
You will need:
- Administrator access on the Mac you are configuring
- Basic familiarity with Terminal and editing files with
sudo - Knowledge of which ports and IPs you want to allow or block
- macOS 11 (Big Sur) or later — commands are the same through macOS 15
The Two macOS Firewall Layers
| Layer | Scope | Config Location | Tool |
|---|---|---|---|
| Application Firewall | Per-app inbound, signing-identity aware | System Settings > Network > Firewall | /usr/libexec/ApplicationFirewall/socketfilterfw |
| pf (packet filter) | Stateful packet-level inbound and outbound | /etc/pf.conf, /etc/pf.anchors/ | pfctl |
The Application Firewall is simple and safe for most users. pf is powerful but has no GUI — it is the right tool when you need to block specific IP ranges, rate-limit connections, or restrict outbound traffic from a server Mac.
Step 1: Enable the Application Firewall (GUI)
- Open System Settings
- Click Network in the sidebar
- Click Firewall
- Toggle Firewall on
- Click Options... to allow or block specific apps
- Enable Stealth Mode to drop unsolicited probes silently
Enabling from the Command Line
The Application Firewall is controlled by the socketfilterfw binary:
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
Turn on stealth mode so the Mac does not respond to ICMP or closed-port probes:
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
Automatically allow signed built-in and downloaded apps:
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsigned on
Check the current state at any time:
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
Step 2: Understand pf Rule Syntax
pf rules live in /etc/pf.conf. Each rule has a consistent shape:
<action> <direction> <on interface> <proto> from <source> to <destination> <port>
Common building blocks:
| Keyword | Purpose |
|---|---|
block / pass | Drop or allow the matching packets |
in / out | Direction relative to the host |
on en0 | Restrict rule to a specific interface |
proto tcp | Match TCP (or udp, icmp) |
from any to any | Source and destination (IPs, CIDRs, tables) |
port 22 | Destination port |
quick | Stop rule evaluation on match |
log | Send matching packets to the pflog0 interface |
Step 3: Write a Custom Anchor
Rather than editing /etc/pf.conf directly (which macOS may overwrite during updates), create your own anchor file and reference it from the main config.
-
Create the anchor file:
sudo nano /etc/pf.anchors/com.inventivehq.rules -
Add an example ruleset that blocks inbound SSH from the public internet but allows it from the local subnet:
# Block everything inbound by default
block in all
# Allow loopback
pass quick on lo0 all
# Allow established outbound
pass out proto { tcp udp icmp } all keep state
# Allow SSH from the local subnet only
pass in quick proto tcp from 192.168.1.0/24 to any port 22
# Explicitly log and block SSH from anywhere else
block in log proto tcp from any to any port 22
- Reference the anchor from
/etc/pf.conf. Edit the file and add, near the bottom:
anchor "com.inventivehq.rules"
load anchor "com.inventivehq.rules" from "/etc/pf.anchors/com.inventivehq.rules"
- Test the syntax without loading the rules:
sudo pfctl -vnf /etc/pf.conf
Step 4: Enable pf and Load Your Rules
Enable pf and load the updated config:
sudo pfctl -E -f /etc/pf.conf
View the active ruleset to confirm your rules loaded:
sudo pfctl -sr
Check overall status:
sudo pfctl -s info
To disable pf temporarily:
sudo pfctl -d
Step 5: Make pf Rules Persist Across Reboot
macOS does not reload /etc/pf.conf automatically after a restart. Create a LaunchDaemon to run pfctl at boot:
-
Create the plist:
sudo nano /Library/LaunchDaemons/com.inventivehq.pf.plist -
Paste the following:
<?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>Label</key>
<string>com.inventivehq.pf</string>
<key>ProgramArguments</key>
<array>
<string>/sbin/pfctl</string>
<string>-e</string>
<string>-f</string>
<string>/etc/pf.conf</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
- Set permissions and load it:
sudo chown root:wheel /Library/LaunchDaemons/com.inventivehq.pf.plist
sudo launchctl load /Library/LaunchDaemons/com.inventivehq.pf.plist
After the next reboot, pf will be re-enabled with your rules.
Step 6: Log and Inspect Blocked Traffic
Any rule with the log keyword sends copies of matching packets to the virtual pflog0 interface. Watch it live:
sudo tcpdump -n -e -ttt -i pflog0
You will see one line per blocked or logged packet with the source, destination, and port. This is the fastest way to confirm a new rule is doing what you expect.
Security Considerations
- Always test rules interactively with
pfctl -vnfbefore loading — a bad ruleset can lock you out over SSH - Prefer custom anchors over editing
/etc/pf.confso OS updates do not clobber your work - Combine Application Firewall stealth mode with a default-deny pf inbound policy for defense in depth
- Remember that pf does not decrypt TLS — it can only filter by IP, port, and protocol
- If you use Little Snitch or LuLu for outbound control, scope pf to inbound rules to avoid duplicated logic
Troubleshooting
pfctl: pf already enabled
pf was already running. This is harmless — your -f flag still reloaded the ruleset.
Rules do not survive reboot
Check the LaunchDaemon loaded correctly with sudo launchctl list | grep com.inventivehq.pf. If it is missing, verify the plist is owned by root:wheel and has no XML syntax errors.
Locked out over SSH
Boot into Recovery Mode, mount the system volume, and remove or rename /etc/pf.anchors/com.inventivehq.rules to clear the bad ruleset.