Security & trust · TrustedRouter Field Manual
Week 1

Security & trust

Attestation, the trust boundary, privacy tiers, keys and BYOK.

Video 08 Security & trust · The trust boundary, verifying attestation, privacy tiers, keys and BYOK.

Research pack for the "0-to-CTO" onboarding. Scope: how TrustedRouter's privacy claim is built, what it does and does not prove, where every control lives in quill-router, and which test keeps it honest. Researched against quill-router@778120494 (2026-10-19) plus read-only gh api reads of Lore-Hex/quill-cloud-proxy. Paths are relative to quill-router/ unless prefixed quill-cloud-proxy:.


1. The one-paragraph model

TrustedRouter is split into two planes, and the split is the security architecture:

  • Attested gateway / prompt path (api.trustedrouter.com, alias api.quillrouter.com). A Go binary (quill-cloud-proxy:enclave-go/) runs inside GCP Confidential Space on Intel TDX. AWS Nitro Enclaves and Azure SEV-SNP Confidential Containers serve as additional planes. TLS terminates inside the workload. Prompts and outputs exist only here and at the upstream provider.
  • Control plane (trustedrouter.com, this repo, FastAPI on Cloud Run). It handles accounts, keys, billing, routing policy, catalog, the trust page and metadata. In production it never sees prompt or output content. It does not even register inference routes (src/trusted_router/main.py:880, :912).

The gateway calls the control plane's /internal/gateway/* routes (authorize, settle, refund) with an internal bearer token. Those calls carry only metadata: key hash, model, token counts and cost. Customers get evidence by fetching a Google-signed attestation JWT bound to their nonce and to the TLS session. They compare the container image_digest against the published release on trust.trustedrouter.com.


2. Honest scope (read this before you say anything to a customer)

From README.md:109-114, docs/security.md, quill-cloud-proxy:tools/verify-attestation.py, and docs/compliance/soc2/README.md:

What attestation does prove What it does not prove
The container serving this TLS session has image digest X, running on genuine TDX hardware with secure boot and debug disabled since boot (dbgstat: disabled-since-boot) That the open-source code is bug-free or free of a logging bug
The response is fresh, because your random nonce is in eat_nonce Anything about the upstream provider (OpenAI, Anthropic, …). Provider handling is policy or contract, cited per provider in the catalog. Only the confidential tier adds provider-side TEE + E2EE
The cert you are talking to is bound into the token: workload TLS cert + RFC 9266 tls-exporter hashes in eat_nonce Resistance to a nation-state with physical host access, or a break in Google, Intel or AMD roots of trust
Secret Manager / KMS access is gated on the image (change one line, get a new digest, and secret access fails) The control plane. It is not attested. It is trusted for billing, metadata and BYOK ciphertext storage only
On Azure, MAA re-attests the SNP report, so Microsoft is a trusted third party. Receipt keys from AWS and Azure are published verified=false (docs/client-receipts.md)

Compliance status (docs/compliance/soc2/README.md): SOC 2 Type I and II are not obtained. HIPAA, ISO 27001 and privacy certifications are not obtained. The DPA and BAA are drafts. The repo contains a readiness binder (docs/compliance/soc2/, docs/compliance/hipaa/), not a report. Lore Hex Corp has one person. .github/CODEOWNERS says so explicitly and leans on attested measurements as the control that doesn't depend on headcount.

Batch API is the documented exception to "no retention". It stores per-artifact AES-256-GCM ciphertext with Cloud KMS-wrapped DEKs and deletes after 30 days (docs/security.md "Prompt And Output Data"; quill-cloud-proxy/docs/design/batch-api.md). This guarantee is weaker than zero-retention: it depends on deployed KMS/IAM policy.


3. Trust-boundary diagram

flowchart LR
  subgraph Client["Customer side (untrusted to us, trusts nothing)"]
    C[App / SDK<br/>trusted-router-py, @lore-hex/trusted-router]
    V[verify-attestation.py<br/>or SDK verifier]
  end

  subgraph Edge["Edge"]
    CA[GCP External ALB + Cloud Armor<br/>infra/cloud_armor.tf<br/>host allowlist, throttles, L7 DDoS]
    L4[Regional L3/L4 LB<br/>TLS passthrough, DNS-only, no Cloudflare proxy]
  end

  subgraph TEE["ATTESTED BOUNDARY: Confidential Space (Intel TDX) / Nitro / SEV-SNP"]
    GW[enclave-go gateway<br/>TLS terminates here<br/>/attestation, /receipt-key<br/>byokcache: KMS unwrap, mem-only]
  end

  subgraph CP["CONTROL PLANE: NOT attested (this repo, Cloud Run)"]
    API[FastAPI trusted_router<br/>no /chat, /messages, /responses, /embeddings in prod]
    INT[/internal/gateway/authorize,settle,refund<br/>TR_INTERNAL_GATEWAY_TOKEN/]
    ST[(Spanner / Postgres<br/>salted key hashes, BYOK ciphertext,<br/>metadata-only generations)]
    SEN[Sentry, scrubbed<br/>control-plane only]
  end

  subgraph GCP["Google-managed"]
    KMS[Cloud KMS<br/>single BYOK envelope key]
    SM[Secret Manager<br/>operator provider keys<br/>access gated by image attestation]
    ISS[confidentialcomputing.googleapis.com<br/>attestation JWT issuer + JWKS]
  end

  subgraph Up["Upstream providers (policy/contract trust; TEE only on 'confidential' tier)"]
    P[OpenAI / Anthropic / Tinfoil / Phala / ...]
  end

  TP[trust.trustedrouter.com<br/>GitHub Pages, signed per-plane bundles<br/>image-digest-gcp.txt, gcp-release.json]

  C -- "prompt (TLS to enclave)" --> L4 --> GW
  V -- "GET /attestation?nonce=..." --> L4
  C -- "console, keys, billing" --> CA --> API
  GW -- "metadata only + internal token" --> INT
  INT --> ST
  API --> ST
  API -. "KMS Encrypt (wrap DEK)" .-> KMS
  GW -. "KMS Decrypt (unwrap DEK)" .-> KMS
  GW -. "attested access" .-> SM
  GW -- "attestation request" --> ISS
  GW -- "prompt" --> P
  V -- "compare image_digest" --> TP
  API --> SEN
Diagram source
flowchart LR
  subgraph Client["Customer side (untrusted to us, trusts nothing)"]
    C[App / SDK<br/>trusted-router-py, @lore-hex/trusted-router]
    V[verify-attestation.py<br/>or SDK verifier]
  end

  subgraph Edge["Edge"]
    CA[GCP External ALB + Cloud Armor<br/>infra/cloud_armor.tf<br/>host allowlist, throttles, L7 DDoS]
    L4[Regional L3/L4 LB<br/>TLS passthrough, DNS-only, no Cloudflare proxy]
  end

  subgraph TEE["ATTESTED BOUNDARY: Confidential Space (Intel TDX) / Nitro / SEV-SNP"]
    GW[enclave-go gateway<br/>TLS terminates here<br/>/attestation, /receipt-key<br/>byokcache: KMS unwrap, mem-only]
  end

  subgraph CP["CONTROL PLANE: NOT attested (this repo, Cloud Run)"]
    API[FastAPI trusted_router<br/>no /chat, /messages, /responses, /embeddings in prod]
    INT[/internal/gateway/authorize,settle,refund<br/>TR_INTERNAL_GATEWAY_TOKEN/]
    ST[(Spanner / Postgres<br/>salted key hashes, BYOK ciphertext,<br/>metadata-only generations)]
    SEN[Sentry, scrubbed<br/>control-plane only]
  end

  subgraph GCP["Google-managed"]
    KMS[Cloud KMS<br/>single BYOK envelope key]
    SM[Secret Manager<br/>operator provider keys<br/>access gated by image attestation]
    ISS[confidentialcomputing.googleapis.com<br/>attestation JWT issuer + JWKS]
  end

  subgraph Up["Upstream providers (policy/contract trust; TEE only on 'confidential' tier)"]
    P[OpenAI / Anthropic / Tinfoil / Phala / ...]
  end

  TP[trust.trustedrouter.com<br/>GitHub Pages, signed per-plane bundles<br/>image-digest-gcp.txt, gcp-release.json]

  C -- "prompt (TLS to enclave)" --> L4 --> GW
  V -- "GET /attestation?nonce=..." --> L4
  C -- "console, keys, billing" --> CA --> API
  GW -- "metadata only + internal token" --> INT
  INT --> ST
  API --> ST
  API -. "KMS Encrypt (wrap DEK)" .-> KMS
  GW -. "KMS Decrypt (unwrap DEK)" .-> KMS
  GW -. "attested access" .-> SM
  GW -- "attestation request" --> ISS
  GW -- "prompt" --> P
  V -- "compare image_digest" --> TP
  API --> SEN

The dashed/colored line that matters is TEE ↔ CP. Content crosses into the TEE and to providers, never into CP. CP crosses into the TEE only with ciphertext envelopes and policy.


4. Attestation: what a real response looks like

I fetched GET https://api.trustedrouter.com/attestation?nonce=<16 random bytes hex> on 2026-10-19. (Disclosure: called twice. The first output was truncated, so I re-fetched to save the token.) The body is a raw RS256 JWT, not JSON. Decoded claims, trimmed:

// header
{"alg":"RS256","kid":"1be6ff7287d640316ddc929e1a8b21fbca7e54e7","typ":"JWT"}
// payload
{
 "aud": "quill-cloud",
 "iss": "https://confidentialcomputing.googleapis.com",
 "sub": "https://www.googleapis.com/compute/v1/projects/quill-cloud-proxy/zones/us-central1-b/instances/quill-enclave-mig-us-j71s",
 "iat": 1792432251, "exp": 1792435851,
 "eat_nonce": [
  "680d068a…e63f",   // binding hash (workload TLS cert / tls-exporter, per gcp-attestation-issuance.md)
  "0a8633b2…ff00",   // binding hash
  "223d263e…5096",   // binding hash (differs per TLS session)
  "6794d51294742e4b2d85989c38d20dd6"   // <- MY nonce
 ],
 "hwmodel": "GCP_INTEL_TDX", "swname": "CONFIDENTIAL_SPACE", "swversion": ["260900"],
 "secboot": true, "dbgstat": "disabled-since-boot",
 "submods": {
  "confidential_space": {"support_attributes": ["LATEST","STABLE","USABLE"], "monitoring_enabled": {"memory": false}},
  "container": {
   "image_reference": "us-central1-docker.pkg.dev/quill-cloud-proxy/quill/enclave-multi:gcp-release-8cad0a1e",
   "image_digest":    "sha256:28ada804dd8befa51e0bc98732e4fd6f211b175ef15bfe08b74afe34ebbf6e9f",
   "image_id":        "sha256:9f076a33…9939",
   "restart_policy":  "Always",
   "env": { "QUILL_ANTHROPIC_SECRET": "trustedrouter-anthropic-api-key", "...": "131 entries; secret *names*, never values" }
  }
 }
}

Doc bug worth fixing: the README's "For the nerds" snippet (README.md:93-99) is wrong in three places. 1. It pipes to jq ., but the body is a JWT, so jq fails. 2. It refers to a top-level image_digest, but the claim lives at submods.container.image_digest. 3. It lists pcrs, but GCP Confidential Space tokens have no PCRs. PCR0 exists on the AWS Nitro COSE document.

The env block also tells you that the operator provider keys are Secret Manager references that the attested image resolves at boot.

4.1 Client verification sequence

sequenceDiagram
  autonumber
  participant C as Client / verifier
  participant GW as api.trustedrouter.com (enclave, TLS inside TEE)
  participant L as CS launcher (in TEE)
  participant G as Google CC issuer + JWKS
  participant T as trust.trustedrouter.com

  C->>C: nonce = 16+ random bytes (hex)
  C->>GW: TLS handshake (SNI api.trustedrouter.com)
  Note over C,GW: Same TLS session MUST carry evidence AND prompt
  C->>GW: GET /attestation?nonce=N
  GW->>L: request token with eat_nonce = [cert hash, tls-exporter hash, ..., N]
  L->>G: TDX quote + claims
  G-->>L: RS256 JWT (iss=confidentialcomputing.googleapis.com)
  L-->>GW: JWT
  GW-->>C: JWT (connection kept alive)
  C->>G: fetch JWKS, verify signature, iss, aud, exp/nbf
  C->>C: N ∈ eat_nonce (freshness, anti-replay)
  C->>C: own tls-exporter + peer cert hash ∈ eat_nonce (no relay / cert substitution)
  C->>C: dbgstat == disabled-since-boot, secboot == true, hwmodel/swname as expected
  C->>T: GET image-digest-gcp.txt / gcp-release.json (signed bundle)
  C->>C: submods.container.image_digest ∈ accepted set
  alt all checks pass
    C->>GW: POST /v1/chat/completions (same TLS session)
  else any check fails
    C->>C: refuse to send prompt (503 from issuer means retry fresh, never reuse)
  end
Diagram source
sequenceDiagram
  autonumber
  participant C as Client / verifier
  participant GW as api.trustedrouter.com (enclave, TLS inside TEE)
  participant L as CS launcher (in TEE)
  participant G as Google CC issuer + JWKS
  participant T as trust.trustedrouter.com

  C->>C: nonce = 16+ random bytes (hex)
  C->>GW: TLS handshake (SNI api.trustedrouter.com)
  Note over C,GW: Same TLS session MUST carry evidence AND prompt
  C->>GW: GET /attestation?nonce=N
  GW->>L: request token with eat_nonce = [cert hash, tls-exporter hash, ..., N]
  L->>G: TDX quote + claims
  G-->>L: RS256 JWT (iss=confidentialcomputing.googleapis.com)
  L-->>GW: JWT
  GW-->>C: JWT (connection kept alive)
  C->>G: fetch JWKS, verify signature, iss, aud, exp/nbf
  C->>C: N ∈ eat_nonce (freshness, anti-replay)
  C->>C: own tls-exporter + peer cert hash ∈ eat_nonce (no relay / cert substitution)
  C->>C: dbgstat == disabled-since-boot, secboot == true, hwmodel/swname as expected
  C->>T: GET image-digest-gcp.txt / gcp-release.json (signed bundle)
  C->>C: submods.container.image_digest ∈ accepted set
  alt all checks pass
    C->>GW: POST /v1/chat/completions (same TLS session)
  else any check fails
    C->>C: refuse to send prompt (503 from issuer means retry fresh, never reuse)
  end

Key subtleties, from quill-cloud-proxy:tools/verify-attestation.py and docs/gcp-attestation-issuance.md: - Exporter binding alone is insufficient. A relay could launder the client's exporter through the nonce channel. The enclave accepts only one caller nonce, so you must send your own random nonce and check the exporter. - A new TLS connection needs fresh evidence. No nonce-bound evidence is cached server-side. Issuance is bounded to 1 active mint, 4 waiters, a 5 s queue and a 30 s issuer timeout. Temporary failures return 503 and must not be bypassed.

4.2 Trust drift workflow

  • scripts/verify_trust_measurements.py compares every published measurement on the control plane's trust records against a live attestation from GCP api_base_url, AWS, and every Azure regions[].attestation_url. It exits non-zero on mismatch or on an un-contacted endpoint when run with --strict.
  • .github/workflows/trust-drift.yml runs it daily at 07:23 UTC and after every successful "Deploy TR control plane". It uses --strict and never gates PRs on prod state. Offline proofs live in tests/test_trust_measurement_drift.py (e.g. test_gcp_digest_drift_fails_and_prints_both_values, test_gcp_matches_the_accepted_set_during_a_roll).
  • Why it exists: the Quill trust bucket once published a PCR0 that matched no running enclave. A stale measurement teaches verifiers to ignore mismatches.
  • Publishing happens in quill-cloud-proxy. make gcp-release writes trust-page/image-digest-gcp.txt and gcp-release.json. Per-plane publish-trust-{gcp,aws,azure}.yml sign bundles. publish-trust-page.yml deploys GitHub Pages at trust.trustedrouter.com, with an S3 mirror at trust.quill.lorehex.co. Publishers refuse unsigned or mismatched copies (tools/check-trust-signatures.py, tools/check-trust-copies.py).
  • The control-plane side of the trust records is src/trusted_router/trust.py (gcp_release :69, aws_release :149, azure_release :268, trust_html :338).
  • Inconsistency: docs/security.md says trust.trustedrouter.com "should point at the control-plane/trust hosting", but the gateway README says it is GitHub Pages. Confirm before you document it externally.

5. Client receipts (signed per-request evidence)

docs/client-receipts.md, src/trusted_router/receipt_keys.py, src/trusted_router/routes/public.py:2511.

  • Each enclave generates an Ed25519 receipt signing key at boot. Receipts (compact JWS) go only to the requesting client and are never stored.
  • The enclave publishes the public key at /receipt-key, committed into its attestation:
  • commitment = sha256(domain || 0x00 || pubkey) (receipt_keys.py:99)
  • A scheduled collector (POST /v1/internal/gateway/receipt-keys/collect, internal token) connects to every gateway A-record by IP with correct SNI, then stores (kid, att_sha256) rows. Rows are append-only: a kid collision never overwrites key material. This was fixed after the 2026-09-10 version-retention defect.
  • Public discovery is at GET /.well-known/inference-receipt-keys or /trust/receipt-keys.json, paginated (≤250 versions, ≤1 MiB).
  • verified=true currently means the GCP CS JWT signature, issuer, validity, audience and non-debug state were checked (verify_gcp_attestation_chain, receipt_keys.py:437, dbgstat check at :497). AWS and Azure rows are verified=false until in-package chain verifiers exist. Clients must not treat verified=false as hardware-anchored.
  • Receipt JWS verification for clients is implemented in quill-cloud-proxy:tools/verify-attestation.py (verify_receipt_*, verify_receipt_stream).

6. Privacy tiers & fail-closed routing

Vocabulary (src/trusted_router/catalog_data.py:61-67):

Tier Int Meaning
standard / any 0 No tracked posture. Assume stored. This is the default for every provider until a cited flag says otherwise
no_store 1 Provider does not store request/response content
zdr 2 Contractual/policy zero data retention
confidential (aliases e2e, e2ee) 3 Verified confidential compute and provider E2EE and explicit ZDR and host is not the model's own vendor (currently Tinfoil, Phala)
  • Request surface: provider.min_privacy is parsed at src/trusted_router/routing.py:694-708. Unknown values return 400. Meta-models trustedrouter/zdr, trustedrouter/e2e and trustedrouter/eu set floors.
  • Enforcement is endpoint_meets_privacy_requirement (src/trusted_router/catalog_privacy.py:231). It is not a numeric comparison: retention and confidential compute are orthogonal, and unknown returns False (fail closed). Applied in _apply_provider_filters (routing.py:1121), which also notes "A TEE route with no tracked retention promise must not satisfy deny/ZDR".
  • Floors compose: the resolved floor dominates everything requested, regardless of order. If no endpoint qualifies, the request fails before dispatch rather than degrading.
  • Gateway replay of stored routes re-checks the floor (tests/test_gateway_replay_privacy.py).
  • The TR gateway hop itself is always attested. These tiers describe only the upstream provider (catalog_privacy.py module docstring).
  • Broadcast observability is metadata-only by default. Content export requires explicit include_content, and content-enabled encrypted destinations are returned only to the attested gateway (README.md "Broadcast Observability").

7. "Never logged": how prompt/output privacy is enforced in this repo

Defense in depth, from structural to cosmetic:

  1. Structural: the control plane cannot receive prompts in prod. register_inference_routes runs only when _control_plane_inference_enabled (main.py:880, :912-919), which returns true for environment in {local,test} only. An outage cannot silently fall back to a non-attested handler.
  2. Contract: gateway↔control-plane calls carry metadata only. Authorize, settle, refund, activity and generation metadata carry no content (docs/security.md "BYOK Secrets"). Generation rows are limited to id, workspace, key hash, model, provider, tokens, cost, usage type, speed, finish reason and status.
  3. No content-storage switch. content_storage_enabled: true is rejected with content_storage_disabled. GET /generation/content always returns content_not_stored (src/trusted_router/types.py:118).
  4. Sentry is control-plane-only and scrubbed (src/trusted_router/sentry_config.py). - send_default_pii=False, max_request_body_size="never" (:240-241). - before_send, before_send_log and before_breadcrumb (:301, :315, :326) recursively _scrub (:563). - SENSITIVE_KEYS (:21) covers authorization, cookie, api_key, byok, secret, token, password, messages, prompt, completion, content, output, input, body, and more. - Also scrubbed: string fragments/prefixes such as sk-tr-v1- (:47, :83). - A floodgate caps events at 3 per fingerprint and 50 per hour per process. - No Sentry in the enclave image.
  5. Logging hygiene tests (tests/test_application_logging.py, tests/test_scrub_totality_property.py). Hypothesis property tests check that no declared secret survives any container shape, scrubbing is idempotent, cycles terminate and the Axiom filter ships no canary.
  6. The gateway retention table (quill-cloud-proxy README "What gets retained"): no prompt, completion, bearer tokens or key hashes. Only per-device daily aggregate counts.

8. Credentials & secrets

8.1 API keys

src/trusted_router/security.py: - Key format: sk-tr-v1-<token_urlsafe(32)> (:8). Key ID: key_<token_urlsafe(18)> (:12). - Verifier: per-key random 32-byte salt, digest SHA-256(salt || key) (:16-21), compared constant-time (hmac.compare_digest, :28, :38). - Lookup index: lookup_hash_api_key = SHA-256(key) unsalted (:24). This is acceptable because keys carry ~256 bits of entropy (no dictionary attack). It gives O(1) lookup, and the salted digest remains the verifier. Sessions use the same lookup-hash scheme (storage_auth_sessions.py:48). - The public hash field is an opaque key ID, not the verifier (docs/security.md "API Keys"). Tested by tests/test_stubs_and_security.py:298 test_api_key_secrets_are_salted.

8.2 BYOK envelope encryption

src/trusted_router/byok_crypto.py, src/trusted_router/key_management.py, src/trusted_router/routes/byok.py.

  • encrypt_byok_secret (:52):
  • Generates a random 32-byte DEK and a 12-byte nonce.
  • Encrypts the provider key with AES-256-GCM(DEK).
  • Wraps the DEK through KeyWrapper (Cloud KMS Encrypt in prod) with a separate 12-byte nonce.
  • Stores only {algorithm, key_ref, encrypted_dek, dek_nonce, ciphertext, nonce} plus a first/last display hint.
  • AAD v2 (:285) is length-prefixed and injective: "trustedrouter/byok/v2" | namespace | workspace_id | context. It binds ciphertext to the workspace and provider. Three namespaces keep secret families from opening as one another:
  • provider: BYOK, opened only by the enclave
  • control: broadcast keys/headers, never shipped to the enclave
  • user_model: opened on both sides, deliberately
  • It must stay byte-identical to quill-cloud-proxy:enclave-go/internal/byokcache/cache.go aadV2. Both repos pin a hex vector (tests/test_byok_aad_namespace_property.py::test_the_v2_vector_matches_the_enclave).
  • V1 envelopes are rejected before any unwrap (:306-312).
  • Wrapper selection (key_management.py:180): an explicit local key, then TR_BYOK_KMS_KEY_NAME (GcpKmsKeyWrapper, :113), then an HKDF-derived dev key only in local/test, else raise. There is no silent dev-key fallback in prod.
  • One KMS key wraps every BYOK DEK. BYOK keys are rows, not one Secret Manager secret each.
  • byok_cache_key (:215) = "byokcache:v1:" + sha256(workspace, provider, all envelope fields). The gateway uses it for short-TTL, memory-only plaintext caching. Rotation produces a new key. Delete means authorize stops returning the envelope, and the cache entry expires by TTL.
  • Returned to the gateway in /internal/gateway/authorize (routes/internal/gateway.py:5528-5529).
sequenceDiagram
  autonumber
  participant U as User (console / mgmt API)
  participant CP as Control plane (byok.py → byok_crypto)
  participant K as Cloud KMS (single BYOK key)
  participant DB as Spanner (BYOK row)
  participant GW as Attested gateway (enclave-go byokcache)
  participant P as Provider

  rect rgba(120,120,255,0.12)
  Note over U,DB: Encrypt (one-time raw input)
  U->>CP: PUT /v1/byok/providers/{p} {api_key}
  CP->>CP: DEK=rand(32), nonce=rand(12), dek_nonce=rand(12)
  CP->>CP: AAD = len‖"trustedrouter/byok/v2"‖len‖"provider"‖len‖ws‖len‖p
  CP->>CP: ct = AES-256-GCM(DEK, nonce, key, AAD)
  CP->>K: Encrypt(DEK) - prod, LocalAes wrapper in local or test only
  K-->>CP: encrypted_dek
  CP->>DB: store {alg=V2, key_ref, encrypted_dek, dek_nonce, ct, nonce, hint}
  CP-->>U: hint (first/last chars) + metadata, never plaintext
  end

  rect rgba(120,255,120,0.12)
  Note over GW,P: Decrypt (inside TEE only)
  GW->>CP: POST /internal/gateway/authorize (internal token, key hash, model)
  CP->>DB: load envelope
  CP-->>GW: byok_encrypted_secret + byok_cache_key (non-secret)
  alt cache hit for byok_cache_key
    GW->>GW: use in-memory provider key
  else miss
    GW->>K: Decrypt(encrypted_dek) (IAM: attested workload identity)
    K-->>GW: DEK
    GW->>GW: key = AES-GCM-open(DEK, nonce, ct, AAD_v2) → cache (short TTL, memory only)
  end
  GW->>P: request with user's provider key
  Note over CP,DB: Delete BYOK → authorize stops returning envelope → fails closed
  end
Diagram source
sequenceDiagram
  autonumber
  participant U as User (console / mgmt API)
  participant CP as Control plane (byok.py → byok_crypto)
  participant K as Cloud KMS (single BYOK key)
  participant DB as Spanner (BYOK row)
  participant GW as Attested gateway (enclave-go byokcache)
  participant P as Provider

  rect rgba(120,120,255,0.12)
  Note over U,DB: Encrypt (one-time raw input)
  U->>CP: PUT /v1/byok/providers/{p} {api_key}
  CP->>CP: DEK=rand(32), nonce=rand(12), dek_nonce=rand(12)
  CP->>CP: AAD = len‖"trustedrouter/byok/v2"‖len‖"provider"‖len‖ws‖len‖p
  CP->>CP: ct = AES-256-GCM(DEK, nonce, key, AAD)
  CP->>K: Encrypt(DEK) - prod, LocalAes wrapper in local or test only
  K-->>CP: encrypted_dek
  CP->>DB: store {alg=V2, key_ref, encrypted_dek, dek_nonce, ct, nonce, hint}
  CP-->>U: hint (first/last chars) + metadata, never plaintext
  end

  rect rgba(120,255,120,0.12)
  Note over GW,P: Decrypt (inside TEE only)
  GW->>CP: POST /internal/gateway/authorize (internal token, key hash, model)
  CP->>DB: load envelope
  CP-->>GW: byok_encrypted_secret + byok_cache_key (non-secret)
  alt cache hit for byok_cache_key
    GW->>GW: use in-memory provider key
  else miss
    GW->>K: Decrypt(encrypted_dek) (IAM: attested workload identity)
    K-->>GW: DEK
    GW->>GW: key = AES-GCM-open(DEK, nonce, ct, AAD_v2) → cache (short TTL, memory only)
  end
  GW->>P: request with user's provider key
  Note over CP,DB: Delete BYOK → authorize stops returning envelope → fails closed
  end

Operators can prove the full contract with TR_BYOK_PROD_SMOKE=1 uv run python scripts/smoke_byok_prod.py. It is destructive and manual, for security releases only. It covers an isolated workspace, rotation, settlement, delete, and fail-closed afterwards.

8.3 Sessions, OAuth, Sign in with TrustedRouter

  • Principal resolution (src/trusted_router/auth.py:90) tries three sources in order: 1. x-trustedrouter-user dev header, rejected outside local/test (:185) 2. tr_session cookie 3. Bearer, dispatched strictly by prefix: sk-tr- means API key only, trsess- means session only, anything else returns 401. There is no cross-shape fallback.
  • Session cookie (auth.py:378): HttpOnly, SameSite=Lax, Secure in every deployed env. The companion tr_signed_in=1 hint cookie is deliberately non-HttpOnly and carries no auth power.
  • Login OAuth (Google/GitHub) is in src/trusted_router/oauth_provider.py. GitHub picks the primary verified email (:181). The OAuth state cookie is HttpOnly/Secure/Lax (tests/test_session_cookie_attributes.py:91).
  • TrustedRouter as OAuth 2.1 AS (docs/sign-in-with-trustedrouter.md, docs/security.md):
  • Redirect URIs are exact-match.
  • PKCE S256 is mandatory on /v1/oauth/token. The verifier is 43–128 unreserved chars.
  • Codes and consent are short-lived and single-use.
  • Consent is server-bound: approval posts only an opaque id plus a constant-time-compared CSRF token.
  • Scopes: inference profile balance:read activity:read.
  • The default app budget is $20/mo. A suggested budget is never auto-applied above $20.
  • The token endpoint has a 30/min per-IP bucket.
  • Nuance: the deprecated legacy /v1/auth/keys path still accepts plain PKCE (tests/test_oauth_key_delegation.py:189, tests/test_oauth_increment_d.py:196).
  • Internal tokens (src/trusted_router/routes/internal/_shared.py:30-67): path-scoped credential classes gateway, observer, operator and lightning, each with its own secret, compared in constant time.
  • Prod returns 403 if the token is unset.
  • Config refuses to start if a surface holds a secret it doesn't own (SERVICE_SURFACE_SECRET_OWNERS, config.py:247-257, check at :1981).
  • Federation directory, settlement and credit secrets are valid only on their exact routes.

9. Edge, WAF, rate limits, body limits

  • Cloud Armor (infra/cloud_armor.tf, owner-applied Terraform, sole writer). Rules:
  • 900: deny non-canonical Host → 403
  • 1000: /chat-proxy/ throttle 120/60 s
  • 1100: state-changing request throttle 300/60 s
  • 1150: throttle 12000/60 s
  • 1200: throttle 2400/60 s
  • L7 DDoS defense is enabled
  • scripts/deploy/_edge_security.sh reconciles non-Terraform policies. Inventory lives in docs/security/edge-surfaces.json. It has 7 surfaces across GCP, AWS and Azure, and live_state_verified: false. Several are p0_external or p0_integration. Treat the edge rollout as in progress.
  • AWS WAF / App Runner helpers are scripts/deploy/_aws_waf.sh and _aws_app_runner_security.sh (tests/test_ddos_edge_deploy.py).
  • api.trustedrouter.com must be DNS-only (no Cloudflare proxy) so TLS reaches the TEE.
  • App limiter (src/trusted_router/request_limits.py):
  • Process-local, never storage-backed.
  • Defaults to TR_RATE_LIMIT_CLIENT_IP_MODE=untrusted: one shared untrusted_lb bucket.
  • X-Forwarded-For, CF-Connecting-IP and X-TrustedRouter-User are ignored. edge_header mode is allowed only behind the GCP ALB, which overwrites X-TrustedRouter-Client-IP.
  • Body limits (src/trusted_router/request_body_limit.py:79): 4 MiB per request, 64 MiB in flight, 16 concurrent bodies, 30 s read deadline. CL+TE smuggling framing is rejected.
  • Headers (src/trusted_router/middleware.py:345): HSTS, X-Frame-Options: SAMEORIGIN, and a nonce-based CSP that is still report-only (tests/test_security_response_headers.py:61).

10. Supply chain & security workflows

Workflow What
.github/workflows/dependency-scan.yml OSV-Scanner. Full scan with SARIF to code-scanning on push/daily. PR mode diffs base vs head. Actions are pinned by SHA
.github/dependabot.yml Weekly updates for uv, npm, docker and github-actions
.github/workflows/trust-drift.yml Published measurement vs live attestation (§4.2)
.github/workflows/cloud-security-baseline.yml Daily read-only GCP CIS-style drift check (scripts/deploy/gcp_security_baseline_check.sh). Opens an issue on drift. The checkers never mutate (tests/test_cloud_security_baseline.py)
.github/workflows/check-repo-security-coverage.yml Weekly check of branch protection and security settings across public Lore-Hex repos (scripts/security/check_repo_security_coverage.py)
.github/workflows/typed-audit.yml, ci.yml ruff + mypy + pytest (coverage ≥70%) gate every change
SECURITY.md GitHub private vulnerability reporting. Ack within 72 h, assessment within 5 business days. No bounty. Safe harbour. Customer-content exposure is top severity and notification does not wait for RCA

11. License (BUSL-1.1) implications

LICENSE: Business Source License 1.1. Licensor is Lore Hex Corp. Additional Use Grant: None. Change License is Apache-2.0, four years after each version's publication. - Anyone may read, copy, modify, and make non-production use. Running it in production (e.g. a competing hosted router) requires a commercial license until the change date. - The security relevance is that "source-available, not OSI open source" is the honest phrasing. It still gives verifiers what they need: the ability to read and rebuild the attested gateway (quill-cloud-proxy is also BUSL-1.1) and compare digests. SDKs are Apache-2.0 / MIT. Marketing copy in competitor_comparisons.py already says this plainly. Keep it that way.


12. Security controls → implementation → enforcing test

# Control Implemented at Test that enforces it
1 No inference routes on prod control plane src/trusted_router/main.py:880, :912 tests/test_stubs_and_security.py:1101 test_production_control_plane_does_not_register_inference_routes; tests/test_contract_regressions.py:195
2 Prompt/output never in metadata store routes/internal/gateway.py (metadata-only contract) tests/test_stubs_and_security.py:1134 test_prompt_output_never_enter_metadata_store
3 Content storage cannot be enabled workspace PATCH handler, types.py:118 tests/test_stubs_and_security.py:55, :69
4 Sentry scrubbing (keys, fragments, prefixes, bodies) src/trusted_router/sentry_config.py:21,47,83,240-241,301-332,563 tests/test_security_contracts.py:255, :281; tests/test_stubs_and_security.py:1150; tests/test_scrub_totality_property.py
5 No Sentry in enclave policy (docs/security.md) tests/test_stubs_and_security.py:1181 (⚠ silently passes if sibling quill-cloud-proxy isn't checked out, likely a no-op in CI)
6 Prod config fails closed (internal token, Stripe secrets, Sentry DSN, storage) src/trusted_router/config.py:1443, :1950-1990 tests/test_stubs_and_security.py:1018, :1049, :1076; tests/test_security_contracts.py:50, :238
7 Surface-scoped secret ownership config.py:247-257, :1981 config surface tests in tests/test_security_contracts.py / tests/test_internal_surface_deploy.py
8 Salted SHA-256 API keys, constant-time verify src/trusted_router/security.py:16-38 tests/test_stubs_and_security.py:298 test_api_key_secrets_are_salted
9 Dev user header disabled in prod src/trusted_router/auth.py:185 tests/test_security_contracts.py:142; tests/test_stubs_and_security.py:539
10 Strict bearer-prefix dispatch auth.py:90-177 tests/test_auth_store_collapse.py (trsess-/sk-tr- paths)
11 Session cookie HttpOnly/Secure/Lax auth.py:378-410 tests/test_session_cookie_attributes.py:112, :177
12 Internal token constant-time, required in prod routes/internal/_shared.py:53-67 tests/test_security_contracts.py:50, :81; tests/test_contract_regressions.py:195
13 OAuth PKCE S256, exact redirect, single-use server-bound consent routes/oauth.py, oauth_app_policy.py tests/test_oauth_increment_d.py:48, :122; tests/test_oauth_key_delegation.py:176
14 BYOK envelope: KMS-wrapped DEK, no plaintext src/trusted_router/byok_crypto.py:52-97, key_management.py:113,180 tests/test_byok_crypto.py:39; tests/test_storage_gcp.py:221; tests/test_oauth_and_console.py:1410; tests/test_billing.py:855
15 BYOK AAD v2 injective + namespace separation + enclave parity byok_crypto.py:285-312 tests/test_byok_aad_namespace_property.py:198,219,271,289,348
16 No dev wrapping key outside local/test key_management.py:193-199 tests/test_byok_crypto.py; tests/test_byok_v1_removal_gate.py
17 Privacy floor fail-closed, order-independent catalog_privacy.py:231, routing.py:694-708,1108-1135 tests/test_routing_privacy_floor_property.py; tests/test_auto_route_privacy_floor.py:191,207,222; tests/test_gateway_replay_privacy.py; tests/test_catalog_privacy_coherence_property.py
18 Receipt-key log append-only, GCP chain verified receipt_keys.py:99,437,497 tests/test_receipt_key_log.py:65,322; tests/test_receipt_key_versions_migration.py
19 Published measurement == running enclave scripts/verify_trust_measurements.py, .github/workflows/trust-drift.yml tests/test_trust_measurement_drift.py:345,382,395,407
20 Synthetic-probe attestation evidence checks (nonce binding, COSE/JWT, cert binding, PCR0 pin) src/trusted_router/synthetic/probes.py (_attestation_evidence) tests/test_attestation_evidence.py; tests/test_attested_transport.py:84,126,135
21 Rate limiter ignores spoofable headers; local-only src/trusted_router/request_limits.py:31,89 tests/test_stubs_and_security.py:876-1018, :1193, :1253
22 Request body limits / smuggling src/trusted_router/request_body_limit.py:79 tests/test_api_edge_contracts.py, request-body tests
23 Security headers / CSP nonce src/trusted_router/middleware.py:345 tests/test_security_response_headers.py
24 Stripe webhook signature required Stripe webhook route tests/test_security_contracts.py:171,215,238
25 Cloud Armor / WAF configuration infra/cloud_armor.tf, scripts/deploy/_edge_security.sh, _aws_waf.sh tests/test_ddos_edge_deploy.py; tests/test_public_surface_edge.py; tests/test_internal_surface_edge.py
26 Cloud baseline checkers read-only scripts/deploy/*_security_baseline*.sh tests/test_cloud_security_baseline.py:50,90,96

13. Threat model

# Threat / actor Asset Mitigation Residual risk
T1 TR operator or insider wants to read prompts Prompt/output Prompts only inside TEE. Control plane has no inference routes. Image change means a new digest, which drift check and customer verifiers detect. Secrets gated on image Operator ships a malicious build and publishes its digest. Detection then relies on someone reading the source diff. Single-person org
T2 Cloud host admin / hypervisor Prompt in memory Intel TDX memory encryption, dbgstat disabled-since-boot, monitoring_enabled.memory=false Physical/side-channel attacks. Trust in Google, Intel and AMD roots
T3 Network MITM / malicious relay Prompt in transit TLS terminates in TEE. Cert hash + tls-exporter + caller nonce bound in eat_nonce. Same-session rule Clients that skip verification get only ordinary TLS
T4 Replay of old attestation Verifier trust Caller nonce, short exp, no evidence caching None significant if verifier checks nonce
T5 Stale or drifted published measurement Verifier trust trust-drift.yml daily + post-deploy, --strict. Signed per-plane bundles Up to ~24 h detection for enclave-side drift
T6 Control plane compromise Keys, BYOK, billing Salted key digests. BYOK ciphertext only (KMS unwrap needs attested identity). Secrets split per surface Attacker can mint keys, alter routing policy (e.g. privacy flags), or read metadata. Control plane is not attested
T7 Leaked BYOK DB rows User provider keys AES-256-GCM + KMS-wrapped DEK + AAD bound to workspace/provider/namespace KMS IAM misconfiguration. Single KMS key for all users
T8 Secrets leaking into logs/Sentry Keys, prompts Scrubbers, max_request_body_size=never, property tests, no Sentry in enclave New field names not in SENSITIVE_KEYS. Third-party library logging
T9 Upstream provider retains or trains on data Prompt Privacy tiers. Default "assume stored". zdr/confidential floors fail closed ZDR is contract/policy, unverifiable by TR. Catalog flags can be wrong
T10 Spoofed client IP / auth headers Rate limits, identity Untrusted mode, ignored XFF/CF headers, dev header off in prod Per-instance limits only. Fleet protection relies on Cloud Armor
T11 DoS / volumetric Availability Cloud Armor throttles + L7 DDoS, body limits, Cloud Run max caps Edge rollout incomplete on AWS/Azure (edge-surfaces.json)
T12 OAuth app abuse / code interception User credits PKCE S256, exact redirect, server-bound consent, $20 default budget, verified owner Legacy /v1/auth/keys still allows plain PKCE
T13 Gateway → control plane impersonation Billing ledger Internal token (constant-time), surface ownership, dedicated federation secrets Shared bearer secret (not mTLS/attestation-bound)
T14 Dependency / supply-chain compromise Everything OSV-Scanner, Dependabot, SHA-pinned actions, attested image A malicious dependency inside the attested image would be attested. Attestation proves identity, not benign behavior
T15 Batch API data at rest Batch prompts/results Per-artifact AES-GCM, KMS-wrapped, 30-day deletion Weaker than zero retention. Depends on KMS/IAM policy
T16 Azure MAA / non-GCP planes Verifier trust Pinned MAA issuer + HOST_DATA, receipts verified=false Microsoft is a trusted third party

14. "Never do this" list for contributors

  1. Never register inference routes on the production control plane, or add any non-attested fallback for /chat/completions, /messages, /responses or /embeddings. Don't touch _control_plane_inference_enabled.
  2. Never log, store, or send to Sentry/Axiom/analytics any prompt, message, completion, or output content. That includes debug logs, exception messages that embed request bodies, and "temporary" diagnostics. When you add a new sensitive field name, add it to SENSITIVE_KEYS / SENSITIVE_STRING_FRAGMENTS, not to a second list.
  3. Never add Sentry (or any third-party telemetry SDK) to quill-cloud-proxy/enclave-go or any attested image.
  4. Never add content fields to gateway↔control-plane contracts (authorize, settle, refund, activity, generation).
  5. Never store a raw BYOK/provider key, return it in an API response, or log it. Use encrypt_byok_secret. Never decrypt provider-namespace envelopes in the control plane.
  6. Never change _aad_v2 without changing enclave-go/internal/byokcache/cache.go in lockstep and updating both pinned vectors. A mismatch breaks every BYOK key silently.
  7. Never introduce a silent dev-key / memory-backend / missing-secret fallback for deployed environments. Production must fail closed at startup.
  8. Never compare secrets with ==. Use constant_time_equal / hmac.compare_digest.
  9. Never trust X-Forwarded-For, CF-Connecting-IP, X-TrustedRouter-User, or Host for identity or rate-limit keys. Never enable edge_header mode behind a front door that doesn't overwrite the header.
  10. Never compare privacy tiers numerically for enforcement. Use endpoint_meets_privacy_requirement. Never flip a provider's ZDR/E2EE/confidential flag without a cited source. Unknown means "assume stored".
  11. Never put api.trustedrouter.com behind Cloudflare proxy or any TLS-terminating front door.
  12. Never publish a measurement without the matching signed bundle, and never weaken trust-drift.yml --strict, issuer queue limits or verifier checks to make a red run green.
  13. Never reuse one internal secret across surfaces (gateway, observer, operator, federation, lightning), and never treat federation secrets as generic internal credentials.
  14. Never claim SOC 2, HIPAA or ISO certification, "open source" (it's BUSL-1.1 source-available), or that attestation covers upstream providers.
  15. Never run scripts/smoke_byok_prod.py casually. It is destructive and meant for security releases only. Never run unbounded "read-only" Spanner scans (see AGENTS.md).
  16. Never weaken a security test or conformance test to make a change pass.

15. Open issues / follow-ups spotted during research

  • README.md:93-99 attestation snippet is inaccurate: raw JWT, not JSON; image_digest is nested; GCP has no pcrs.
  • tests/test_stubs_and_security.py:1181 (test_no_sentry_in_enclave_code) returns early when the sibling repo is absent, so it probably enforces nothing in CI.
  • docs/security.md vs the quill-cloud-proxy README disagree on where trust.trustedrouter.com is hosted.
  • CSP is still report-only.
  • The edge-surface inventory is live_state_verified: false, with several P0 items pending.
  • The legacy /v1/auth/keys path still accepts plain PKCE.
  • A single KMS key wraps all BYOK DEKs, and the internal gateway token is a shared bearer secret rather than attestation-bound. Both are reasonable today and worth revisiting at scale.