FreeBSD Agent Sandboxing: Bastille, Capsicum, and Deno
Practical testing of isolation mechanisms for AI agents on FreeBSD

Table of Contents

1. Summary

Tested three isolation approaches for AI agents on FreeBSD 14.3:

Approach Isolation Performance Network Control Verdict
Podman (Linux) Container Very slow Host network ❌ Not viable
Bastille jail OS-level Native pf firewall ✅ Production
Deno in jail Runtime Native Permissions ✅ Best UX

Winner: Deno running inside a Bastille jail provides defense-in-depth: OS isolation from jail + runtime permissions from Deno.

2. Architecture: The Keyless Proxy Pattern

┌─────────────────────────────────────────────────────────┐
│ FreeBSD Host (hydra)                                    │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │ Bastille Jail (claude-sandbox)                   │   │
│  │ IP: 10.20.30.5                                   │   │
│  │                                                  │   │
│  │  Deno Agent                                      │   │
│  │    --allow-net=192.168.86.22:4000               │   │
│  │    (no --allow-env, no --allow-read)            │   │
│  │                                                  │   │
│  └──────────────────┬───────────────────────────────┘   │
│                     │ pf NAT                            │
└─────────────────────┼───────────────────────────────────┘
                      │ HTTP only to proxy
                      ▼
┌─────────────────────────────────────────────────────────┐
│ Mac Mini (mini) - 192.168.86.22                         │
│                                                         │
│  LiteLLM Proxy :4000                                    │
│    ├── Ollama :11434 (local models)      ← Working     │
│    │     └── mistral, llama3.2, deepseek-coder-v2      │
│    ├── Gemini API                         ← Working     │
│    │     └── gemini-2.5-flash                          │
│    ├── Anthropic API                      ← No key     │
│    └── OpenAI API                         ← No key     │
│                                                         │
└─────────────────────────────────────────────────────────┘

Key property: The sandbox has zero access to API keys. Secrets live entirely on the proxy host. Even if agent code is compromised, it cannot exfiltrate credentials.

3. FreeBSD-Specific Isolation

3.1. Bastille Thin Jails

Native FreeBSD jail manager with ZFS integration:

# Create jail
bastille create claude-sandbox 14.3-RELEASE 10.20.30.5

# Start and enter
bastille start claude-sandbox
jexec $(jls -j claude-sandbox jid) /usr/local/bin/bash

# Install tools inside jail
pkg install -y node22 npm-node22 deno

Properties:

  • ~500MB per jail (thin, shared base)
  • ZFS snapshots for instant rollback
  • pf firewall for network egress control
  • Native performance (no emulation)

3.2. Capsicum (Not Tested)

FreeBSD's capability-mode sandbox for process-level isolation:

// After opening necessary file descriptors
cap_enter();
// Process now has ZERO ambient authority
// - No new file opens
// - No new network connections
// - Only pre-opened FDs usable

Use case: Sandboxing parsers, decoders within a single process.

3.3. pf Firewall for Jail Egress

/etc/pf.conf snippet:

claude_net="10.20.30.0/24"

# NAT jail traffic
nat on $ext_if from $claude_net to any -> ($ext_if)

# Allow only specific egress
pass out on $ext_if proto udp from $claude_net to any port 53
pass out on $ext_if proto tcp from $claude_net to any port { 80 443 }

4. Deno Runtime Permissions

Deno's permission system provides fine-grained control:

# Deny all by default, allow ONLY proxy
deno run --allow-net=192.168.86.22:4000 agent.ts

# What this blocks:
# - Environment variables (no --allow-env)
# - Filesystem access (no --allow-read/write)
# - Other network hosts (only proxy allowed)
# - Subprocess execution (no --allow-run)

4.1. Verified Behavior

# Attempt to reach google.com - BLOCKED
[root@claude-sandbox]# deno run --allow-net=192.168.86.22:4000 test.ts
┏ ⚠️  Deno requests net access to "google.com:443".
┠─ Requested by `fetch()` API.
┗ Allow? [y/n/A] > n
❌ Denied net access to "google.com:443".
error: Uncaught NotCapable: Requires net access to "google.com:443"

5. Agent Script Template

// agent.ts - Sandboxed LLM agent
// Run with: deno run --allow-net=192.168.86.22:4000 agent.ts

const PROXY = "http://192.168.86.22:4000";

interface Message {
  role: "user" | "assistant" | "system";
  content: string;
}

async function chat(model: string, messages: Message[]): Promise<string> {
  const response = await fetch(`${PROXY}/v1/chat/completions`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ model, messages, max_tokens: 500 })
  });

  if (!response.ok) {
    throw new Error(`${response.status}: ${await response.text()}`);
  }

  const data = await response.json();
  return data.choices?.[0]?.message?.content || "";
}

// Example: Use working models (Ollama or Gemini)
const result = await chat("ollama-mistral", [
  { role: "user", content: "Explain sandboxing in one sentence." }
]);
console.log(result);

6. Performance Observations

Tested on FreeBSD 14.3 (hydra):

Operation Native (jail) Linux emulation (Podman)
npm install claude-code ~30s 16+ min (killed)
npm install gemini-cli 12s Not tested
pkg install deno 8s N/A
Deno startup <100ms N/A

Conclusion: Linux binary emulation (linux64_enable) adds 10-20x overhead for npm operations. Use native FreeBSD packages.

7. What Works Through the Proxy

Tested 2026-06-19:

Model Status Notes
ollama-mistral ✅ 200 Via Ollama on mini
ollama/llama3.2 ✅ 200 Via Ollama on mini
ollama/deepseek-coder-v2 ✅ 200 Via Ollama on mini
gemini-2.5-flash ✅ 200 Google API key present
claude-sonnet-4-6 ❌ 401 No Anthropic key
gpt-4o-mini ❌ 500 No OpenAI key

8. Blockers and Next Steps

8.1. LiteLLM Authentication

The proxy currently requires an API key header even for models that don't need external auth (Ollama). Options:

  1. Configure master_key for sandbox IP range
  2. Add IP allowlist for unauthenticated access
  3. Generate sandbox-specific keys with rate limits

8.2. Claude Code Native Binary

Claude Code npm package doesn't include FreeBSD native binary:

Error: claude native binary not installed.
Either postinstall did not run (--ignore-scripts, some pnpm configs)
or the platform-native optional dependency was not downloaded

Workaround: Use Deno-based agents or Gemini CLI instead.

9. Comparison Matrix

Tool OS Isolation Level Network Control Overhead Complexity
Bastille FreeBSD OS (jail) pf firewall Zero Medium
Podman Linux/BSD* Container netns/pf Native/High* Medium
Deno Any Runtime Permissions Zero Low
Capsicum FreeBSD Process Inherited FDs Zero High
Docker sbx macOS microVM Network policy Low Low

* FreeBSD Podman uses Linux emulation, significantly slower.

10. Related Work

11. Appendix: Quick Reference

11.1. Start Sandbox Session

# On hydra
sudo bastille start claude-sandbox
sudo jexec $(jls -j claude-sandbox jid) /usr/local/bin/bash

# Inside jail
deno run --allow-net=192.168.86.22:4000 /tmp/agent.ts

11.2. Monitor from Host

# tmux session for jail console
tmux attach -t jail-sandbox

# Check jail status
sudo bastille list
sudo jls

# Check pf states for jail traffic
sudo pfctl -ss | grep 10.20.30

11.3. Cleanup

sudo bastille stop claude-sandbox