Appearance
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 shape | Enforcing control | Seam | Posture in prod binary |
|---|---|---|---|
| Forged / replayed mesh event injected at a node | per-envelope signature + nonce + ReBAC relation + node-existence gate | internal/transport/http/v1/handlers/events_dispatch.go, internal/mesh/sse/publisher.go | Deferred → 501 fail-closed (F-1) |
Stolen / forged OIDC id_token (expired, wrong aud/iss) | standard-claim validation with bounded leeway | internal/identity/idp/oidc/adapter.go | Active, test-locked (F-3) |
| Secret exfiltration without an audit trail | fail-closed audit on every read + per-Node/per-Domain rate limit | internal/secrets/services/fetch_service.go, internal/secrets/services/ratelimit.go | Active, test-locked (F-2) |
| Privilege use without fresh strong auth (elevated actions) | step-up acr/amr class + auth_time freshness window | internal/access/services/issue.go | Active, test-locked (F-4) |
| Node-secret plaintext at rest | wrapped-only schema + compile-time column guard | internal/identity/nodes/repo/nsk_no_plaintext_assertion.go, internal/platform/db/migrations/0008_node_secret_keys.sql | Active |
| Audit-log tamper / unauthorised retention bypass | hash-chain verify + refusing-Down migration (0A000) | internal/audit/chain/verify.go, internal/platform/db/migrations/0011_audit_log.sql | Active |
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 intocmd/plexspherein 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 testTestGetNodeEvents_501WhenAnyOfFivePortsNil, which reds at least one row whenever fewer than five ports are wired. The problem code isproblemCodeSignedEventBusNotProvisioned, surfaced assigned_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 readsacr/amr/auth_timefrom the IdP-claims port at issuance time — none of them are columns onaccess_sessionor 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 theactrelation, 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, wrongacrclass, staleauth_time, the freshness boundary,amr-only, and the API-token fail-closed case — is locked at the service tier (TestIssue_StepUpininternal/access/services/issue_test.go) and at the integration tier (TestAccessIssue_StepUpintests/integration/access_stepup_test.go, which asserts the typed*access.StepUpErrorcarries the Domain's requiredacrvalues). 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-nodesecret:<id>#readReBAC 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 (
TestFetchNodeSecretSuccessininternal/transport/http/v1/secrets/handler_test.go, asserting theX-Plexsphere-Secret-KIDandCache-Control: no-storeheaders) and at the integration tier (TestSecretsAuditCoverage_GrantedAndDeniedThroughHTTPintests/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 (TestFetchNodeSecretPermissionDeniedand thedeniedsubtest ofTestSecretsAuditCoverage_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.