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:
- Configure
master_keyfor sandbox IP range - Add IP allowlist for unauthenticated access
- 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
- Agent Sandbox Architectures - Cloudflare, Docker, Deno comparison
- Efrit - Native Emacs coding agent (elisp)
- Bastille - FreeBSD container manager
- LiteLLM - OpenAI-compatible proxy for multiple providers
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