Skip to content

Security model

The gateway is intentionally small. Its security model is built on five principles:

  1. Default deny. Sensitive content is the default; free models cannot receive it; secret-bearing content is automatically upgraded to sensitive.
  2. Server-authoritative controls. Routing overrides require authenticated headers; client-supplied prompt content cannot affect routing.
  3. Loopback-first deployment. The gateway binds 127.0.0.1:9600; only the host nginx is public; TLS is terminated at the host.
  4. Stateless where possible. Only the budget ledger and a small in-process health registry are stateful. There is no per-user record store, no message archive, and no prompt cache.
  5. Fail-closed on safety, fail-open on availability. A privacy or budget violation is a hard stop; an upstream outage becomes a STOP_FOR_HUMAN request rather than a silent downgrade.

Defense in depth

Layer What it does Where
TLS Public TLS, cert auto-renewed by certbot host nginx
Authentication Bearer token required for every client request FastAPI
Identity tier Session/goal/task resolution; sets reclassify scope identity.py
Privacy gate Secret redaction + sensitivity upgrade privacy_gate.py
Routing policy Hard Pro rules, budget caps, model health filtering policy.py
Upstream auth OpenRouter key held server-side clients/openrouter.py
Container isolation Production runs in a dedicated Docker network docker-compose.production.yml
Filesystem .env.production is 0600 root:root operator runbook

What's not in v1.0.0

  • Real key verification. The current implementation only checks that the bearer token is non-trivial in length. Adding value verification is an open architectural decision tracked in PROJECT-COMPLETION-REPORT.md §"Unresolved Items".
  • Per-user audit. There is a single tenant; the gateway does not partition logs by caller.
  • MFA / SSO. Not applicable to a single-user deployment.

Threat model

The threat model in frozen-v3/14-threat-model.md enumerates threats and mitigations:

  • Compromised OpenRouter key. Mitigated by .env.production being root-only and the gateway never echoing it. The rotation procedure is in docs-internal/operations/secret-rotation.md (operator-only).
  • Compromised gateway key. Same as above. Stop traffic first, rotate, restart, audit logs.
  • Internal attacker with shell on the VPS. Has the keys, but is also responsible for the keys. Mitigation: standard server hardening, restricted shell access, audit trail in /var/log/nginx/.
  • External attacker. Can only reach the public nginx. The gateway is loopback-only. The PostgreSQL container is on a private bridge network.
  • Prompt injection. Cannot override the privacy gate or the routing policy. Markers in message bodies (/pro, [reasoning:high]) affect the policy but cannot bypass privacy or health.
  • Tool-output exfiltration. Tool output is untrusted data. The privacy gate scans it like any other message body. There is no control-plane channel through tool output.

How secrets are handled

  • OpenRouter key: server-side only, in .env.production (0600). Never sent to the client.
  • Gateway API key: server-side only, in .env.production. Sent to the client via the operator's secret rotation procedure.
  • Database password: server-side only, in .env.production. The database is not public.
  • TLS private key: /etc/letsencrypt/live/.../privkey.pem, root-only.
  • No secrets in the repository. .gitignore excludes .env* (except .env.example) and standard secret files. .env.example contains only placeholder values.

What about a leaked client-side key?

If a client API key is leaked:

  1. Stop traffic: docker compose -p smart-supervisor-production -f /srv/apps/smart-supervisor-production/docker-compose.production.yml stop gateway.
  2. Rotate the key in .env.production immediately.
  3. Restart the gateway.
  4. Audit sgw_requests_total and the structured routing logs for any traffic from the leaked identity (the only label today is "authenticated" — log inspection requires the request body, which the gateway does not keep).
  5. File an incident report at docs-internal/operations/incidents/ (operator-only).

Because the gateway does not currently verify the bearer token's value, a leaked key cannot be revoked by the gateway alone; you must also restart with a new key. This is the architectural caveat tracked in PROJECT-COMPLETION-REPORT.md.

See also