0027. The threat model and the security-review discipline
Context
Vord is the security system. It holds the credentials, the signing keys, the sessions, and the directory, and its surfaces face the open internet and untrusted relying parties. Yet the security material so far is scattered and non-adversarial: the risk list (architecture §10) catalogs engineering risks; §8.4 states the security posture; SECURITY.md is the reporting policy. None of these is a threat model — a systematic enumeration of assets, trust boundaries, and adversaries, and of the attacks an identity provider must withstand, each mapped to its mitigation and made testable.
For a system whose entire purpose is security, the threat model is as load-bearing as the constitution, and the work is mostly collection, not invention: the mitigations are already decided across the ADRs (short-TTL + status list for token theft, ADR-0005; PKCE for code injection, ADR-0014; SAML confined to Modgud, ADR-0017; the four tenant boundaries, ADR-0012; KeyCustody for key compromise, ADR-0003/0023; the consistency token against the New-Enemy problem, ADR-0004/0026). What is missing is the frame that ties them together adversarially, and the discipline that keeps it current as surfaces are built.
Decision
-
Adopt a structured, living threat model, recorded in architecture §12: assets, trust boundaries, adversaries, a STRIDE pass per boundary, and an identity-specific attack catalog in which every entry maps to its mitigation and the ADR that owns it. It is edited continuously like the rest of the architecture overview, not written once. The frameworks are named, not ad hoc: STRIDE is the classification taxonomy (§12.4); OWASP ASVS v5.0 is the verification rubric the security review checks against — its authentication, session, access-control, and cryptography requirements map onto Vord's planes, and abuse-case tests derive from them (§13); and OWASP Threat Dragon (Apache-2.0, OWASP-governed — selected over the single-maintainer Threagile under the ADR-0008 governance test) is the threat-model-as-code tool, its model version-controlled and its checks run in CI, reserved until code begins (a generic tool earns its keep once there is a system to model; in the design phase the hand-curated §12 model plus ASVS suffice). The identity-specific catalog (§12.5) and the IETF OAuth 2.0 Security BCP (RFC 9700) remain essential — generic frameworks miss protocol attacks like SAML signature-wrapping and OIDC mix-up.
-
Trust boundaries are explicit, and the edge is least-trusted. A compromised edge (Vör) node has bounded blast radius: it holds only public key material, cached projections, and a subscribe-only revocation-stream credential (ADR-0035) — so it can deny service, serve stale revocation, or observe the revocation feed, but it cannot forge a token (no private keys), cannot publish or suppress revocations, and cannot read the directory. That is a deliberate security payoff of the constitution's edge/core split, to be stated alongside its scaling payoff — moving state off the edge also shrinks what an edge compromise yields. The revocation/projection transport (NATS and the projection-distribution pulls) is itself a named trust boundary — the highest-value spoof-or-suppress target inside the perimeter — authenticated under ADR-0035 and integrity-protected by projection signatures and freshness (ADR-0043).
-
Abuse-case tests are first-class — the security analog of the edge-independence property test (ADR-0010). The canonical attacks (algorithm-confusion /
alg:noneforgery, token replay, OAuth mix-up and code injection, SAML signature-wrapping and XXE, cross-tenant token presentation, the New-Enemy privilege escalation, …) are asserted to fail in the test suite, seed-reproducible, from the milestone where the surface exists. A defended attack that ever succeeds is a committed regression, exactly as a re-monolithization is under ADR-0010. -
Continuous security assurance — the model and the solution are kept current by named, recurring practices, not a one-time pass.
- Per-milestone review. Each milestone's definition of done (the implementation arc) includes a threat-model review of its new surface and the abuse-case tests for it — the security counterpart to the per-milestone conformance and DST gates.
- Cadenced + event-triggered review. The threat model is reviewed on a fixed
cadence — at least at the maintainers' annual "state of the solution"
(
GOVERNANCE.md) — and on every triggering event: a new component or protocol/flow, a new external dependency at a seam, a published advisory affecting a dependency, a security incident, or a major refactor. The cadence interval is a budget set during implementation; the triggers are fixed now. - Dependency & advisory hygiene. The
cargo-denyadvisory wall plus scheduled Dependabot updates (ADR-0008) surface a RUSTSEC advisory as a tracked change rather than a surprise CI failure; the permissive-only license rule stays a standing gate. - Continuous testing. Abuse-case tests run in CI; every parser of untrusted
input — the JOSE/token codec, the SAML/XML parser (ADR-0017), the SCIM JSON
surface (ADR-0018), the OIDC request parsers, and the admin API decoders
(ADR-0021) — is fuzzed (OSS-Fuzz, with a
cargo-fuzzscheduled-CI fallback if admission is declined — ADR-0028); and every fixed vulnerability lands a permanent regression test (the rule a DST seed-found bug already follows, ADR-0010). - Cryptographic agility. Keys rotate on a security-driven schedule (ADR-0023), the signing-suite is reviewed periodically (including a path to post-quantum algorithms), and the token-format version (ADR-0003) is the lever that makes a suite change deployable without breaking verifiers.
- External validation, open-source-native. Vord is an open-source project and
does not depend on commissioned audits or paid penetration tests. Independent
scrutiny comes from a coordinated vulnerability-disclosure program
(SECURITY.md) that welcomes researcher review — the OSS substitute for a hired
pentest; free best-in-class tooling — OSS-Fuzz (continuous fuzzing), GitHub
CodeQL and secret scanning, Dependabot, the OpenSSF Scorecard and Best
Practices badge, and
cargo-deny/ RUSTSEC (ADR-0008); the free OpenID conformance suite run in CI (ADR-0025); and assurance inherited from vetted upstream crypto libraries (rustls, RustCrypto, webauthn-rs) rather than bespoke crypto (ADR-0003, ADR-0006). A commissioned third-party audit is welcome if a sponsor or foundation ever funds one, but it is never a gate the project depends on. OIDC (and later FAPI / SAML) conformance is re-run, not certified once. - Standards-drift tracking. The standards catalogue (architecture §11) is reviewed as IETF drafts graduate — e.g. SD-JWT became RFC 9901 and FAPI 2.0 went Final — so pinned references and conformance targets do not silently age.
- Coordinated disclosure & incident response. Vulnerabilities are reported and handled privately (SECURITY.md); every incident produces a post-incident review that updates this model and adds a regression test — the loop that keeps the model honest.
Several of these (fuzzing, external pentest, SBOM / signed releases) attach when the code and the releases exist; the commitment is recorded now so they are scheduled, not discovered.
-
Privacy threat modeling is a reserved extension. A LINDDUN-style privacy pass over the directory / PII (ADR-0011) and CIAM consent (ADR-0012, ADR-0019) is flagged where it attaches but is not built into the v1 STRIDE pass; the directory's PII is carried as one asset class here, with the deeper privacy analysis reserved.
Consequences
- The security posture stops being scattered across §8.4, §10, and the ADRs and becomes one coherent, testable model — discoverable, reviewable, and gate-able.
- The edge-compromise blast-radius property gives the constitution a security argument, not only a scaling one: an attacker who owns an edge node owns no secrets.
- The DoS surface shifts onto the write path. Edge-statelessness defeats verification-flooding (verification scales with edge nodes, ADR-0010), so abuse and DoS pressure concentrates on the issuance / registration / grant write path (ADR-0012, ADR-0026) — the same path that is the scale ceiling — making per-tenant rate-limiting and write-path hardening doubly load-bearing.
- Abuse-case tests make "did we actually defend X?" a mechanical check, not a claim — the same move ADR-0010 made for edge-independence and ADR-0025 made for conformance.
- The model is v0 / evolving and hardens through the per-milestone reviews; it is not stamped "complete," because a threat model never is.
- Because the practices are cadenced and event-triggered rather than one-time, the model and the solution track the threat landscape — a new protocol, a published advisory, a graduated standard, or an incident each feeds back in. That feedback loop is what "stays secure" means for a living system, and it is why the discipline, not any single review, is the durable artifact.