# neuronetz-gateway — Threat Model From [`scope-docs/SPEC.md`](../scope-docs/SPEC.md) §3. The governing principle, in one line: > **Fail closed, always.** If a security or budgeting check cannot be performed (Redis down, > DB unreachable, ambiguous state), **deny** the request. Never default to allow. > (AGENT_PROMPT non-negotiable #1.) The gateway exists because the Ollama instance at `api.neuronetz.ai` was exposed without authentication — a standing security incident. Every defense below traces back to closing that gap and keeping it closed. --- ## Threats & mitigations (SPEC §3) | Threat | Mitigation | |---|---| | Internet scanners hitting Ollama directly | Ollama bound to the internal Docker network; **never published**. No `ports:` mapping in any shipped compose file. | | Unauthenticated API abuse | Mandatory Bearer token; **fail-closed** on auth errors (401). | | API key brute force | Argon2id hashing; constant-time compare; rate limit on auth failures per source IP (`AUTH_FAILURE_RATE_LIMIT_PER_IP_PER_MIN`). | | GPU/token exhaustion (cost attack) | Per-key TPM + token budget; per-tenant ceiling; concurrent-connection cap. | | Resource exhaustion via large payloads | Request body size limit (default 256 KiB); `num_predict` cap (default 4096). | | Model enumeration / training-data exfil via uncommon models | Model allowlist, **default-deny**. Discovery only exposes models actually installed; `/api/tags` and `/v1/models` never reveal models outside the tenant's effective set; "not allowed" and "doesn't exist" return the **same** generic response. | | Discovery backend unreachable | **Fail-closed:** an empty/stale-expired discovered set means no model resolves, so requests are denied — never "allow because we couldn't list models." | | Ollama mutation (model pull/delete) by attacker | Endpoint allowlist; mutating endpoints (`/api/pull`, `/api/push`, `/api/create`, `/api/copy`, `/api/delete`, `/api/blobs/*`) **hard-blocked** at the gateway, not configurable. | | Information disclosure via error messages | Upstream errors **sanitized** at the boundary; Ollama internals never proxied to the client. Each error carries an `X-Request-ID` for correlation. | | Audit log tampering | Append-only at the app layer; DB role separation; optional WAL archiving. | | Prompt data leakage | Prompt logging **off by default**; opt-in per key; TTL'd retention; redaction hook. | | Redis outage causing "fail open" | **Fail-closed:** if the rate-limit/budget backend is unavailable, deny (503), not allow. | | Compromised admin token | There is **no admin endpoint** in the gateway. Admin lives in `neuronetz-console`; the gateway has nothing to compromise here. | --- ## Notes on selected defenses ### `allow_all_models` is an audited opt-in `allow_all_models` lets a tenant use any currently-installed model, so models newly pulled into Ollama are auto-granted on the next discovery refresh. This is convenient but widens the attack surface for *that tenant*, so it is: - **opt-in per tenant** (default `false`), set explicitly via the CLI (`create-tenant --allow-all-models` or `set-models --allow-all`); - **overridable per key** — a non-`NULL` key-level `allow_all_models` overrides the tenant flag; otherwise the tenant flag applies (SPEC §13.7); - **audited** — every request records the model used in `gateway.audit_log`. Default-deny tenants instead see only `allowed_models ∩ discovered`. Either way the effective set is always intersected with the *live* discovered set, so stale or typo'd allowlist entries never resolve. ### No existence disclosure A model that is installed-but-unpermitted and a model that is not installed both return the **same** generic `403`. An attacker cannot use the gateway to enumerate which models exist on the backend (SPEC §13.6). ### Sanitized errors + request IDs Clients never receive Ollama's error text, stack traces, or internal hostnames. Errors are mapped to generic `4xx`/`5xx` JSON with a `request_id`. Operators correlate that ID with the audit log to investigate without leaking internals to callers (SPEC §4.3 step 14). ### Streaming integrity is also a safety property Token counting and audit writes happen **after** stream close, never on the hot path. This keeps time-to-first-byte honest and ensures budget decrements and audit rows reflect the true final token counts reported by Ollama (`prompt_eval_count` + `eval_count`), not estimates. --- ## Out of scope (v0.1.0) Documented as future work, **not** mitigations present today: content moderation / prompt-injection filtering, response caching, multi-backend routing, billing, SSO/OAuth2 for admin, and any web admin UI (that lives in `neuronetz-console`).