
An agent that can call any host on the internet is an unaudited data pipe. One hallucinated URL, one compromised dependency, or one runaway retry loop is enough to leak data. Pinning the agent to its approved endpoints is a one-time setup that removes that surface.
Start on the Figment virtual Intel macOS environment at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk). It runs macOS on Intel via Linux/KVM, with no GPU or audio, and is accessed through the browser console. For an agent that calls a small, stable set of HTTPS APIs, that is enough: pf is part of the macOS base system and enforces IP/port egress on this environment.
Move up to an M-series Mac from $95/month when the agent needs Apple silicon features — ARM execution, Core ML, or Vision framework work — on top of the network restrictions. The exact chip, RAM and disk at $95 are confirmed per quote, so request a configuration before you commit. Physical i3 Macs at $49 and M-series units collect requests until supply is confirmed.
If you are cross-shopping, check the vendor pricing pages directly: CloudMac publishes $99.99/month and a $999/year prepay on cloudmac.org (effective $83.25/month, below 12×$95 at Figment) for an M4/16GB/256GB; MacStadium lists M2.S at $109 and M4.S at $149 with support and a published 99.99% uptime target. Figment's difference on managed plans is the specific CI setup, backups and monitoring work, not a hardware spec match.
macOS ships with two independent network controls, and neither does everything on its own.
| Layer | What it filters | Per-process | Hostnames | Best for |
|---|---|---|---|---|
pf (packet filter) | IP addresses, ports, protocols | No | No (IPs only) | Broad egress allow-lists |
| Application Firewall | Signed binaries, inbound only | No | No | Not useful for agent egress |
| LuLu (Objective-See) | Outbound per process | Yes | Resolved at prompt time | Per-agent rules |
Use pf as the baseline deny-by-default on the virtual Intel macOS. Add LuLu (version 4.5.7, requires macOS 10.15+) on an M-series host where you can approve its system extension in System Settings. In the $29 virtual environment, system-extension approval is not guaranteed; confirm in the configuration quote if you need per-process rules there, and otherwise rely on pf plus application-level allow-lists.
Document every destination the agent legitimately needs before you write a rule. This table is the artifact you verify against.
| Service | Hostname | Port | Protocol | Enforcement |
|---|---|---|---|---|
| OpenAI API | api.openai.com | 443 | TCP/HTTPS | pf CIDR + LuLu rule |
| Anthropic API | api.anthropic.com | 443 | TCP/HTTPS | pf CIDR + LuLu rule |
| GitHub (self-hosted runner) | endpoints from GitHub meta API | 443 | TCP/HTTPS | pf CIDR |
| Your artifact store | your bucket host | 443 | TCP/HTTPS | pf CIDR |
| Local loopback | 127.0.0.1 | any | any | pf pass |
| Everything else | * | any | any | pf default deny |
pf matches on IPs, not hostnames. Resolve each host with dig +short at deploy time and schedule a weekly re-resolution. Pull provider ranges from the vendor's own published source (for example GitHub's /meta API or an AWS IP-range feed) rather than inventing CIDRs. Do not add App Store Connect or TestFlight "webhook" ranges — Apple does not publish such an allow-list; update only the firewalls and CI services you actually control.
The macOS pf configuration lives in /etc/pf.conf and loads anchor files from /etc/pf.anchors/. Keep agent rules in their own anchor. See man pfctl and man pf.conf on the host for the exact syntax of your macOS version.
Create /etc/pf.anchors/agent-egress:
# /etc/pf.anchors/agent-egress
# pf uses last-match semantics unless 'quick' is set. Put the broad
# default-deny first and let specific allow rules override it below.
# Replace the IPs with values re-resolved from your destination policy.
block out all
# Loopback
pass out on lo0 all
# DNS so the agent can resolve its own destinations
pass out proto udp to any port 53
# HTTPS to approved service IPs (re-resolve before each deploy)
pass out proto tcp to <openai_ips> port 443
pass out proto tcp to <anthropic_ips> port 443
pass out proto tcp to <github_ips> port 443
Reference the anchor from /etc/pf.conf:
anchor "agent-egress"
load anchor "agent-egress" from "/etc/pf.anchors/agent-egress"
Check syntax, then load:
sudo pfctl -n -f /etc/pf.conf
sudo pfctl -ef /etc/pf.conf
Persist across reboots with a LaunchDaemon plist in /Library/LaunchDaemons/ — the location Apple documents for system daemons in the launchd developer archive. Set the plist to invoke pfctl -ef /etc/pf.conf at load; keep the plist owned by root with mode 644.
Run this after every rule change. It scores reachability against the policy and exits non-zero on any mismatch.
#!/bin/bash
# verify_network_policy.sh
set -uo pipefail
PASS=0; FAIL=0
check() {
local label="$1" host="$2" port="$3" expect="$4"
if nc -z -G 3 -w 3 "$host" "$port" 2>/dev/null; then
reached=yes
else
reached=no
fi
if [ "$reached" = "$expect" ]; then
echo "PASS $label (expected=$expect)"
PASS=$((PASS+1))
else
echo "FAIL $label (expected=$expect got=$reached)"
FAIL=$((FAIL+1))
fi
}
# Allowed
check "OpenAI" api.openai.com 443 yes
check "Anthropic" api.anthropic.com 443 yes
check "GitHub API" api.github.com 443 yes
# Must be blocked
check "SMTP egress" smtp.gmail.com 587 no
check "Random web" example.com 80 no
check "Metadata" checkip.amazonaws.com 80 no
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]
A FAIL on any blocked line means a rule gap — fix it before the agent handles sensitive data. pf drops silently by default, so an unexpected timeout in the agent's own logs is the usual signal of a new destination you have not approved.
Cloud providers rotate IPs. Subscribe to the provider's own range feed, re-resolve the hostnames in your destination policy weekly, and diff against your anchor. When the agent needs a new endpoint, add it to the policy table first, update pf, then re-run the verification script.
Keep API keys in the host's Keychain or your CI secret manager, not in a plaintext env file. If you move secrets between hosts, transfer the names and restore values from the secret manager on the new host — never dump the whole environment to disk.
Write the destination policy table first; it takes thirty minutes and eliminates most future debugging. Then browse Mac hosting plans and request your configuration to confirm chip, RAM, disk and whether per-process controls like LuLu are supported on the plan you pick.
Related reading