How to Configure pf Firewall, Packet Filtering Rules, and NAT Port Forwarding in macOS Terminal

Architectural Foundation of macOS Packet Filter (PF) and Darwin Kernel Networking

In macOS, network traffic security operates across two fundamentally distinct abstraction layers: the Application Layer Firewall (ALF, managed by socketfilterfw accessible in System Settings) and the low-level, kernel-resident Packet Filter (PF). While the ALF monitors which application binaries are authorized to bind listening sockets, the BSD-derived Packet Filter intercepts raw IP packets directly within the Darwin XNU kernel networking stack prior to socket allocation.

PF provides high-throughput stateful inspection, network address translation (NAT), packet redirection, and connection rate limiting. Rather than editing monolithic legacy rulesets directly, macOS organizes PF rules using a modular, declarative Anchor Hierarchy. Root rules reside in /etc/pf.conf, which delegates specific firewall subsystems to sub-rulesets stored in /etc/pf.anchors/.

Firewall Subsystem Kernel Layer Administrative Utility Configuration Path Primary Capabilities
Application Firewall (ALF) Socket layer / User space socketfilterfw /Library/Preferences/com.apple.alf.plist Binary signing checks; prompt for incoming app connections
Packet Filter (PF) XNU Kernel (Layer 3 & Layer 4) pfctl /etc/pf.conf & /etc/pf.anchors/ Deep packet inspection, port blocking, NAT, port forwarding

Rule Syntax, Macros, and Strict Evaluation Ordering

PF enforces strict syntactic ordering: Translation (NAT / RDR) rules must precede Filtering (Pass / Block) rules. A syntax error or incorrect rule ordering prevents the entire firewall ruleset from compiling into kernel memory.

By default, PF evaluates rules from top to bottom; the last matching rule determines packet disposition unless the quick keyword is appended. Specifying quick forces immediate rule execution, halting downstream evaluation.

# General PF Syntax Model:
action [direction] [log] [quick] [on interface] [af] [proto protocol] \
       [from src_addr [port src_port]] [to dst_addr [port dst_port]] \
       [flags tcp_flags] [state]

Step-by-Step Implementation: Building a Custom macOS Firewall Anchor

Step 1: Create a Dedicated Anchor Ruleset

To preserve Apple system updates that periodically overwrite /etc/pf.conf, store custom rules in an isolated anchor file under /etc/pf.anchors/:

# Create the custom corporate anchor definition
sudo tee /etc/pf.anchors/com.custom.firewall > /dev/null << 'EOF'
# ==========================================================
# Custom macOS PF Anchor: com.custom.firewall
# ==========================================================

# 1. Macro and Interface Declarations
ext_if = "en0"
vpn_if = "utun1"

# 2. Table Definitions for Blacklisted IP Subnets
table <blocked_ips> persist { 198.51.100.0/24, 203.0.113.50 }

# 3. Port Forwarding / Redirection Rules (NAT / RDR)
# Forward inbound traffic on port 8080 to an internal container on port 80:
rdr pass on $ext_if proto tcp from any to any port 8080 -> 127.0.0.1 port 80

# 4. Filtering Rules
# Block all traffic matching the blacklisted IP table immediately
block in quick log on $ext_if from <blocked_ips> to any

# Drop incoming ICMP ping echo requests (stealth mode)
block in quick proto icmp all icmp-type echoreq

# Block inbound SSH (Port 22) from external interfaces while permitting LAN
block in quick on $ext_if proto tcp from any to any port 22
pass in quick on $ext_if proto tcp from 192.168.1.0/24 to any port 22 keep state

# Permit full outbound traffic and track stateful TCP connections
pass out quick all keep state
EOF

# Enforce secure root permissions on the anchor file
sudo chmod 644 /etc/pf.anchors/com.custom.firewall
sudo chown root:wheel /etc/pf.anchors/com.custom.firewall

Step 2: Link the Anchor within /etc/pf.conf

Open /etc/pf.conf in a text editor with root privileges. Register the anchor name in both the NAT section and the Filter section:

# In the Translation section (beneath existing rdr-anchor lines):
rdr-anchor "com.custom.firewall"

# In the Filter section (beneath existing anchor lines):
anchor "com.custom.firewall"
load anchor "com.custom.firewall" from "/etc/pf.anchors/com.custom.firewall"

Step 3: Validate and Test the Ruleset via pfctl

Never load a ruleset into the kernel without verifying syntax. An undetected parse error will abort firewall activation.

# Perform dry-run syntax verification
sudo pfctl -vnf /etc/pf.conf

# If validation returns no errors, load the ruleset into the active kernel
sudo pfctl -ef /etc/pf.conf

# Inspect active filter rules currently running in kernel space
sudo pfctl -sr

# Inspect active translation and port redirection rules
sudo pfctl -sn

# Inspect real-time table entries
sudo pfctl -t blocked_ips -T show

Enforcing Persistent PF Activation across System Reboots

By default, macOS disables the Packet Filter across system reboots unless invoked by an active VPN client or sharing service. To ensure custom firewall policies remain active continuously, deploy a system LaunchDaemon.

sudo tee /Library/LaunchDaemons/com.custom.pfctl.plist > /dev/null << 'EOF'
<?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.custom.pfctl</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/>
    <key>StandardErrorPath</key>
    <string>/var/log/pfctl.err</string>
    <key>StandardOutPath</key>
    <string>/var/log/pfctl.out</string>
</dict>
</plist>
EOF

# Set required permissions and load daemon into system domain
sudo chown root:wheel /Library/LaunchDaemons/com.custom.pfctl.plist
sudo chmod 644 /Library/LaunchDaemons/com.custom.pfctl.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/com.custom.pfctl.plist

Real-Time Packet Logging and Diagnostics via pflog0

When debugging dropped packets, PF dispatches logging events to a virtual network tap interface termed pflog0.

# Check if the pflog0 virtual interface is present
ifconfig pflog0

# Monitor blocked packet headers in real time via tcpdump
sudo tcpdump -nettti pflog0

Every rule tagged with the log keyword streams intercepted packet headers to this listener, allowing engineers to audit source ports, destination addresses, and TCP sequence flags without interfering with production throughput.

Troubleshooting macOS PF Firewall Faults

Observed Fault Probable Root Cause Technical Diagnostic Method Remediation Protocol
pfctl: Rules must be in order: options, normalization, queueing, translation, filtering rdr or nat translation rule placed below a pass or block filter rule Run sudo pfctl -vnf /etc/pf.conf Relocate all rdr and nat directives above filter declarations in ruleset
Firewall Rules Lost After macOS Minor OS Update Apple installer updated /etc/pf.conf to system default stock version Check /etc/pf.conf for missing custom anchor directives Maintain custom rules strictly in /etc/pf.anchors/; use automated script to append anchor to pf.conf
Total Network Lockout (Cannot SSH or browse) Default block all rule loaded without corresponding pass out or loopback rule Terminal becomes unresponsive Access machine locally via console; execute sudo pfctl -d to disable firewall immediately
pfctl: pf not enabled Firewall compiled rules but daemon has not been enabled in the kernel Run sudo pfctl -si; check Status indicator Execute sudo pfctl -e to activate kernel inspection engine
Port Forwarding (RDR) Fails for Local Loopback Packet Filter RDR rules do not redirect packets originating from localhost by design Test curl request from an external machine rather than local 127.0.0.1 For local testing, configure loopback redirection or test via secondary LAN host

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.