Raxx · internal docs

internal · gated ↑ index

Auth Unification — RBAC Reconciliation

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


1. Context

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, auditor as 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.


2. Invariants

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.

3. Existing-State Inventory

3.1 Current role enforcement in console/app/blueprints/ and middleware/

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.

3.2 Data model in main tree

console/app/models/admin.py defines: - Admin — the operator identity record; has a roles relationship to AdminRole - AdminRoleadmin_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.

3.3 Velvet v2 / NV10 (#954) role vocabulary

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.


4. Google Workspace Identity Provider Survey

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.


5. Decision Matrix

Three concrete integration options are evaluated. All three preserve the <app>-<resource>-<level> role naming as the internal canonical namespace.

Option 1: CF Access as network gate only; all authz in app-layer

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.

Option 2: CF Access groups mirror <app>-<resource>-<level> roles fully

What 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:

  1. 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.

  2. 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:

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:

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).


6.1 CF Access group taxonomy (replaces the rejected flat proposal)

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.

6.2 Per-surface CF Access policy templates (revised from auth-unification.md §5.4)

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

7. Mapping Table — Per Surface

7.1 Console (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)

7.2 FreeScout (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.

7.3 Vault (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.

7.4 Velvet admin UI (Phase 2)

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.

7.5 Operator onboarding flow (end-to-end)

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

8. Velvet v2 / NV10 (#954) Alignment

NV10 specifies a yaml-driven revocation auth gate with vocabulary group, role, or single-user. Under the recommended option:

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.


9. Migration Plan

The migration must not disrupt the existing Console auth flow at any step. Each phase is independently releasable and independently rollback-able.

Phase 0: Schema (pre-CF change)

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.

Phase 1: CF Access IDP swap (NV01)

Dependency: Phase 0 complete; DQ-4 resolved (this doc resolves it).

Rollback: re-enable one-time-pin IDP in CF Zero Trust (CF supports multiple IDPs simultaneously).

Phase 2: App-layer reads JWT groups claim

Dependency: 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.

Phase 3: Switch to RBAC v2 DAG resolution

Dependency: Phase 2 complete; RBAC tables seeded; shadow-testing in staging confirms parity.

Rollback: RBAC_V2=0 — reverts to admin_roles string comparison. New RBAC tables retained but not read.

Phase 4: Drop legacy admin_roles column

Dependency: Phase 3 complete; 30-day prod soak with RBAC_V2=1.

Rollback: restore admin_roles table from backup. This is the only destructive step — it requires a full database backup pre-migration.

Phase 5: Velvet v2 auth gate (NV10 / B10)

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.


10. Sequence: CF JWT → App-Layer Permission Resolution

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

11. Security Considerations

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).


12. Open Questions

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.