Skip to content

Silo IdP management port: mode-scoped Zitadel credential (standalone = full org, fleet = claims-only PAT) #659

Description

@jrosseel

Context

Decision (Jente, 2026-08-18) — refines the 2026-06-29 per-silo management-credential decision. The silo's Zitadel management authority is mode-scoped:

  • Standalone mode: the silo holds full access to its own Zitadel org (it is the whole deployment — org lifecycle, users, roles, grants).
  • Fleet mode: the silo holds a fine-grained service-user PAT that can only read and write claims (project role grants / user authorizations) for its own org. User add/remove stays fleet-owned — the fleet adds/removes silo users.

Today the silo holds no management credential at all (apps/_infra/deploy-k8s/values.yaml: the per-org login client "never holds an identity-provider management credential"), and silo-side Zitadel role-grant writes are explicitly deferred in libs/backend/server/iam/identity/main/src/mirror-groups.ts (referenced #126, now closed — this issue re-tracks that half). Consequence: org admins manage group roles in the Zitadel Console by hand, and claims are login-time-stale.

Design (from the micro-app identity discussion):

  • New port + adapter, e.g. libs/backend/server/iam/idp-management (domain-lib /main sub-package convention), consumed by the groups routes and later by micro-app access grants.
  • Zitadel is the source of truth for the access graph; enforcement stays local per-request. Every grant/revoke writes Zitadel through the port AND mirrors local rows in one $transaction (external call last), so revocation closes access immediately without waiting for re-login. mirror-groups.ts remains the inbound login-time sync.

Todo

  • Define the port with a mode-scoped capability surface keyed on OPENCRANE_MEMBERSHIP_MODE: fleet = { createProjectRole, grantAuthorization, revokeAuthorization, listAuthorizations } only; standalone = additionally full org/user management. Fail-closed Unavailable adapter when the credential Secret is absent (Obot-custody pattern).
  • Zitadel adapter: PAT (or JWT-profile key) auth, x-zitadel-orgid pinned to the silo's own org so cross-org calls are IdP-rejected, bounded failure taxonomy, release-local config only.
  • Verify and document the narrowest Zitadel manager role that permits authorization read/write WITHOUT user management (PROJECT_OWNER is broader than needed — check fine-grained/manager-role options against the live Zitadel version); record in the adapter README.
  • Wire the groups routes (libs/backend/server/iam/groups/main): platform-UI group mutations write Zitadel role grants through the port; Prisma rows + Zitadel call in one $transaction, external call last.
  • Keep per-request enforcement on local rows (guards/gateway); claims stay a login-time sync channel.
  • Helm: Secret mount contract (name agreed with the fleet-side issue), values wiring, k8s-deploy plumbing; docs update: website/security/identity.md + fleet-silo-model (supersedes "silo makes no Zitadel Management API calls").
  • Negative tests per the trust-boundary checklist: missing credential fails closed, cross-org write rejected, revoked grant closes access immediately, standalone-only ops rejected in fleet mode.

Depends on / paired with

  • Fleet-side provisioning of the per-silo claims-scoped service user + PAT (WeOwnAI repo issue).
  • Consumer: micro-app access grants issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions