Raxx · internal docs

internal · gated ↑ index

Raxx Platform — Auth Posture (Single Source of Truth)

Status: Accepted
Date: 2026-04-30
Owner: software-architect
Related ADRs: 0031, 0032
Related design docs: docs/architecture/auth.md, docs/architecture/rbac-design.md, docs/security/web-surface-posture.md
Related issues/PRs: #110 (WebAuthn registration), #568 (CF Access MFA), PR #633 (TOTP/passkey MFA on CF Access), #534 (env-switcher design), #651 (support epic), #670 (multi-passkey enrollment + backup codes), #672 (this update)


1. Vision

Raxx authenticates users with a layered, defense-in-depth model. The governing principle:

The strongest factor lives at the layer closest to the user's actual capability. CF Access is for surface gating. App-level auth is for identity and capability.

Two corollaries follow from this:

The model is not static. Surfaces graduate between classes as app-level auth matures. The graduation rule is explicit and documented, not a silent drift.


2. Invariants (non-negotiable)

These constraints are set at the product level. Any design, PR, or surface addition must comply.

  1. No stored credentials. The system must not store anything that could replay an authentication: no passwords, no TOTP seeds, no recovery codes. WebAuthn public keys and credential IDs are not credentials in this sense — they cannot impersonate a user without the private key held by the authenticator.
  2. Passkeys / WebAuthn only at the app layer. No password flow. No SMS OTP. No email OTP as a login factor. Platform authenticators (Face ID, Touch ID, Windows Hello) and roaming authenticators (YubiKey, Titan) are the only accepted factors.
  3. Email is the single contact channel, and only after verification. Used for verification round-trips and recovery re-attestation, not as a login factor.
  4. GDPR by default: data subject rights (access, portability, erasure, rectification), defined retention, DPA-ready audit logging, breach-notification automation.
  5. Audit trail for every state change affecting money, permissions, or data access.
  6. Credentials into infra, not into code: API keys and secrets live in env / secret stores. Never in files that ship.

CF Access policies are infrastructure decisions and do not create exceptions to the above.


3. The Four Surface Classes

Class 1 — Customer-facing public

Intended audience: end customers. Traffic is expected to be high, anonymous or authenticated at the app layer.

Class 2 — Operator with strong app auth

Intended audience: Raxx operators (Kristerpher and any future staff). The app layer provides a known, audited, phishing-resistant auth factor (passkey). CF Access provides a defense-in-depth outer perimeter.

Class 3 — Operator with weak or varying app auth

Intended audience: Raxx operators, but via third-party tools (FreeScout, Infisical) whose auth implementation is not audited at our standard. We cannot guarantee these tools will always enforce our invariants.

Class 4 — Static internal (no app layer)

Intended audience: internal team (designers, engineers, contractors) viewing static HTML artifacts (mockups, internal docs). No app exists; CF Access is the sole authentication mechanism.


4. Surface Taxonomy

Current surfaces and their class assignments. Columns: surface | hostname | audience | app-level auth | CF Access role | CF Access MFA today | CF Access MFA target | rationale.

Surface Hostname Audience App auth CF Access role CF MFA today CF MFA target Class Rationale
Antlers app app.raxx.app Customers WebAuthn passkey (#110) None (public) N/A N/A 1 Customer funnel; app is the gate
Raxx brand / landing raxx.app Public None (marketing/redirect) None (public) N/A N/A 1 Public marketing surface
Marketing site getraxx.com Public None (marketing) None (public) N/A N/A 1 Public marketing site
Demo app demo.raxx.app Customers / prospects None or limited app auth CF Access (allowlist, pre-GA) Presence-only Presence-only or public on GA 1 Pre-GA gate; relax to public on GA
Customer docs docs.raxx.app Customers None (public docs) None (public) N/A N/A 1 Public documentation
Status page status.raxx.app Public None (read-only) None (public) N/A N/A 1 Public trust signal; must be reachable without auth
Raxx Console console.raxx.app Operators WebAuthn passkey (epic #146, in-flight) Allowlist gate Strong MFA (TOTP, PR #633) Presence-only once passkey GA 2 Transitional: strong CF MFA until app passkey GA; then relax
Infisical Vault vault.raxx.app Operators + machine identities Infisical-native (not audited) Allowlist gate Strong MFA (TOTP, PR #633) Strong MFA always 3 Holds all credentials; defense-in-depth non-negotiable
FreeScout Tickets tickets.raxx.app Operators FreeScout-native (not audited) Allowlist gate Strong MFA (TOTP, PR #633) Strong MFA always 3 FreeScout auth not audited at our standard
Internal docs internal-docs.raxx.app Internal team None (static) Sole auth Strong MFA Strong MFA always 4 No app layer; CF Access is the only gate
Mockups raxx-mockups.pages.dev Internal team None (static) Sole auth Strong MFA Strong MFA always 4 No app layer; CF Access is the only gate

Surfaces not yet built (planned):

Surface Hostname Anticipated class Notes
API (post-GA) api.raxx.app 1 Public API; app-layer auth (passkey-attested tokens) is the gate
API (pre-GA) api.raxx.app Pre-GA: allowlist CF gate, no app MFA needed since it's machine-to-machine Collapses to class 1 at GA

5. Graduation Rule (Class 3 → Class 2)

A surface moves from Class 3 to Class 2 — and CF Access MFA is relaxed to presence-only — when all four conditions are met:

  1. App-level passkey auth is GA. All operators who access the surface are enrolled. No fallback to email-OTP, password, or any non-passkey factor exists in the application code path.
  2. App-level audit log is complete. Every login event and every privileged action writes an audit row. The audit log is accessible to ops and captures enough detail for incident response (actor, action, timestamp, target).
  3. The transition is documented. A PR exists that modifies the CF Access policy, and an ADR entry (or amendment to this doc) records the decision. Silent policy changes are not permitted.
  4. CF Access policy is updated explicitly. The require: [auth_method: "swk"] or require: [auth_method: "mfa"] block is removed; the policy reverts to require: [] (presence-only, allowlist confirmed). The Terraform codification is updated in the same PR.

Current console status: console.raxx.app is in transition. PR #633 applied strong MFA at CF Access. Once epic #146 (console passkey auth GA) closes, the graduation PR can be filed.


6. Decision Matrix for New Surfaces

Use this flowchart when adding any new surface to the Raxx platform.

You are adding a new surface. Walk these questions in order:

1. WHO SEES IT?
   ├── Customers / general public
   │   └── Go to [A]
   ├── Operators (Raxx team) only
   │   └── Go to [B]
   └── Mixed (customers + operators at different paths)
       └── Split into two surfaces or apply the stricter class to the
           restricted sub-path (e.g. /admin/* gets CF Access, /* is public)

[A] CUSTOMER / PUBLIC SURFACE
   2. Does app-level auth exist and is it GA with passkeys?
      ├── Yes → Class 1. No CF Access gate. App passkey is the auth.
      └── No  → Pre-GA: add CF Access allowlist (presence-only, no MFA).
                Remove CF Access gate when app auth goes GA.

[B] OPERATOR SURFACE
   2. Is the app-level auth under Raxx control and auditable?
      ├── Yes, and passkey auth is GA → Class 2.
      │   CF Access: presence-only allowlist gate. No extra MFA.
      ├── Yes, but passkey not yet GA → Class 2 transitional.
      │   CF Access: strong MFA until passkey GA, then graduate.
      └── No (third-party tool, or auth not audited at our standard)
          └── Go to [C]

[C] THIRD-PARTY OR UNAUDITED APP AUTH
   3. Does the surface serve credential-bearing or privileged data?
      ├── Yes → Class 3. CF Access strong MFA always. Never relaxable
      │         without replacing the tool with an audited one.
      └── No  → Class 3. Same rule applies: we cannot verify the tool's
                auth, so CF Access carries the strong factor regardless.

[D] STATIC INTERNAL (no app layer at all)
   The surface is static HTML, documentation, or a read-only artifact.
   → Class 4. CF Access strong MFA is the sole auth. Non-negotiable.

Test case — social-media-team@raxx.app hypothetical:
If this is a Notion-style workspace or a dedicated surface for social media contractors (not Raxx employees, limited trust), the walk is: Audience = operators (contractors) → app auth = third-party tool (not audited) → Class 3. CF Access strong MFA. If it is a shared static brief page, Class 4.


7. What NOT to Layer

These anti-patterns create friction without security gain or create false coverage:

Anti-pattern Why it is wrong
CF Access strong MFA on top of app-level passkey GA The passkey already satisfies AAL2. Adding a TOTP prompt on top means the operator enters TOTP twice per session: once at CF, once conceptually at the app. No additional attacker surface is blocked. Relaxing to presence-only after graduation is correct.
Relying on CF Access alone for credential-holding surfaces CF Access is a surface gate, not an app auth. An attacker who compromises a CF Access session gets into the tool; the tool's own auth must also resist the attacker. Class 3 surfaces layer CF Access + tool auth precisely for this reason. Never deploy a new vault/secrets surface as Class 4.
Omitting CF Access entirely on operator surfaces even after app passkey is GA Defense-in-depth means CF Access stays as the outer perimeter, even when it is relaxed to presence-only. Removing CF Access entirely from an operator surface eliminates the layer that catches misconfiguration or breach in the app-layer auth itself.
Applying presence-only CF Access to a Class 3 (unaudited app auth) surface If you cannot audit the tool's auth, you cannot treat a presence check as sufficient. Class 3 always keeps strong MFA at CF Access.

8. Security Considerations

PII and data handling

The auth-posture doc itself does not store PII. The surfaces it governs do:

Audit trail

Event Where logged Retention
Operator CF Access login Cloudflare Zero Trust access logs Per CF account settings (30–180 days)
Operator console login (post #146) Console audit_log 2 years (DPA)
Privileged action in console Console audit_log 2 years
Customer passkey registration Raptor audit_log 2 years
Customer login Raptor audit_log 2 years
DSR export / erase Raptor audit_log 2 years

Secrets and rotation

Kill-switches

Breach notification

Any breach.* event written to the audit log triggers the GitHub Action runbook in docs/agents/security_response.md. GDPR Art. 33 (72-hour supervisory authority notification window) is tracked from the breach.* write timestamp. Runbook is authoritative; this doc does not duplicate it.


9. Session Lifetime Targets (per class)

Class Description Target session lifetime Rationale
1 Customer (app passkey) 12h rolling (per auth.md §6) Balances UX with security; step-up required for privileged actions
2 Operator with strong app auth ≤8h fixed Operator surfaces; shorter lifetime limits blast radius of session compromise
3 Operator via unaudited tool ≤4h (CF Access session) Shorter because app-layer revocation is not under our control
4 Static internal 24h (CF Access session) Read-only; longer session is acceptable friction tradeoff for internal team

These are targets. Current CF Access session duration defaults to 24h across all gated apps (see PR preview bot comment on PR #633). Hardening session lifetimes per class is a follow-up task.


10. Open Questions

These require Kristerpher's decision before downstream work can be finalized.

  1. SAML federation trigger. At what contractor count or contractor diversity does the org move from CF Zero Trust's built-in IdPs to a SAML provider (Okta, Google Workspace SSO)? This affects how CF Access MFA is enrolled for non-operator users. Proposal: trigger = first contractor who is not on moosequest.net or raxx.app email domain, or at 5+ distinct operator identities, whichever comes first.

  2. Hardware-key requirement. When does WebAuthn passkey enrollment become hardware-key-required (YubiKey / Titan) vs platform-allowed (Touch ID, Face ID, Windows Hello)? Proposal: hardware-required triggers on (a) a regulatory event (SEC/FINRA licensing), or (b) first contractor with vault or token-admin access. Until then, platform authenticators are accepted.

  3. Session lifetime enforcement. The targets in §9 are proposals. CF Access session durations are currently 24h for all gated apps. When do we harden these per-class? This is a Terraform change per app, low-risk, but needs a decision on the exact values before filing the sub-card.

  4. demo.raxx.app graduation. The demo surface is pre-GA gated by CF Access presence-only. When does it go public? Trigger should be: app auth is GA + demo is feature-flagged for a known subset of flows. Needs a product decision on what "demo" means at GA.

  5. ~~Tickets path (tickets.raxx.app vs support.raxx.app).~~ Resolved 2026-04-30 (#716). The CF Access app for tickets.raxx.app/admin was dead config — FreeScout does not expose an /admin path. The CF Access app (eafdf1f1-c2f1-4847-8777-c54acf22aca5) has been deleted. FreeScout's canonical login URL is tickets.raxx.app/login (built-in FreeScout auth, no CF Access gate). The surface taxonomy row for FreeScout Tickets in §4 reflects the correct state: Class 3, CF Access gate on the root domain, no /admin sub-path.


11. Customer Account Recovery Policy

Decision date: 2026-04-30 UTC
Decision owner: Kristerpher
Implementation card: #670
ADR: ADR-0032

Surface class

Customer accounts sit in Class 1 (customer-facing public, app is the auth layer). CF Access is not applied to the customer app surface. The account recovery policy applies to that class.

Policy: A+B only

Customer account recovery is limited to two mechanisms, both provisioned at signup:

A. Multi-passkey enrollment (non-skippable gate)

Signup requires at least two passkeys enrolled before the account is activated. The enrollment step cannot be skipped. If a customer has only one device available at signup, they can pause and resume — the account does not go live until a second passkey exists. This is enforced at the API layer, not just the UI.

Customers can add and revoke passkeys from the account dashboard at any time after signup. The enrolled-device list is always visible so customers know their recovery state.

B. Backup codes (non-skippable save gate)

Ten single-use backup codes are generated at signup. The signup flow does not complete until:

  1. The codes have been displayed for at least 30 seconds, and
  2. The customer has explicitly affirmed "I have saved my backup codes."

Using a backup code does not restore normal access directly. It opens a forced passkey re-enrollment flow; the customer must enroll a new passkey before the session can do anything else. Each backup code is single-use — it is burned in the audit log on consumption.

Customers can regenerate backup codes from the dashboard. Regeneration invalidates the entire previous batch immediately.

What is not built

There is no third recovery path. The following are explicitly not features of this platform:

These are not omissions to be filled later. They are deliberate non-features. ADR-0032 records the full rationale for each rejection.

What happens on total loss

If a customer loses all enrolled devices and all backup codes, the account is permanently inaccessible. Raxx cannot recover it. This is a deliberate posture: account-takeover defense takes priority over convenience for a financial-services platform. Permanent lockout is the cost of not building a phishable recovery surface.

Signup copy requirement: the customer-facing signup flow must state this in plain language before signup completes. Suggested copy: "Account recovery is not possible without your enrolled devices or backup codes — please save both before completing signup." The copy must not soften this to the point of misrepresenting the risk.

Support agent posture

Support agents must not promise or attempt recovery for a customer who has lost both factors. Doing so creates a social-engineering vector. The correct response is: "Account recovery is not possible without your enrolled devices or backup codes. This is a security decision that protects your account from takeover."

A full response template should land in docs/ops/runbooks/support/ as part of the support epic work (#651). That directory does not exist yet — it is not created here.

Audit log events

Every passkey and backup-code event is written to the audit log:

Action Event name
Customer adds a passkey customer.passkey.added
Customer revokes a passkey customer.passkey.revoked
Customer uses a backup code customer.backup_code.used
Customer regenerates backup codes customer.backup_codes.regenerated

Retention: 2 years (DPA requirement, consistent with §8 audit trail table).

GDPR note

Backup codes are short-lived, single-use values. They are not stored in recoverable form after generation (per ADR-0002). On a DSR erasure request, all backup code records (consumed and unconsumed) are deleted with the account. Passkey credential IDs and public keys follow the erasure rules in auth.md §4.


References