Security model¶
The gateway is intentionally small. Its security model is built on five principles:
- Default deny. Sensitive content is the default; free models cannot receive it; secret-bearing content is automatically upgraded to sensitive.
- Server-authoritative controls. Routing overrides require authenticated headers; client-supplied prompt content cannot affect routing.
- Loopback-first deployment. The gateway binds
127.0.0.1:9600; only the host nginx is public; TLS is terminated at the host. - 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.
- Fail-closed on safety, fail-open on availability. A privacy
or budget violation is a hard stop; an upstream outage becomes a
STOP_FOR_HUMANrequest 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.productionbeing root-only and the gateway never echoing it. The rotation procedure is indocs-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.
.gitignoreexcludes.env*(except.env.example) and standard secret files..env.examplecontains only placeholder values.
What about a leaked client-side key?¶
If a client API key is leaked:
- Stop traffic:
docker compose -p smart-supervisor-production -f /srv/apps/smart-supervisor-production/docker-compose.production.yml stop gateway. - Rotate the key in
.env.productionimmediately. - Restart the gateway.
- Audit
sgw_requests_totaland 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). - 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¶
- Security → Sensitive data.
- Security → Free-model policy.
- The secret-rotation procedure is in
docs-internal/operations/secret-rotation.md(operator-only).