Security model
- What an agent cannot do
- Egress
- On DLP / injection scanning
- Audit receipts
- Test coverage, honestly
- Terms of service
- Reporting a vulnerability
Isolation is layered — each layer holds on its own, so a gap in one doesn’t open the sandbox. The credential’s confidentiality does not rest on any single control.
agent code (untrusted)
│
┌────┴─────────────────────────────────────────────────────────────────┐
│ 1. Process non-root (uid 1001) · CapEff=0 · seccomp RuntimeDefault │
│ 2. Mounts no service-account token · read-only where possible · │
│ setuid bits stripped from the image │
│ 3. Chokepoint nftables OUTPUT default-DROP — k8s permits Warden's uid; │
│ Docker permits only the per-session Warden IP/port. DNS │
│ cut. Pure L3/L4 enforcement makes Warden unbypassable │
│ 4. Egress Warden holds ALL destination policy — domain / IP / │
│ policy CIDR allow · deny · inspect · kill · private-range + │
│ metadata floors · signed audit (+ DLP regex — telemetry,│
│ not containment; see below) │
│ 5. Credential sandbox holds a realistic DECOY; Warden gets the real │
│ token via host state (Docker) or a per-Pod Secret (k8s) │
│ 6. Hygiene mediation details removed from the child environment; │
│ useful noise reduction, not an isolation boundary │
└────────────────────────────────────────────────────────────────────────┘
What an agent cannot do
- Reach a raw IP — the kernel chokepoint drops every destination except the Warden pinhole, and Warden denies any CONNECT target not covered by policy (bare IPs and private/metadata ranges are denied by default; an operator can allow specific IPs/CIDRs per agent). DNS is cut, so the agent can’t even resolve hosts itself — Warden resolves and dials exactly the evaluated address.
- Read the real credential — it isn’t in the sandbox’s env, filesystem, or memory. Only Warden (a separate container) holds it, and the sandbox’s copy is a decoy.
- Modify the firewall — no
CAP_NET_ADMINafter the entrypoint drops privileges. - Escalate to the cluster — there is no mounted service-account token.
Egress
Warden is mandatory; there is no unmediated path, and no flag that could create one. The manifest builder cannot emit a sandbox without the sidecar, and the Docker runner refuses to launch if the sidecar didn’t come up.
| Mode | Selected by | Credential | Behavior |
|---|---|---|---|
| Warden (only mode) | always on | Warden process; Docker state file or per-Pod Kubernetes Secret supplies it | full mediation: TLS-terminate, inject, allow-list, DLP, audit |
Warden is the single policy brain: it enforces the destination policy — domains, IP
literals, or CIDRs for allow/deny/inspect — evaluated against the resolved address, which
makes it rebinding-safe. The private-range block and cloud-metadata deny are policy defaults
an explicit allow CIDR can lift (metadata only via a separate allow_metadata flag), so
“let this agent reach 10.20.0.0/16” is one per-agent policy entry — still tunneled, audited,
and kill-switchable.
Every session — API-key or subscription — routes through its per-session Warden, and the real credential is injected at the egress boundary, so it never enters the sandbox in any mode.
On DLP / injection scanning
The DLP pass is a compact regex scan. Treat it as telemetry and a tripwire, not a containment boundary.
It cannot stop a motivated agent from exfiltrating data over an allow-listed channel — base64, gzip, field-splitting, or simply the prompt body all defeat a regex — and traffic to the credentialed LLM host is logged, not blocked. It blocks exfil to an untrusted (non-credentialed) host in enforce mode, and logs on the agent’s allow-listed LLM traffic.
The credential’s confidentiality does not rest on it. It rests on layers 3–5: the credential is injected at the egress boundary and never enters the sandbox (which holds only a decoy), behind a default-drop firewall. DLP narrows the easy exfil paths and makes the rest auditable; that is its job.
Audit receipts
Warden signs an HMAC-chained receipt for every connection decision, one independent chain per session. The orchestrator persists the per-session key, so a chain stays verifiable long after the sandbox is gone:
terra-cli verify-egress <session-id>
The chain is drained onto the orchestrator’s own volume on every runner, which is what makes post-mortem verification work for exactly the sessions worth reviewing.
Test coverage, honestly
- Unit-tested in CI: the Rust Warden logic — allow-list, SSRF guard, credential
injection, audit chain (
cargo test, 38 tests) — plus the Python control plane (structural assertions that mediation cannot be disabled, that every durable store is0600, that download refuses symlinks and traversal). - Manual before a deploy: the firewall pinhole, sandbox isolation and concealment are
exercised by image-based red-team scripts (
make redteam,make redteam-pinhole,make redteam-conceal). They need a built sandbox image, so they are not in the unit CI. - Not continuously asserted: on-cluster behavior is spot-checked.
Terms of service
Running a Claude subscription credential through a programmatic MITM is against Anthropic’s ToS for that token type. Terrarium supports the subscription path because it was explicitly authorized for the self-hosted, single-operator deployment it was built for — the real token is never printed, logged, or exposed to the sandbox.
If you have not obtained that authorization, use the API-key path (ANTHROPIC_API_KEY),
which carries no such restriction. Decide this before you deploy; it is your call to make,
not a default to inherit.
Reporting a vulnerability
Open a security advisory rather than a public issue.