> ## Documentation Index
> Fetch the complete documentation index at: https://usenaive.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Security model — topology, vaultd, and the egress gateway

> The compose network topology and the four load-bearing assertions, vaultd's TPM-rooted key hierarchy and envelope, the egress gateway's three paths and its explicit limits, and the gVisor/runc distinction.

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`):

| Network    | Mode                 | Members                                      | Purpose                                                             |
| :--------- | :------------------- | :------------------------------------------- | :------------------------------------------------------------------ |
| `frontnet` | egress-capable       | panel, device-agent, **egress-gateway**, api | the only place with a route to the internet                         |
| `agentnet` | `internal: true`     | agent-runtime ↔ egress-gateway               | the runtime's **only** peer is the gateway; it has no route out     |
| `datanet`  | `internal: true`     | postgres, **governor**, api, gateway         | the closed governor + DB, reachable only from api/gateway           |
| vaultd     | `network_mode: none` | vaultd                                       | no network at all; reachable only over its bind-mounted UNIX socket |

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.

<Steps>
  <Step title="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.)
  </Step>

  <Step title="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**.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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**.

<Warning>
  **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.
</Warning>

## 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.

<CardGroup cols={3}>
  <Card title="1 · Region API passthrough" icon="arrow-right-arrow-left">
    `/v1/*` → the region api. The governed surface. **No injection.**
  </Card>

  <Card title="2 · Terminating provider endpoints" icon="key">
    `/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.
  </Card>

  <Card title="3 · Generic web egress (CONNECT)" icon="tower-broadcast">
    An HTTP `CONNECT` tunnel. The governor decides the host/SNI allowlist per
    operator + environment. **Allow or refuse only.**
  </Card>
</CardGroup>

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).

<Note>
  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](/docs/self-hosting/tower-image).
</Note>
