Status: Design locked 2026-05-03
Owner: software-architect
Refs: Kristerpher directive 2026-05-03 ~08:00 UTC (DQ-4 resolution); epic #146 (console operator admin console)
Sibling docs: auth-unification.md, rbac-design.md
Related ADRs: 0020, 0025, 0042, 0043
Does NOT supersede: ADR-0042
ADR-0042 (auth-unification hybrid model) resolved the IDP question: Google Workspace is the IDP for all operator surfaces, bound via CF Access Zero Trust. It left one piece explicitly unresolved — DQ-4:
"Approve
ops,superadmin,developer,auditoras the four CF Access group names."
Kristerpher's 2026-05-03 ~08:00 UTC answer: "we should review the taxonomy and RBAC."
The prior proposal in auth-unification.md §5.3 defined four flat CF Access groups (ops, superadmin, developer, auditor) carrying short string claims. That flat taxonomy is incompatible with the fine-grained <app>-<resource>-<level> model specified in rbac-design.md and ADR-0020. This document resolves the incompatibility.
The reconciliation problem in one sentence: CF Access JWTs carry group claims at the network edge, but the <app>-<resource>-<level> role taxonomy is internal to each app — the question is how those two layers connect without duplicating policy state or tying Workspace group management to every app's release cycle.
The following constraints are non-negotiable and take precedence over every design choice in this document.
| # | Invariant |
|---|---|
| I1 | No stored credentials. CF Access JWTs are short-lived bearer tokens; they are not stored between requests. |
| I2 | No direct permissions on users. Permissions flow exclusively through: user → groups → roles → permissions (ADR-0020). A permission check against a bare user ID must not exist. |
| I3 | Superadmin is break-glass only. It is never a working group in day-to-day operations. |
| I4 | Every state-changing action that affects money, permissions, or data access writes an audit row. |
| I5 | CF Access remains the outer perimeter gate for all operator surfaces. App-layer authz is a second gate, not a replacement. |
| I6 | The <app>-<resource>-<level> naming convention from rbac-design.md §3.1 is the canonical role namespace. CF Access group names do not replicate this namespace. |
| I7 | Workspace admin controls CF Access group membership. Raxx role taxonomy is internal. The IdP never needs to know which specific permissions a Raxx role carries. |
| I8 | GDPR: group membership metadata is operational data, not PII. But the join between group membership and admin_id touches PII and must be retained/erasable per ADR-0003. |
The Console uses a flat four-level AdminRole model (superadmin, ops, support, readonly) resolved via console/app/middleware/rbac.py. Every blueprint calls @require_role(...) with one or more of these strings. No fine-grained <app>-<resource>-<level> roles exist in the running code today.
Current role gates per blueprint:
| Blueprint | Routes | Role requirement |
|---|---|---|
auth.py |
Login, passkey, TOTP flows | No role gate (pre-auth) |
dashboard.py |
GET /, tile toggle |
readonly or above; ops/superadmin for toggle |
api_status.py |
Status reads | readonly or above; /secrets endpoint superadmin only |
billing.py |
Billing index | support or above |
billing.py |
Billing mutations | @require_permission(...) — role-to-permission matrix in rbac.py (ops+superadmin; superadmin for unlimited) |
secrets.py |
All secrets routes | superadmin exclusively; rotation POST additionally requires @require_totp_elevation |
flags.py |
Flag reads | superadmin |
flags.py |
Flag promote/reject | superadmin + @require_totp_elevation |
deploys.py |
GET (history) | Any logged-in role |
deploys.py |
POST (trigger deploy) | ops or superadmin; prod target additionally inline TOTP elevation |
deploy_freeze.py |
Toggle freeze | superadmin |
security.py |
Security tab | ops or superadmin |
ops.py |
All ops dispatch routes | superadmin |
internal.py |
Internal status probe | superadmin (some read routes accept all roles) |
env_switch.py |
Environment switching | Gated by console-env-admin semantics (env_guard middleware) |
Key observation: The current code draws effectively three tiers: superadmin (all destructive/rotation ops), ops (operational actions), support/readonly (read-only). The BILLING_PERMISSION_ROLES matrix in rbac.py is the only existing hint of a per-resource permission concept.
console/app/models/admin.py defines:
- Admin — the operator identity record; has a roles relationship to AdminRole
- AdminRole — admin_id, role (CHECK IN superadmin,ops,support,readonly), granted_at, granted_by
- Admin.has_role(*roles) — intersection check
- Admin.primary_role() — returns highest-privilege role by _ROLE_HIERARCHY order
No groups, role_permissions, or role_inheritance tables exist yet. The full group/role/permission schema in rbac-design.md §7 is prospective.
NV10 (yaml-driven revocation auth gate) specifies a gate configurable by group, role, or single-user. The Velvet v2 design (v2-rotation-flows.md) uses operator_id in rotation_jobs as the identity anchor. No specific role strings are burned into the Velvet schema yet — operator_id maps to a console_admins.id value. The auth gate on Velvet stage endpoints (B10 in the v2 slate) is specified as "service-token auth on all stage endpoints; rotate/revoke permission scoping" — the permission names are not yet defined.
CF Access can consume Google Workspace identity in the following forms:
| Workspace construct | CF Access consumption | JWT claim produced |
|---|---|---|
Google account email (@raxx.app) |
Email domain restriction policy | email claim |
Workspace Group (group@raxx.app) |
CF Access Groups UI → "Google Workspace Group" selector | groups array (group email addresses or mapped short names) |
| Organizational Unit (OU) | CF Access can filter by OU path | Encoded in google sub-claim object |
| Custom attributes (Directory API) | Not directly consumed by CF Access without a SCIM bridge | Not in JWT by default |
| Security Group labels | Not distinct from regular Workspace groups in CF Access | Same groups claim |
Operational conclusion: CF Access can carry Workspace group membership directly in the JWT groups claim. The groups can be named arbitrarily (e.g., raxx-platform-admins@raxx.app). CF Access maps each Workspace group to a CF Access Group object; the groups JWT array contains those object names (short strings, not email addresses) — the exact string is configured in the Zero Trust Groups UI.
Three concrete integration options are evaluated. All three preserve the <app>-<resource>-<level> role naming as the internal canonical namespace.
What JWT claims carry: Only email and a minimal presence claim (is_raxx_operator: true). No group information.
Where authz decisions live: Entirely in each app's role table. The app reads email, looks up admin_id, resolves roles from admin_roles, resolves permissions from role_permissions via the DAG walk.
How groups are managed: All Raxx group management happens in the Console admin UI (the user_groups / group_roles tables from rbac-design.md §7). Workspace groups carry no authz meaning beyond "allowed past CF Access."
Blast radius of misconfiguration: CF misconfiguration lets an unauthorized Google account past the edge. App-layer still enforces role check. Net effect: unauthorized person hits login screen, cannot proceed without being in admin_roles.
Pros: Clean separation. App-layer is the single source of truth. No duplication between Workspace admin and Raxx role admin.
Cons: The CF Access JWT groups claim is useless. Adding a new operator requires two operations: (1) add them to a Workspace group so CF lets them through, (2) grant them roles in the Console. Onboarding friction is doubled. The "one login for everywhere" goal is not fully met — FreeScout and Vault still need app-level provisioning that cannot be driven from a CF JWT claim.
<app>-<resource>-<level> roles fullyWhat JWT claims carry: Full Raxx role names, e.g., groups: ["console-token-admin", "console-secrets-user", "vault-reader", "freescout-tickets-read"]. Every role that exists in rbac-design.md §4 is a Workspace group. Workspace admin is the authz source of truth.
Where authz decisions live: App-layer is enforcement only — it reads the JWT groups claim and checks "is console-token-admin in groups?" with no local DB lookup for role assignment.
How groups are managed: Workspace admin creates and manages ~20+ Workspace groups (one per fine-grained Raxx role). Adding a new operator requires adding them to each applicable Workspace group.
Blast radius of misconfiguration: Workspace admin error silently grants or revokes app-level permissions. A typo in a group name means the app never sees the role claim. Any new role addition requires a Workspace group creation before the app ships.
Pros: Single source of truth (Workspace). Audit trail is in Workspace admin logs. App-layer is pure enforcement; no DB writes for role assignment.
Cons: Ties Workspace group management to every app's release cycle. ~20+ Workspace groups is operationally heavy for a one-operator team. A role renamed in the app must be renamed in Workspace simultaneously or authorization breaks. SAML readiness is lost — the external IdP would need to know Raxx's full internal role taxonomy. Violates I7 (the IdP must not know Raxx's permission semantics).
Verdict: Rejected. Violates I7 and creates unacceptable coupling.
What JWT claims carry: Workspace groups map to CF Access Groups that encode operator function (not fine-grained permissions). Group names follow <team>-<function> convention, e.g., raxx-platform-admin, raxx-support, raxx-devops, raxx-break-glass. These are the CF Access group names. The JWT groups claim carries 1–2 of these short strings.
Where authz decisions live: Two layers, each with a distinct job:
CF Access (edge): "Is this person a Raxx operator at all, and which broad function do they serve?" Uses the CF Access groups to gate surface access. A raxx-support member cannot reach the vault surface; a raxx-platform-admin can reach all surfaces.
Console app-layer (authz): "What specific permissions does this person hold?" Reads the CF JWT email claim, maps to admin_id, resolves roles via the full user_groups → group_roles → role_permissions → role_inheritance DAG from rbac-design.md §7. This is the authoritative decision.
How groups are managed:
raxx-platform-admins@raxx.app) by Workspace admin. CF Access automatically grants them past the edge.admin_id (created at invite time) is assigned to one or more Raxx internal groups (e.g., raxx-platform-admins) in the Console DB. The Console console-invite-admin role governs who may do this.How the JWT groups claim is used at the app layer:
The app-layer reads the CF JWT groups claim for one purpose only: initial routing and surface filtering. Specifically:
raxx-break-glass in groups triggers an extra audit alert before the passkey step even loads.raxx-support in groups shows a scoped subset of the console UI before role resolution completes (performance optimization; not a security gate).user_groups → role_permissions DAG walk, never the JWT groups claim alone.Blast radius of misconfiguration:
Pros: Clear separation of concerns. Workspace groups are small in number (4–6). App-layer retains the full <app>-<resource>-<level> taxonomy without coupling to Workspace. SAML-ready (ADR-0020): IdP emits group names like raxx-platform-admins; app maps group name → Raxx internal group → roles. FreeScout and vault are gated at CF by the same Workspace group; app-layer provisioning (FreeScout user creation, Infisical org member add) is a separate onboarding step but only needs to happen once.
Cons: Two provisioning steps per new operator (Workspace group add + Console DB role assignment). This is acceptable and explicit — it is deliberately not auto-provisioned (invariant D5 from ADR-0042: no auto-provisioning via Google OAuth).
The four flat groups from the rejected proposal (ops, superadmin, developer, auditor) are replaced with team-function groups. These are CF Access object names — short strings that appear in the JWT groups claim. Each maps to one or more Workspace groups.
| CF Access group name | Workspace group | Operator function | Surfaces allowed |
|---|---|---|---|
raxx-platform-admins |
raxx-platform-admins@raxx.app |
Platform operation and configuration. Day-to-day admin. | Console, FreeScout, Vault, Internal docs |
raxx-support |
raxx-support@raxx.app |
Customer support. Read access to Console and tickets. | Console (read-only surfaces), FreeScout |
raxx-devops |
raxx-devops@raxx.app |
Infrastructure, deploys, flag management. Overlaps with platform-admins at small team size. | Console, Internal docs |
raxx-break-glass |
raxx-break-glass@raxx.app |
Emergency access only. Single member (Kristerpher). Triggers mandatory audit alert. | All operator surfaces |
Notes:
- At current team size (single operator), Kristerpher will be in raxx-platform-admins and raxx-break-glass. raxx-support and raxx-devops are empty groups created to reserve the namespace for future hires.
- A member of raxx-devops is not automatically a member of raxx-platform-admins. Vault and FreeScout are restricted to raxx-platform-admins and raxx-break-glass only (higher-trust surfaces).
- Group names use hyphens, not underscores, to match the Workspace group email convention.
These replace the cf-ops / cf-superadmin / cf-developer / cf-auditor group references in the prior doc.
# Console — Class 2
policy:
name: "console-operator"
decision: allow
include:
- group: raxx-platform-admins
- group: raxx-devops
- group: raxx-support
- group: raxx-break-glass
# FreeScout (tickets) — Class 3
policy:
name: "tickets-operator"
decision: allow
include:
- group: raxx-platform-admins
- group: raxx-support
- group: raxx-break-glass
require:
- auth_method: mfa # non-relaxable per ADR-0031
# Vault (Infisical) — Class 3
policy:
name: "vault-operator"
decision: allow
include:
- group: raxx-platform-admins
- group: raxx-break-glass
require:
- auth_method: mfa # non-relaxable per ADR-0031
# Internal docs — Class 4
policy:
name: "internal-docs-operator"
decision: allow
include:
- group: raxx-platform-admins
- group: raxx-devops
- group: raxx-support
- group: raxx-break-glass
require:
- auth_method: mfa # non-relaxable (no app layer)
# Velvet admin UI — Class 2 (Phase 2)
policy:
name: "velvet-operator"
decision: allow
include:
- group: raxx-platform-admins
- group: raxx-break-glass
console.raxx.app)| Workspace group | CF Access group | App-layer Raxx group | Raxx roles |
|---|---|---|---|
raxx-platform-admins@raxx.app |
raxx-platform-admins |
raxx-platform-admins |
console-token-admin, console-secrets-admin, console-manager, console-audit-user, vault-admin, raptor-admin |
raxx-support@raxx.app |
raxx-support |
raxx-support-team |
console-user, console-audit-user, antlers-support-readonly, raptor-read |
raxx-devops@raxx.app |
raxx-devops |
raxx-devops-team |
console-user, console-flag-admin, console-env-admin, console-audit-user |
raxx-break-glass@raxx.app |
raxx-break-glass |
break-glass |
All roles; session capped 2h; mandatory audit alert |
<app>-<resource>-<level> permissions within Console:
| Raxx role | Key permissions granted |
|---|---|
console-token-user |
console:tokens:read |
console-token-admin |
console:tokens:read, console:tokens:rotate, console:tokens:delete |
console-secrets-user |
console:secrets:read |
console-secrets-admin |
console:secrets:read, console:secrets:write, console:secrets:rotate |
console-audit-user |
console:audit:read |
console-flag-admin |
console:flags:read, console:flags:write |
console-env-admin |
console:env:switch |
console-invite-admin |
console:admins:invite, console:groups:write |
console-user |
console:dashboard:read |
console-manager |
Composition: console-user + console-flag-admin + console-env-admin + console-invite-admin |
console-ops |
Composition: console-user + console-audit-user |
Migration from current flat roles to new groups:
Current admin_roles.role |
Target Raxx internal group |
|---|---|
superadmin |
break-glass (working day-to-day ops move to raxx-platform-admins) |
ops |
raxx-platform-admins |
support |
raxx-support-team |
readonly |
raxx-support-team (read-only) OR raxx-devops-team (depends on function) |
tickets.raxx.app)FreeScout has its own user table; there is no Raxx <app>-<resource>-<level> layer inside FreeScout. CF Access is the outer gate; FreeScout's own OAuth module (Google OAuth) is the app-layer identity.
| Workspace group | Access granted | FreeScout role |
|---|---|---|
raxx-platform-admins |
Full FreeScout access | FreeScout admin |
raxx-support |
FreeScout access | FreeScout agent |
raxx-break-glass |
Full FreeScout access | FreeScout admin |
FreeScout role assignment (admin vs agent) is managed within FreeScout itself after the operator's Google OAuth login. This is not driven by the JWT groups claim.
vault.raxx.app — Infisical)Infisical uses its own native RBAC (org member → project role). Raxx maps local roles to Infisical scopes via a machine identity managed by Velvet (see rbac-design.md §3.3).
| Workspace group | CF Access group | Infisical access |
|---|---|---|
raxx-platform-admins |
raxx-platform-admins |
Org admin → all project scopes |
raxx-break-glass |
raxx-break-glass |
Org admin → all project scopes |
No raxx-support or raxx-devops access to the vault. Vault is restricted to the highest-trust group only.
The Velvet admin UI is unbuilt. It ships with Google OIDC from day one (per auth-unification.md §6.3). App-level RBAC within Velvet uses the same <app>-<resource>-<level> pattern:
| Raxx role | Velvet permissions |
|---|---|
velvet-rotation-trigger |
velvet:rotations:trigger |
velvet-rotation-read |
velvet:rotations:read |
velvet-revocation-execute |
velvet:revocations:execute |
velvet-admin |
Composition: velvet-rotation-trigger + velvet-rotation-read + velvet-revocation-execute |
JWT groups claim usage in Velvet: Velvet reads the CF JWT groups claim to bootstrap the initial permission check before the Console DB lookup completes. If raxx-platform-admins is in the JWT groups, the session is allowed to proceed to app-layer role resolution. This is a guard against routing errors, not a substitute for the role check.
sequenceDiagram
actor K as Kristerpher (admin)
participant WS as Google Workspace Admin
participant CF as CF Zero Trust (Groups)
participant C as Console (invite + RBAC)
participant FS as FreeScout admin
participant IN as Infisical org
K->>WS: Add new operator to raxx-platform-admins@raxx.app
WS-->>CF: Group sync (next poll or SCIM push)
Note over CF: Operator can now pass CF Access edge gate
K->>C: console-invite-admin: send invite to operator email
C-->>Op: Magic-link invite email (no credentials in email)
Op->>C: Follow link → register passkey (WebAuthn)
C->>C: Create admin row + assign to raxx-platform-admins group (DB)
K->>C: Verify group assignment in Console RBAC UI
K->>FS: Add FreeScout user (email → agent or admin role)
K->>IN: Add Infisical org member (if vault access required)
Note over C,IN: Operator now has: CF edge access, Console roles, FreeScout identity, optional vault access
NV10 specifies a yaml-driven revocation auth gate with vocabulary group, role, or single-user. Under the recommended option:
group maps to Raxx internal group names (e.g., raxx-platform-admins), resolved via the user_groups → group_roles DAG. This is fully compatible.role maps to Raxx role names (e.g., velvet-revocation-execute), resolved via role_permissions. This is fully compatible.single-user maps to an admin_id UUID. This is an exception to invariant I2 ("no direct permissions on users") — its use must be documented in the audit log and is only permitted for emergency single-user gates, not for routine access patterns.The NV10 yaml gate should reference Raxx internal group names and role names only. It must not reference CF Access group names or Workspace group emails — those are network-edge constructs, not app-layer identifiers.
Proposed NV10 gate config vocabulary:
# Velvet rotation auth gate (NV10)
rotation_auth:
trigger_rotation:
require_any:
- group: raxx-platform-admins
- role: velvet-rotation-trigger
execute_revocation:
require_any:
- group: raxx-platform-admins
- role: velvet-revocation-execute
force_revoke_override:
require_all:
- group: raxx-platform-admins
step_up: totp # TOTP elevation retained for force-revoke only
break_glass_override:
single_user: <admin_id-kristerpher> # Emergency only; generates mandatory Slack alert
step_up: passkey_reauth
No vocabulary changes are needed — the group/role/single-user taxonomy of NV10 maps directly onto the Raxx internal RBAC model.
The migration must not disrupt the existing Console auth flow at any step. Each phase is independently releasable and independently rollback-able.
Dependency: None. Can ship before CF Access IDP swap.
Add the RBAC tables from rbac-design.md §7:
0NNN_rbac_tables.sql — groups, roles, permissions, role_permissions,
role_inheritance, group_roles, user_groups
0NNN_rbac_seed_roles.py — seed all <app>-<resource>-<level> roles
0NNN_rbac_seed_groups.py — seed raxx-platform-admins, raxx-support-team,
raxx-devops-team, break-glass groups
0NNN_rbac_migrate_admin_roles.py — for each admin_roles row, insert user_groups
membership per the mapping table in §7.1
The existing admin_roles table is NOT removed yet. Both systems coexist. The rbac.py middleware continues using admin_roles. Feature flag RBAC_V2=1 switches to the new path in the Console.
Rollback: drop the new RBAC tables. No impact on existing auth.
Dependency: Phase 0 complete; DQ-4 resolved (this doc resolves it).
raxx-platform-admins, raxx-support, raxx-devops, raxx-break-glass.Rollback: re-enable one-time-pin IDP in CF Zero Trust (CF supports multiple IDPs simultaneously).
groups claimDependency: Phase 1 complete.
The Console (and Velvet when it ships) reads the CF JWT groups claim for two purposes:
1. raxx-break-glass in groups → emit Slack alert before passkey step.
2. Surface-filtering optimization (show reduced UI until role resolution completes).
The authoritative permission check remains the admin_roles → role hierarchy check (still the v1 path under RBAC_V2=0).
Rollback: remove JWT groups claim consumers. Auth behavior unchanged.
Dependency: Phase 2 complete; RBAC tables seeded; shadow-testing in staging confirms parity.
RBAC_V2=1 on staging: rbac.py resolves permissions via user_groups → group_roles → role_permissions → role_inheritance DAG.RBAC_V2=1 on prod after 72h staging soak.Rollback: RBAC_V2=0 — reverts to admin_roles string comparison. New RBAC tables retained but not read.
admin_roles columnDependency: Phase 3 complete; 30-day prod soak with RBAC_V2=1.
AdminRole model and admin_roles table.has_role() and primary_role() methods from Admin model (replaced by effective_permissions() method backed by DAG walk).@require_role(...) decorator; all routes use @require_permission(...) with fine-grained permission names.Rollback: restore admin_roles table from backup. This is the only destructive step — it requires a full database backup pre-migration.
Dependency: Velvet v2 scaffold (B1); Phase 0 RBAC tables.
Velvet B10 (auth middleware) is implemented against the velvet-rotation-trigger, velvet-rotation-read, velvet-revocation-execute role names from §7.4. The yaml gate format from §8 is adopted.
sequenceDiagram
participant Op as Operator
participant CF as CF Access (edge)
participant App as Console app
participant DB as Console Postgres
Op->>CF: GET console.raxx.app (carries Google session)
CF->>CF: Validate Google Workspace IDP session
CF->>CF: Check group membership → raxx-platform-admins
CF-->>Op: Set CF Access JWT (email, groups: ["raxx-platform-admins"])
Op->>App: GET /dashboard (CF JWT in cookie)
App->>App: Validate CF JWT signature (JWKS)
App->>App: Extract email + groups claim
App->>App: If "raxx-break-glass" in groups → emit audit alert NOW
App->>App: Verify passkey (WebAuthn assertion)
App->>App: Issue console session cookie on passkey success
Op->>App: GET /api/secrets (requires console:secrets:read)
App->>DB: SELECT group_id FROM user_groups WHERE user_id = admin_id
DB-->>App: [raxx-platform-admins]
App->>DB: SELECT role_id FROM group_roles WHERE group_id IN (...)
DB-->>App: [console-secrets-admin, console-token-admin, ...]
App->>App: Walk role_inheritance DAG → expand role set
App->>DB: SELECT permission FROM role_permissions WHERE role_id IN (...)
DB-->>App: [console:secrets:read, console:secrets:write, ...]
App->>App: "console:secrets:read" IN effective_permissions → ALLOW
App-->>Op: 200 secrets index
GDPR checklist:
| Question | Answer |
|---|---|
| What PII does this collect? | user_groups and group_roles contain admin_id (foreign key to console_admins.email). No new PII beyond what console_admins already holds. |
| What is the retention period? | user_groups rows retained for lifetime of console_admins row. Removed on operator off-boarding (cascade). Retained with soft-delete if account is suspended. |
| How is it deleted on DSR? | Deletion of console_admins cascades to user_groups via FK. group_roles, role_permissions, groups, roles rows are shared resources and are not personal data. |
| What is logged for audit? | Every write to user_groups, group_roles, role_permissions produces a console_audit_log row. Role-grant and role-revoke events are high-sensitivity. |
| Does any part of this store a credential that can be replayed? | No. The JWT groups claim is validated in-flight and never stored. |
| What happens on breach? | user_groups exfiltration exposes group membership (operational sensitivity). No credentials. Incident response: rotate all Console session tokens, audit group membership for anomalies, notify per ADR-0003. |
| Where are secrets? | CF Access OAuth app credentials in Infisical. No secrets in RBAC tables. |
| Kill-switch? | RBAC_V2=0 reverts to old flat-role check without redeploy. CF Access policies revertable via CF dashboard without a Raptor deploy. |
Privilege escalation guard: Only console-invite-admin may write to user_groups and group_roles. That permission is itself resolved via the RBAC DAG — bootstrapped by the seed migration. An operator without console:admins:invite cannot self-escalate via the API.
SAML readiness: Preserved per ADR-0020. When SAML lands, the IdP emits group names like raxx-platform-admins. Those names match groups.name in the Console DB. Raxx role taxonomy is never exposed to the IdP.
Break-glass hardening: raxx-break-glass in the CF JWT groups claim triggers: (1) mandatory console_audit_log entry before passkey step, (2) Slack alert to ops@raxx.app, (3) 2-hour session cap enforced server-side. No additional WebAuthn step is added (the existing passkey is already phishing-resistant and the alert is the accountability mechanism).
These are operational specifics that need Kristerpher's decision before Phase 1 CF reconfiguration begins.
OQ-1: CF group sync method. CF Access can pull Workspace groups via direct Google OIDC scopes or via a SCIM bridge. Direct OIDC scopes (simpler; no extra infra) have a sync delay of up to 1 hour for group membership changes. SCIM bridge provides near-real-time sync but requires a SCIM endpoint. For a small operator team, 1-hour eventual consistency is likely acceptable — confirm before Phase 1.
OQ-2: raxx-devops scope now vs later. At current team size (single operator), raxx-devops is an empty Workspace group. Creating it now reserves the namespace. Confirm whether to create all four Workspace groups upfront (recommended) or create raxx-devops and raxx-support only when first members are added.
OQ-3: TOTP elevation retention. The existing @require_totp_elevation decorator is used on secrets rotation and flag promotion/reject routes today. Under the hybrid model, Workspace 2FA is the MFA mechanism. Should TOTP elevation at the app layer be retained as an additional step-up for the highest-risk operations (secrets rotate, force-revoke), or replaced by passkey re-authentication? The NV10 yaml gate supports a step_up: totp option — this only matters if TOTP is retained. Retaining TOTP adds zero operational cost (seeds already enrolled) and provides defense-in-depth for destructive operations.
OQ-4: Velvet single-user gate. NV10 supports single-user: <admin_id> as a gate option. This is an exception to invariant I2 (no direct permission grants to users). Confirm whether single-user gates are permitted at all in Raxx's deployment, or whether break-glass actions must always go through the break-glass group.
OQ-5: raxx-support access to Console secrets endpoint. Under the current flat model, support cannot reach /secrets (blocked at @require_role("superadmin")). Under the new model, raxx-support-team group does not include console-secrets-user. A future hire in a support role who needs limited secret-name visibility (not values) would need a separate role grant. Confirm whether to add a console-secrets-list permission (names only, no values) as a prospective addition to the taxonomy, or keep secrets fully restricted to raxx-platform-admins.
End of design. See ADR-0043 for the architectural decision record. Sub-cards for the migration phases are filed against this design doc.