Skip to content

Security model and threat-model sign-off

This decision record captures the threat-model review and sign-off of the consolidated platform security model, and the residual risks that the review accepted. It reads alongside the consolidated Security model map, which inventories every enforcing control; this record states the verdict on that map, pins the one production deferral the review agreed to carry into production, and records the e2e-tier test-placement deferrals where a control is enforced and test-locked at tighter tiers but not additionally replayed over the wire.

Status

Accepted (draft). Audience: contributor. The review covered the platform-wide threat list below, confirmed the posture of each enforcing control in the production binary, and recorded one load-bearing production deferral — the deferred signed-SSE-bus production wiring — plus two e2e-tier test-placement deferrals (the step-up issuance denial and the secret-read granted-path replay), where the control is enforced and test-locked at the service and integration tiers but is not additionally replayed over the wire by the end-to-end threat-scenarios suite.

Threat model

The review aligns with the four attacker shapes the audit substrate defends against — a hostile DBA who rewrites a chain row, a panic dump that leaks audit-relevant memory, a backup-tamper substitution at restore time, and an archive-mirror divergence — documented in Threat model and boundaries. Beyond the audit-of-audit chain, the platform threat surface widens to mesh-event forgery, federated-identity token theft, secret exfiltration, privilege use without fresh strong auth, and node-secret plaintext at rest. Each threat shape maps to one enforcing control, the seam that enforces it, and the posture that control holds in the production binary today.

Threat shapeEnforcing controlSeamPosture in prod binary
Forged / replayed mesh event injected at a nodeper-envelope signature + nonce + ReBAC relation + node-existence gateinternal/transport/http/v1/handlers/events_dispatch.go, internal/mesh/sse/publisher.goDeferred → 501 fail-closed (F-1)
Stolen / forged OIDC id_token (expired, wrong aud/iss)standard-claim validation with bounded leewayinternal/identity/idp/oidc/adapter.goActive, test-locked (F-3)
Secret exfiltration without an audit trailfail-closed audit on every read + per-Node/per-Domain rate limitinternal/secrets/services/fetch_service.go, internal/secrets/services/ratelimit.goActive, test-locked (F-2)
Privilege use without fresh strong auth (elevated actions)step-up acr/amr class + auth_time freshness windowinternal/access/services/issue.goActive, test-locked (F-4)
Node-secret plaintext at restwrapped-only schema + compile-time column guardinternal/identity/nodes/repo/nsk_no_plaintext_assertion.go, internal/platform/db/migrations/0008_node_secret_keys.sqlActive
Audit-log tamper / unauthorised retention bypasshash-chain verify + refusing-Down migration (0A000)internal/audit/chain/verify.go, internal/platform/db/migrations/0011_audit_log.sqlActive

Defense-in-depth posture

The controls above sit inside a layered posture the platform readme describes; the review confirmed each layer holds end to end:

  • Transport. TLS protects every wire hop between the core binary, the nodes, and the backing stores.
  • Signed instructions. Ed25519 signatures cover every event so a rogue CA or a compromised proxy cannot forge instructions a node will accept — nodes only ever trust a signed envelope.
  • Replay resistance. Per-envelope nonce and issued-at staleness checks reject a captured-and-replayed event.
  • Trust-root isolation. Each Domain carries its own signing key, so a key compromise is contained to one tenant rather than the whole fleet.
  • Identity-federation boundary. The platform stores no passwords and runs no second-factor flow of its own; every credential and step-up assertion is federated through the Domain's IdP and consumed as standard OIDC claims.
  • Credential custody. Cloud credentials and platform secrets never land in PostgreSQL; the database holds only metadata and a reference to the secret backend.

Residual-risk acceptances

The review accepted one load-bearing deferral (the signed-SSE-bus production wiring) plus two e2e-tier test-placement deferrals, where the control is enforced and test-locked at the service and integration tiers but is not additionally replayed over the wire by the end-to-end threat-scenarios suite.

Signed-SSE-bus production wiring

  • Decision. The signed SSE event-bus verification path (SetSignatureVerifier / SetRelationChecker / SetNodeRepo) is intentionally NOT wired into cmd/plexsphere in this story. Wiring it is out of scope here, and this hardening-and-review story does not depend on the deferred signed-event-bus wiring story — the two stories are independent and may land in either order.
  • Posture. The endpoint is fail-closed. With the verification ports nil, the five-port dispatch gate returns 501 signed_event_bus_not_provisioned; the bus is never half-on (for example, a verifier present while the relation check is missing). The posture is locked by the regression test TestGetNodeEvents_501WhenAnyOfFivePortsNil, which reds at least one row whenever fewer than five ports are wired. The problem code is problemCodeSignedEventBusNotProvisioned, surfaced as signed_event_bus_not_provisioned.
  • Tracking and citation. The follow-up wiring is tracked in the Signed event bus roadmap, which enumerates each load-bearing leg the scaffolding leaves unfinished. This acceptance is the recorded, user-agreed residual risk; it lives here in the decision record so the deferral is reviewable as a deliberate trade-off, never buried as a bare inline code comment.
  • Publish-side note. The publish path is fail-closed independently of the read-side gate. The publisher resolves the per-Domain signing key, canonicalises the envelope, signs it, and only then writes the signed wire body to JetStream; it refuses to publish an unsigned envelope, so an unsigned event never reaches the bus even while the read-side verification ports remain nil.

Step-up issuance denial — e2e-tier replay deferral

  • Decision. The end-to-end threat-scenarios suite (tests/e2e/security/threat-scenarios/chainsaw-test.yaml, scenario 4) does NOT replay the step-up issuance denial over the wire. The step-up gate reads acr / amr / auth_time from the IdP-claims port at issuance time — none of them are columns on access_session or any other table — so there is no SQL- or curl-only fixture shape that exercises the negative branches. A wire-level replay would require standing up the full identity + ReBAC issuance stack (signer, SpiceDB authorizer admitting the act relation, policy reader, IdP-claims source) plus the per-Node bearer the issuer mints in process, which is out of this suite's scope.
  • Posture. The control fails closed and is enforced today. The issuance-denial behaviour — missing acr, wrong acr class, stale auth_time, the freshness boundary, amr-only, and the API-token fail-closed case — is locked at the service tier (TestIssue_StepUp in internal/access/services/issue_test.go) and at the integration tier (TestAccessIssue_StepUp in tests/integration/access_stepup_test.go, which asserts the typed *access.StepUpError carries the Domain's required acr values). The e2e suite records the control and cross-references those tiers so the coverage is accounted for, not silently absent.
  • Tracking and citation. This acceptance records the e2e-tier test-placement gap as a deliberate trade-off, reviewable here rather than buried only in the suite's inline comments. The corresponding ledger row is F-5 in the Security model map.

Secret-read granted-path replay — e2e-tier deferral

  • Decision. The end-to-end threat-scenarios suite (tests/e2e/security/threat-scenarios/chainsaw-test.yaml, scenario 3) asserts the wrapped-only storage invariant for secrets at the schema level (information_schema: no plaintext-shaped column), but does NOT replay the authenticated read over the wire — it does not assert the granted-path wrapped-ciphertext headers (X-Plexsphere-Secret-KID, Cache-Control: no-store) nor the cross-node secret:<id>#read ReBAC denial against a fully-wired stack. Reaching the authenticated read path needs the full secrets graph (OpenBao + SpiceDB + the fetch service) AND a per-Node NSK bearer the issuer mints in process, which a SQL/curl fixture cannot deterministically produce.
  • Posture. The granted-path header contract is positively pinned at the unit tier (TestFetchNodeSecretSuccess in internal/transport/http/v1/secrets/handler_test.go, asserting the X-Plexsphere-Secret-KID and Cache-Control: no-store headers) and at the integration tier (TestSecretsAuditCoverage_GrantedAndDeniedThroughHTTP in tests/integration/secrets_audit_coverage_test.go, which now asserts the same headers on the granted path against the real v1 router). The cross-node read denial is pinned at the same tiers (TestFetchNodeSecretPermissionDenied and the denied subtest of TestSecretsAuditCoverage_GrantedAndDeniedThroughHTTP). The wrapped-only-at-rest half of the control is the leg the e2e suite drives end to end.
  • Tracking and citation. This acceptance records the e2e-tier test-placement gap as a deliberate trade-off. The corresponding ledger row is F-6 in the Security model map.