ADR 0031 — Platform Auth Posture: Defense-in-Depth Across Surface Classes
Status: Accepted
Date: 2026-04-30
Deciders: Kristerpher (product owner), software-architect
Scope: Every web surface in the Raxx platform
Canonical reference doc: docs/security/auth-posture.md
Supersedes: informal per-surface decisions in docs/security/web-surface-posture.md
Context: PR #633 review thread (comment 2026-04-30 UTC) — Kristerpher asked for a clear, documented direction on how the platform is secured. PR #633 implements Class 3 and transitional Class 2 MFA at CF Access.
Context
As of 2026-04-30, Raxx operates multiple surfaces for different audiences:
- Customer-facing apps where end users will eventually authenticate with passkeys (per ADR-0001).
- Operator-facing surfaces where Raxx staff access the console, secrets vault, and ticketing.
- Static internal surfaces (mockups, internal docs) with no application layer.
The authentication posture across these surfaces had grown incrementally without a unifying framework:
- CF Access MFA was absent from all three operator-gated apps until PR #633.
- The console had a passkey-in-progress story (epic #146) but no documented rule for when its CF Access could be relaxed.
- There was no decision framework for new surfaces: what CF Access policy applies, and why.
Kristerpher's directive from the PR #633 review: "Can we make sure we are clear on the direction on how our platform is secured?" This ADR captures that direction.
Decision
D1: Defense-in-depth is the governing principle
CF Access is for surface gating. App-level auth is for identity and capability. These are two distinct security layers, and both must be present on operator surfaces — even when their individual contributions overlap. The rationale: misconfiguration at one layer should not result in zero authentication.
D2: Four surface classes govern every surface
Every Raxx surface belongs to exactly one class. Class determines the CF Access policy template and the app-auth requirement.
| Class | Description | CF Access role | CF MFA |
|---|---|---|---|
| 1 | Customer-facing public | None | N/A |
| 2 | Operator, strong app auth | Presence-only allowlist | Transitional: strong MFA until passkey GA; then presence-only |
| 3 | Operator, unaudited app auth | Allowlist gate | Strong MFA always (non-relaxable without replacing the tool) |
| 4 | Static internal (no app layer) | Sole auth | Strong MFA always (non-negotiable) |
Classification is documented and updated when surfaces are added or postures change. It is never implicit.
D3: Strong CF Access MFA does not stack on top of app-level passkey GA
Once an operator surface reaches passkey GA (all operators enrolled, no non-passkey fallback, audit log complete), the CF Access MFA requirement is relaxed to presence-only. A phishing-resistant passkey already satisfies NIST SP 800-63B AAL2. Adding TOTP on top of that at the CF layer is friction without security gain.
This is not "less secure." It is the correct application of defense-in-depth: the strong factor is at the app layer (passkey, phishing-resistant, origin-scoped), and CF Access maintains the outer perimeter (identity allowlist, session management, origin guard).
D4: The graduation rule is explicit and documented
The transition from Class 2 transitional to Class 2 target is not a silent infrastructure change. It requires:
- App-level passkey auth is GA (no fallback path exists).
- App-level audit log captures every login and privileged action.
- A PR modifies the CF Access Terraform policy from
require: [auth_method: "mfa"]torequire: []. - This ADR (or
auth-posture.md) is updated to record the transition.
D5: Class 3 and Class 4 CF MFA is non-relaxable absent architectural change
Class 3 surfaces use third-party tools (Infisical, FreeScout) whose auth implementations are not audited at Raxx's standard. CF Access strong MFA compensates for this. The only path to relaxing Class 3 is replacing the surface with an internally-controlled, passkey-GA app — at which point it would graduate to Class 2.
Class 4 surfaces have no app layer at all. Removing CF Access would leave zero authentication. Strong MFA here is structural, not a policy choice.
D6: The decision matrix in auth-posture.md §6 is the canonical test for new surfaces
Any new surface must be classified before deployment. The flowchart in auth-posture.md §6 is the authoritative procedure. The question "given who can see this and what app auth exists, what's the right CF Access posture?" is answered by walking the matrix, not by individual judgment.
Consequences
Positive
- Every surface has an explicit, documented security posture. No surface is "we'll figure it out later."
- PR #633 is validated: it correctly applies strong MFA to Class 3 surfaces (vault, tickets) and to the transitional Class 2 surface (console) while passkey is in flight.
- The console graduation path is clear: once epic #146 closes, file the CF Access relaxation PR. No ambiguity.
- New surfaces have a decision framework. Classification is auditable.
- Double-prompting friction is designed out for the long term (Class 2 target = presence-only).
Negative / risks
- Graduation depends on epic #146 completing. Until console passkey auth is GA,
console.raxx.appretains CF Access TOTP — Kristerpher must TOTP-enroll and carry an authenticator app. This is the correct transitional state, not a regression. - Class 3 CF MFA is a permanent tax on using FreeScout and Infisical directly. The mitigation is that both tools are accessed infrequently enough that TOTP on top of email is acceptable. If operator access frequency increases, this is a signal to evaluate replacing the tool with an internally-controlled surface.
- Session lifetime targets (§9 of
auth-posture.md) are not yet enforced. Current CF Access session duration defaults to 24h across all gated apps. Hardening to per-class targets is a follow-up task.
Neutral
- This ADR does not change the WebAuthn invariant (ADR-0001), the no-stored-credentials invariant (ADR-0002), or the GDPR posture (ADR-0003). Those remain in force.
- CF Access is Cloudflare-hosted infrastructure. Its data processing terms apply to login event logs. This was already the case before this ADR.
Alternatives Considered
Alternative A: CF Access MFA on all operator surfaces, always, regardless of app auth
Rejected as the permanent target (though this is the correct transitional state for Class 2). Post-graduation, stacking TOTP on top of a phishing-resistant passkey satisfies NIST AAL2 twice simultaneously. The security marginal gain is zero; the UX friction is real. Defense-in-depth requires independent layers, not redundant layers of the same type.
The distinction: a passkey is phishing-resistant and origin-scoped; TOTP is phishable (attacker can relay a TOTP code in real time). Adding a weaker second factor on top of a stronger first factor does not improve the overall security posture. If anything, it trains operators to enter codes mechanically, which reduces their TOTP vigilance on surfaces where it matters.
Alternative B: Remove CF Access entirely from Class 2 (rely only on app passkey)
Rejected. Defense-in-depth requires the outer perimeter to remain even when it is relaxed to presence-only. CF Access provides: operator identity allowlist enforcement, session audit logging at the edge, origin guard integration, and the ability to lock a surface instantly (kill-switch) without a Raptor/Heroku deploy. These benefits are independent of MFA. Removing CF Access entirely would lose them.
Alternative C: One CF Access policy template for all operator surfaces (no classes)
Rejected. The "strong MFA always everywhere" version of this fails because it conflates surfaces where we control the app auth (and can graduate) with surfaces where we do not (and must compensate). It would permanently impose TOTP on the console even after passkey is GA, which is Alternative A above. The "presence-only everywhere" version fails because it relies on FreeScout and Infisical having equivalent auth quality to a Raxx-built passkey flow, which they do not.
Alternative D: Require hardware keys (YubiKey / Titan) for all Class 2+ surfaces now
Deferred, not rejected. Hardware key enforcement is the correct long-term posture for credential-bearing operator surfaces. The trigger is a regulatory event or the onboarding of contractors with vault/token-admin access. Requiring hardware keys today would block legitimate operators who rely on platform authenticators (Touch ID, Windows Hello) and is premature given the current operator count (1). This is recorded as Open Question #2 in auth-posture.md §10.
References
- docs/security/auth-posture.md — canonical posture doc
- [ADR-0001](https://internal-docs.raxx.app/architecture/adr/0001-webauthn-passkeys-only.html) — WebAuthn passkeys only
- [ADR-0002](https://internal-docs.raxx.app/architecture/adr/0002-no-stored-credentials.html) — No stored credentials
- [ADR-0003](https://internal-docs.raxx.app/architecture/adr/0003-gdpr-by-default.html) — GDPR by default
- Issue #110 — WebAuthn registration endpoint (app passkey track)
- Issue #568 — CF Access TOTP/passkey MFA (CF Access track)
- PR #633 — TOTP/passkey MFA on CF Access for console/tickets/vault
- Issue #534 — env-switcher design (single console, env dropdown)
- NIST SP 800-63B §5.1.7 — Phishing-Resistant Authenticators
- Cloudflare Zero Trust MFA requirements — https://developers.cloudflare.com/cloudflare-one/policies/access/mfa-requirements/