Skip to main content
macOSintermediate

How to Use the macOS Firewall (pf) to Restrict Network Access

Learn how to configure both the macOS Application Firewall and the low-level pf packet filter to restrict inbound and outbound network access on macOS.

7 min readUpdated April 2026

Want us to handle this for you?

Get expert help →

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

LayerScopeConfig LocationTool
Application FirewallPer-app inbound, signing-identity awareSystem 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)

  1. Open System Settings
  2. Click Network in the sidebar
  3. Click Firewall
  4. Toggle Firewall on
  5. Click Options... to allow or block specific apps
  6. 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:

KeywordPurpose
block / passDrop or allow the matching packets
in / outDirection relative to the host
on en0Restrict rule to a specific interface
proto tcpMatch TCP (or udp, icmp)
from any to anySource and destination (IPs, CIDRs, tables)
port 22Destination port
quickStop rule evaluation on match
logSend 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.

  1. Create the anchor file: sudo nano /etc/pf.anchors/com.inventivehq.rules

  2. 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
  1. 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"
  1. 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:

  1. Create the plist: sudo nano /Library/LaunchDaemons/com.inventivehq.pf.plist

  2. 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>
  1. 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 -vnf before loading — a bad ruleset can lock you out over SSH
  • Prefer custom anchors over editing /etc/pf.conf so 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.


Frequently Asked Questions

Find answers to common questions

SIP does not block pfpfctl is a signed Apple binary and can load custom rulesets at runtime. However, SIP does protect /etc/pf.conf and the default anchors in /etc/pf.anchors/ from being modified by non-root processes. Always edit these with sudo, and prefer adding your own anchor file rather than modifying Apple's defaults so future macOS updates do not overwrite your rules.

No, not by default. macOS does not automatically load custom rules from /etc/pf.conf on boot the way FreeBSD does. To make your rules survive reboot, create a LaunchDaemon plist in /Library/LaunchDaemons/ that runs pfctl -e -f /etc/pf.conf (or a path to your own ruleset) at startup. Without a LaunchDaemon, pfctl -e only enables pf for the current session.

Little Snitch and LuLu operate at a different layer — they hook into the Network Extension framework to filter per-application outbound connections, while pf filters at the packet level. In most cases they coexist, but if pf blocks a packet the application firewall will never see it. If you rely on Little Snitch for outbound control, keep pf rules focused on inbound traffic to avoid confusing overlaps.

The Application Firewall is a user-facing, application-aware filter that allows or blocks inbound connections based on the signing identity of an app. pf is a stateful packet filter inherited from OpenBSD that operates on IPs, ports, and protocols regardless of which process owns the socket. They can run simultaneously, and pf rules take effect at a lower layer than the Application Firewall.

Run sudo pfctl -sr to show the active ruleset, sudo pfctl -sa to see all information including states and interface stats, and sudo pfctl -si to view statistics. If you want to confirm pf is enabled, run sudo pfctl -s info — the first line will say Status: Enabled or Status: Disabled.

Add the log keyword to any rule, for example block in log proto tcp from any to any port 22. Then read the pflog0 interface with sudo tcpdump -n -e -ttt -i pflog0. macOS also writes captured pf log data to /var/log/pf.log if you configure the pflog service, but running tcpdump directly against pflog0 is the simplest way to watch blocks in real time.

Need Professional IT & Security Help?

Our team of experts is ready to help protect and optimize your technology infrastructure.