LLMProxy — Threat Model & Security Whitepaper
A formal, code-grounded threat model for LLMProxy. Every control below maps to a real module in this repository (cited as path.py), and every coverage number is generated by an executable test — not a marketing claim. Where a defense is partial or gray-zone, this document says so: honest measurement is the point.
- Methodology: STRIDE (per-category threat analysis) + OWASP LLM Top 10 (2025) mapping.
- Living scorecard: OWASP_LLM_COVERAGE.md, regenerated by
pytest tests/test_owasp_corpus.pyon every build.
1. Scope & trust boundaries
LLMProxy is an OpenAI-compatible gateway that sits between untrusted callers and one or more upstream LLM providers. It is itself a security control plane, so its own attack surface is in scope.
(untrusted) ┌─────────────── LLMProxy trust boundary ───────────────┐
client ─── HTTP ──▶ ASGI byte firewall ─▶ auth/RBAC ─▶ SecurityShield ─▶ plugin rings ─▶ forwarder ─── HTTPS ──▶ upstream LLM
(firewall_asgi.py) (rbac.py, (security.py) (plugin_engine) (forwarder.py) (provider)
identity.py)
│ │
threat ledger, response signer,
session memory audit hash-chain
(threat_ledger.py) (response_signer.py)Trust boundaries crossed: (a) network → firewall; (b) firewall → authenticated identity; (c) authenticated request → security inspection; (d) inspected request → upstream provider; (e) response → signed/audited before return. State stores (SQLite audit ledger, Redis rate-limit/circuit-breaker buckets) are inside the boundary. In scope: /v1/* inference, /api/v1/* admin, plugin loader, state stores. Out of scope: the upstream model's own behavior, the caller's host, and the build pipeline (covered separately by SBOM + provenance attestations in CI).
2. Defense in depth — the request pipeline
A request is inspected by independent layers; any one can reject it. This is deliberate: signature evasion at one layer is caught by scoring at the next.
| # | Layer | Module | What it stops |
|---|---|---|---|
| L0 | ASGI byte firewall | core/firewall_asgi.py | 180 attack signatures scanned across 8 decoding layers (URL, Unicode-escape, Base64, hex, ROT13) with iterative chain-decoding — catches encoded/obfuscated known payloads before any Python object is built. |
| L1 | Auth + RBAC | core/rbac.py, core/identity.py | API-key / OIDC-JWT (RS256) validation; per-role permission matrix; admin paths fail-closed. |
| L2 | Zero-trust identity | core/zero_trust.py | Verifies Tailscale device identity for network-level provenance. |
| L3 | Injection scoring | core/security.py, core/confidence.py | Regex threat patterns (incl. 7-locale multilingual) over both raw and homoglyph-normalized text → high-confidence hard block (≥ 0.85) or composite (regex + semantic-Jaccard + session-trajectory) → block / gray-zone escalate. |
| L4 | PII masking | core/security.py (mask_pii) | Reversible vault tokenization of emails, SSN, cards, IBAN, phones, API keys (regex + optional Presidio/ONNX NER). |
| L5 | Plugin rings | core/plugin_engine.py | Sandboxed pre/routing/post/background hooks; AST import scan, SHA-256 pin verification, path-traversal guard, allow-listed modules. |
| L6 | Trajectory + ledger | core/security.py, core/threat_ledger.py | Multi-turn "crescendo" detection per session; cross-session/IP threat accumulation. |
| L7 | Provenance + audit | core/response_signer.py, audit hash-chain | HMAC response signing; tamper-evident append-only audit ledger (/api/v1/audit/verify). |
3. STRIDE analysis
3.1 Spoofing (identity)
Threats: posing as a legitimate client or admin; forging upstream identity.
- Controls: API-key verification (
_verify_api_key) and OIDC/JWT with RS256 (identity.py); admin endpoints gated by_check_admin_auth(fail-closed when auth is enabled); Tailscale device verification (zero_trust.py) for network provenance; invalid-key attempts are logged asSECURITYevents to the live feed. - Residual: static API keys remain a supported (simpler) mode; operators who choose it inherit key-management responsibility. OIDC is the hardened path.
3.2 Tampering (data / instructions)
Threats: prompt injection (OWASP LLM01); corrupting ledger/config state.
- Controls: the L0/L3 layers above. Prompt-injection detection scans both raw and confusable-normalized text, so leetspeak, zero-width, Cyrillic-homoglyph, and native non-Latin scripts are all covered (see §4). Config edits from the UI are admin-only, validated (dry-run) before an atomic write, backed up, and audited (
proxy/routes/admin.py). Volatile state (rate limits, breakers) lives in Redis with Lua-atomic updates to remove race windows. - Honest note: static pattern-matching is necessary but not sufficient against a probabilistic model. It is one layer; the composite score + optional AI gray-zone escalation exist precisely because no regex set is complete.
3.3 Repudiation
Threats: a malicious action (e.g. budget drain) with no provable trail.
- Controls:
EventLoggerrecords SECURITY/SYSTEM events; the audit ledger is an append-only hash chain verifiable via/api/v1/audit/verify(a broken link is detectable);ResponseSignerHMAC-signs outgoing responses so a consumer can prove the payload transited the proxy unmodified.
3.4 Information disclosure
Threats: PII regurgitation; secret/stack-trace leakage.
- Controls: vault-based PII masking (L4);
Authorizationheaders scrubbed from all logs; the runtime config view (/api/v1/config/yaml) is secrets-redacted; FastAPI exception handlers return generic errors (no stack traces to clients); theServerbanner is rebranded (nouvicornfingerprint). - Compliance: GDPR DSAR export + Article-17 erasure (
/api/v1/gdpr/*).
3.5 Denial of service
Threats: payload flooding; wallet-exhaustion.
- Controls: Redis Lua-backed token buckets enforce per-IP/per-key rate limits at the ASGI edge (before the expensive pipeline); hard body-size + max-token caps; predictive FinOps budget gating returns HTTP 402 before a costly upstream call; ReDoS-resistant threat patterns (bounded quantifiers, no catastrophic backtracking — see
core/security.pycomment on_THREAT_PATTERNS).
3.6 Elevation of privilege
Threats: RCE via the plugin loader; path traversal on admin endpoints.
- Controls: the plugin loader (
plugin_engine.py) AST-scans source for forbidden imports (allow-listALLOWED_MODULES), verifies a SHA-256 pin when the manifest records one, and blocks path traversal by resolving + containing every entrypoint underplugins_dir. RBAC (rbac.py) constrains what an authenticated role may do. - Residual (declared): the Python plugin path is trusted-code execution by design. Untrusted third-party plugins should use the WASM runtime path; the AST scan is a lint, not a sandbox. This is an explicit, documented trust assumption — not a claimed guarantee.
4. OWASP LLM Top 10 (2025) coverage
Full scorecard: OWASP_LLM_COVERAGE.md (regenerated every build).
| Category | Coverage | Notes |
|---|---|---|
| LLM01 — Prompt Injection | 100 % (26/26) | Direct, encoded (base64/hex/zero-width), leetspeak, role-play, suffix, multilingual (it/de/fr/es/pt/zh/ru), jailbreak-framing, tool-call injection. |
| LLM02 — Sensitive Info | 100 % | Email · SSN · Visa · Amex · IBAN · phones · API keys. |
| LLM07 — System-Prompt Leakage | 100 % (6/6) | Direct + indirect + continuation-bait + translation-trick + meta-instruction + persona-rebase. |
| Benign false-positive rate | 6 % | 18 controls incl. roleplay/fiction/multilingual benigns; only meta-discussion of attacks trips, on purpose. |
LLM03/04/06/08/09/10 are out-of-scope for the proxy (build-time, training-time, caller-side, model-side) and are documented as N/A rather than silently claimed.
5. Measurement honesty (why this model is study-worthy)
The corpus harness (tests/test_owasp_corpus.py) evaluates each attack through the exact decision the live proxy makes in SecurityShield.inspect():
- firewall byte-signature scan, or
- high-confidence regex short-circuit (
threat_score ≥ _HARD_BLOCK_SCORE), or - composite
calculate_confidence"block".
An "escalate" verdict — which needs an upstream model to adjudicate — is not counted as a deterministic block. Attacks that only AI-escalation would catch (e.g. bare refusal-suppression, which overlaps benign pleas) are flagged expected_pass_known_gap and reported in the "known gaps" table, never folded into the headline number.
This matters because an earlier version of the harness measured a helper (_check_injections, block ≥ 0.7) that the runtime doesn't use — inflating LLM07 to a reported 100 % when the live composite let 4 of 6 extraction attacks through. Aligning the harness to the runtime exposed the gap; the fix (the hard-block short-circuit) closed it. A security number you can't reproduce against the running system is theater. This model's numbers are reproducible.
6. Residual risks & roadmap
- Probabilistic evasion: no static detector is complete against a model. Mitigation is layered (composite + AI gray-zone escalation) and measured, not assumed. Ongoing: expand the multilingual/technique corpus; local ONNX classifier in pre-flight (opt-in plugin already shipped).
- Plugin trust: Python plugins are trusted code; untrusted extensions belong in the WASM runtime. The AST scan is a lint, not isolation.
- Static API-key mode: supported for simplicity; OIDC is the hardened default for multi-user deployments.
- Gray-zone attacks: documented, not hidden. AI escalation (when an upstream judge is configured) raises real-world block rate above the deterministic floor.
Reproduce every claim in §4/§5: pytest tests/test_owasp_corpus.py -v.