Security model

  1. What an agent cannot do
  2. Egress
  3. On DLP / injection scanning
  4. Audit receipts
  5. Test coverage, honestly
  6. Terms of service
  7. 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_ADMIN after 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 is 0600, 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.