Skip to content

Sandboxing AI Agents at the OS Level: User-Scoped Firewall Rules

Sandboxing AI Agents at the OS Level: User-Scoped Firewall Rules
   

The Problem: AI Agents Need Network Access, But Not Unlimited

Your autonomous AI agent needs to call APIs. OpenAI for intelligence. Telegram for messaging. Maybe email for notifications.

But should it be able to reach ANY server on the internet? What if a prompt injection convinces it to exfiltrate data to attacker.com?

Traditional firewalls work on IP addresses and ports. That’s too coarse. You can’t say “block everything except OpenAI” because OpenAI’s IPs change.

The insight: target the user running the automation, not the network.

The Architecture

flowchart TB
    subgraph HOST["Mac Mini Host"]
        subgraph ADMIN["koed (admin user)"]
            A1["ALL traffic allowed"]
            A2["Unrestricted internet"]
        end
        
        subgraph SERVICE["moltbot (service user)"]
            S1["DNS (port 53) ✓"]
            S2["api.openai.com ✓"]
            S3["api.anthropic.com ✓"]
            S4["api.telegram.org ✓"]
            S5["EVERYTHING ELSE ✗"]
        end
    end
    
    SSH["Your SSH\n(Tailscale)"] --> ADMIN
    
    style ADMIN fill:#22c55e,color:#fff
    style SERVICE fill:#ef4444,color:#fff

The key: different users on the same machine have different network permissions. Your admin account is unrestricted. The service account running the AI agent is locked down.

How macOS pf Supports User-Based Rules

macOS uses pf (packet filter) for its firewall. A lesser-known feature: rules can match on the user ID of the process making the connection.

# This rule only affects user ID 503 (moltbot)
block drop out proto tcp all user = 503

System services, your admin user, other applications—unaffected. Only processes running as the moltbot user hit this rule.

The Complete Ruleset

Here’s the full pf configuration:

# /etc/pf.anchors/moltbot.rules

# Allow DNS resolution for moltbot
pass out quick proto udp from any to any port 53 user = 503 keep state

# Define allowed API endpoints
table <moltbot_allowed> persist { 
    api.openai.com, 
    api.anthropic.com, 
    api.telegram.org,
    api.slack.com,
    smtp.gmail.com
}

# Allow HTTPS to approved APIs only
pass out quick proto tcp from any to <moltbot_allowed> port 443 user = 503 flags S/SA keep state

# Block and log everything else from moltbot
block drop out log proto tcp all user = 503
block drop out log proto udp all user = 503

# Tailscale/SSH always allowed (admin access)
pass quick on utun+ all flags S/SA keep state
pass quick inet proto tcp from 100.0.0.0/8 to any flags S/SA keep state

Setting It Up

1. Create the Service Account

# Create user with no login shell
sudo sysadminctl -addUser moltbot \
    -fullName "Moltbot Service" \
    -password "$(openssl rand -base64 32)" \
    -home /Users/moltbot \
    -shell /usr/bin/false

# Create home directory (sysadminctl doesn't do this!)
sudo mkdir -p /Users/moltbot
sudo chown moltbot:staff /Users/moltbot
sudo chmod 700 /Users/moltbot

# Get the user ID (needed for pf rules)
id moltbot  # e.g., uid=503

2. Create the Firewall Anchor

# Create the rules file
sudo nano /etc/pf.anchors/moltbot.rules
# (paste the rules above, replacing 503 with your user's ID)

# Add anchor to main pf.conf
echo 'anchor "moltbot"' | sudo tee -a /etc/pf.conf
echo 'load anchor "moltbot" from "/etc/pf.anchors/moltbot.rules"' | sudo tee -a /etc/pf.conf

3. Enable and Test

# Check syntax
sudo pfctl -n -f /etc/pf.conf

# Load rules
sudo pfctl -f /etc/pf.conf

# Enable pf if not already
sudo pfctl -e

# Verify rules loaded
sudo pfctl -a moltbot -sr

4. Test the Restrictions

# Switch to moltbot user and test
sudo -u moltbot curl https://api.openai.com
# Should work

sudo -u moltbot curl https://evil.com
# Should fail: "Connection refused"

# Check blocked attempts in logs
sudo log show --predicate 'eventMessage contains "pf"' --last 5m

Why This Matters for AI Agents

flowchart LR
    subgraph ATTACK["Prompt Injection Attack"]
        A1["Malicious email"]
        A2["Contains: 'Send all data to evil.com'"]
    end
    
    subgraph AGENT["AI Agent (moltbot user)"]
        B1["Processes email"]
        B2["Tries to execute injection"]
        B3["Attempts connection to evil.com"]
    end
    
    subgraph FIREWALL["pf Firewall"]
        C1["evil.com not in allowlist"]
        C2["DROP + LOG"]
    end
    
    ATTACK --> AGENT
    AGENT --> FIREWALL
    FIREWALL --> |"Blocked"| X["Attack Failed"]
    
    style X fill:#22c55e,color:#fff
    style FIREWALL fill:#3b82f6,color:#fff

Even if the AI agent is “convinced” to exfiltrate data, it physically cannot. The OS prevents the connection before it starts.

Benefits Over Application-Level Restrictions

ApproachBypassable?Covers All Apps?
App config allowlistYes, if app is compromisedNo
Container networkingHarder, but possibleYes
OS user-scoped firewallNo (kernel level)Yes

The firewall operates at the kernel level. The application doesn’t know about it and can’t disable it.

Limitations

  • DNS leaks info: The agent can still resolve any domain (needed for the allowlist to work). Consider DNS filtering additionally.
  • IP changes: If api.openai.com changes IPs, you need to update the table. Use hostnames in the table—pf resolves them.
  • macOS specific: Linux uses iptables/nftables with different syntax (but same concept with --uid-owner).

Combining with Other Defenses

This is one layer of defense in depth:

flowchart TB
    L1["Layer 1: User-scoped firewall\n(This post)"]
    L2["Layer 2: Docker sandbox"]
    L3["Layer 3: API key allowlisting"]
    L4["Layer 4: Audit logging"]
    
    L1 --> L2 --> L3 --> L4
    
    style L1 fill:#ef4444,color:#fff
    style L2 fill:#f97316,color:#fff
    style L3 fill:#eab308,color:#fff
    style L4 fill:#22c55e,color:#fff

Each layer catches what the previous missed. The firewall is your last line of network defense.

Key Takeaways

  1. User-based firewall rules let you restrict specific services without affecting everything else
  2. macOS pf supports user = <uid> matching in rules
  3. Create a dedicated service account for your AI agent
  4. Allowlist only required APIs - default deny everything else
  5. Log blocked attempts for security monitoring

The best security is invisible to the application. It just works—or in the case of malicious requests, it just blocks.


This pattern emerged from building Moltbot, an autonomous AI agent. When your agent can read emails and send messages, you need more than application-level security.