Skip to main content
A Tower’s security is network topology + a closed enforcement engine, not trust in the agent runtime. The runtime is assumed hostile; it is boxed so that its only route to the outside world is through a process that asks the governor first and holds the keys itself.

The compose topology

One compose file wires four networks (image/compose.yaml; the golden image boots the equivalent compose.tower.yaml): The agent runtime sits on agentnet alone. It cannot reach the internet, the database, or vaultd directly — only the gateway. Isolation is selective: the gateway is reachable, everything else is not.

The load-bearing test

ci/topology.test.ts boots the full stack and asserts the four properties that make a Tower a Tower. It is never skipped.
1

Direct runtime egress FAILS

curl https://api.stripe.com from inside agent-runtime cannot even establish a connection — no route, no DNS, no HTTP status ever returned. (Sanity check in the same test: the gateway is reachable, proving isolation is selective, not total.)
2

The gateway terminating path WORKS — two ways, both audited

The same class of call, sent through the gateway’s terminating path, succeeds:
  • a sandbox operator gets a synthetic body — no real egress, no injection;
  • a production operator’s call is forwarded over the gateway’s own TLS connection with the vaultd-injected credential (the echo provider reflects the injected header back).
Each writes an activity.logged audit row stamped with the operator’s environment.
3

A non-allowlisted CONNECT is REFUSED and audited

A CONNECT to blocked.example.com is answered 403 (no tunnel opens). The refusal is audited by host + verdict only — never tunnel contents.
4

Governor stopped ⇒ 403 enforcement_unavailable

With the governor stopped, a gated action through the gateway returns 403 enforcement_unavailable. The open plumbing + gateway are inert without the closed engine — fail closed, no cached verdicts.

vaultd — the TPM-rooted secrets daemon

vaultd (Go, closed) holds the vault root key and derives everything else. It runs with network_mode: none and speaks JSON over the UNIX socket /run/vault/vaultd.sock (owner-only, 0600); only api and the gateway, which bind-mount the socket, can reach it. Key hierarchy
  • Root key — TPM2-sealed against PCR 7 (Secure Boot state). A tamper or an absent TPM means vaultd cannot unseal and stays inert (fail closed). Dev/CI use --dev-root-file (a 32-byte hex key) or swtpm. On the golden image the root file currently lives on the LUKS/TPM-unlocked /data, so the TPM protects it transitively; direct vaultd TPM sealing (and PCR 11) is a documented beta.
  • Per-tenant KEK = HKDF-SHA256(root, salt="naive-vault-kek", info="company_id|tenant_user_id"). The root and the KEKs never leave vaultd.
  • Per-entry DEK — a fresh data key, wrapped by the tenant KEK.
  • Envelope — every wrapped value is nonce(12) || tag(16) || ciphertext (AES-256-GCM), exactly the cloud KMS layout, so vault_entries rows port unchanged.
The socket API is generateDataKey, decryptDataKey, reEncrypt, use, and health. use() is how the gateway injects a credential: vaultd decrypts under (ctx, ref), applies a registered injection function, and returns the injected result (e.g. a header directive) — never the secret.
No reveal on a Tower. The KMS driver’s optional reveal has no vaultd counterpart; the method is compiled out on towerbuild and the vault reveal route returns 501. Plaintext secrets never leave vaultd on a Tower.

The egress gateway — three paths

The egress-gateway (closed) is the only egress-capable process on a Tower. Every request first resolves a per-operator NAIVE_RUNTIME_TOKEN to { company, tenant_user, agent_profile, environment } before asking the governor. All governor calls go through the fail-closed governorCall() wrapper.

1 · Region API passthrough

/v1/* → the region api. The governed surface. No injection.

2 · Terminating provider endpoints

/proxy/<name>/*. The gateway terminates TLS, asks the governor, and then: sandbox ⇒ synthetic body; production ⇒ fetch the credential via vaultd use(), set the header, open its own TLS to the provider.

3 · Generic web egress (CONNECT)

An HTTP CONNECT tunnel. The governor decides the host/SNI allowlist per operator + environment. Allow or refuse only.
Credential injection happens only at path 2, where the gateway is the terminating endpoint for a named provider. It is never applied to the generic CONNECT tunnel and never to arbitrary destinations. Every path writes an audit row (path 3 records host + verdict only).

Limits (what the gateway deliberately does NOT do)

  • No interception of a CONNECT tunnel. Path 3 is an opaque TCP relay: allow or refuse. No credential is set inside it and no synthetic 200 is faked in it.
  • The MITM-CA is explicitly REJECTED. The gateway does not install a trusted CA to man-in-the-middle TLS. If a destination needs a credential it must be a named terminating forward (path 2), where the gateway is the legitimate endpoint; otherwise egress is allow/deny only.
  • No enforcement without the governor. A governor error or timeout is deny-all (enforcement_unavailable), never fail-open.

gVisor and the runc fallback

The agent runtime is sandboxed with gVisor (runsc) on the golden image. runsc interposes a userspace kernel and therefore needs a real Linux kernel underneath. macOS Docker Desktop has no such kernel available to runsc, so the compose dev stack on macOS uses runc (recorded in PREFLIGHT.md). This changes only the in-sandbox isolation layer — the routing assertions above hold identically under either runtime, because isolation on a Tower is enforced by network topology, not by the sandbox runtime. runsc is exercised where it can be: in the golden image and in Linux CI (10-docker.sh installs runsc with the containerd SystemdCgroup=true fix; compose.tower.yaml runs the real runsc agent-runtime).
This is why the compose quickstart works on a Mac while the production isolation guarantee is validated on Linux: the topology test proves the load-bearing routing on either host, and the gVisor sandbox is proven on the image itself. See Tower golden image.