Repository navigation
Epic: MCTL Unified Identity on ZITADEL and Agent Identity #91
Description
Goal
Finish MCTL's canonical identity and authorization model on top of the already deployed ZITADEL human IdP, so users, agents and services are identified by stable MCTL principals rather than GitHub usernames, while MCP client identity and delegated agent identity remain explicit and auditable.
Architecture decision — current state
ZITADEL is already the platform human identity provider at auth.mctl.ai. This epic is no longer about choosing or deploying an IdP.
The intended boundary is:
- ZITADEL authenticates humans and issues OIDC identities;
- mctl-api canonical principals (
prn_…) are the stable identity used by MCTL authorization, tenant membership, audit and policy; - external identities are linked by
(provider, issuer, subject)to a canonical principal; - GitHub remains a linked identity and capability/integration provider, not the canonical authorization key;
- MCP client identity (pre-registered / CIMD / DCR) is distinct from the authenticated human or service principal;
- an agent action records both the agent actor and the server-derived on-behalf-of subject.
ZITADEL is not a broker in front of every credential type. Static service tokens, MCTL-issued MCP OAuth tokens and supported legacy GitHub credentials continue to terminate in mctl-api through the provider registry where appropriate.
Completed foundation
Required foundation already delivered:
mctl-api#373— canonical human/agent/service principals and linked external identities;mctl-gitops#1499— self-hosted ZITADEL deployment;mctl-gitops#1503— platform hostname reservation protectingauth.mctl.ai;mctl-api#434— ZITADEL registered in the federation registry with issuer/audience validation;mctl-api#435— explicit verified linking of existing identities to ZITADEL identities;mctl-api#374— core provider-agnostic federation/authentication boundary;mctl-api#572 / PR #622— canonical actor/subject identity evidence for Slice D.
Also delivered, non-blocking for epic completion:
mctl-gitops#1500— platform sign-in moved to ZITADEL.
Required work — final status 2026-10-18
All nine required work items in roadmap/epics/unified-identity.yaml are closed as completed. Roadmap publication run 39116819850, captured 2026-10-18T09:49:09Z, reports completion: complete, required 9/9, blocking: [], state healthy, drift 0 and error 0.
MCP client identity: mctl-api#375, done
- CIMD was implemented in mctl-api#624 and merged as
abc14bbdon 2026-10-18, after main was merged in and the review was re-run on the final head (Claude APPROVED, agy PASS, CI green). - It was released in mctl-api 4.100.0, and mctl-gitops
3a949776ebumped prod to it. - It ships dark:
OAUTH_CIMD_ENABLEDis unset, so it is off. - Live check after rollout:
- discovery does not advertise
client_id_metadata_document_supported; - an
https://URLclient_idgets400; - a DCR client's authorize request still reaches the normal sign-in page, with no consent step;
- an existing OAuth token still validates (
identity/selfreturns 200); - no ERROR logs.
- discovery does not advertise
- Turning CIMD on is a separate GitOps decision: it waits for a real directly connected client and an exact-host allowlist.
Delegated agent identity: mctl-api#376, done
- mctl-agents#636 shipped in 1.76.0.
- DevLoop 655 proved the chain
wi_ → we_ → aar_ → art_ → mctl-agents-approve:- the grant was consumed once;
- the decider was recorded as
decided_by_principal_id=prn_….
- mctl-gitops#1913 turned the legacy free-text relay off on 2026-10-16.
- #376 was closed completed on 2026-10-18; the evidence is in its closing comment.
Canonical authorization cut-over: mctl-api#377, done
TENANT_AUTHZ_MODE=principalhas been live since 2026-10-17 (mctl-gitops#1873, mctl-api 4.91.0).- The owner cut the shadow window short.
- Live checks passed, including a ZITADEL customer sign-in to the portal.
Foundation, done
- mctl-api#373, #374, #434, #435
- mctl-gitops#1499, #1503
Non-blocking follow-ups
mctl-api#422: carry the numeric GitHub id in legacy local OAuth JWTs (closed, optional);mctl-api#629: resolve surface binding and human-input respondents by principal (a ZITADEL-only follow-up to #377);mctl-api#684: CIMD hardening P3s from the #624 review, and the rule for turning CIMD on;mctl-gitops#1504— separate tenant applications onto a PSL-listed registrable domain;mctl-gitops#1501— tenant-application SSO through per-tenant ZITADEL organizations.
Success criteria
- Human authentication uses self-hosted ZITADEL, while MCTL authorization is keyed by stable canonical principals.
- A user's authorization is unchanged when the same principal is reached through a different linked identity provider.
- GitHub username/email is not a canonical authorization or tenant-membership key.
- MCP OAuth keeps client identity distinct from the human/service principal; CIMD is supported additively and DCR remains available where required.
- Agent actions distinguish the acting agent from the on-behalf-of subject, and the subject is derived only from server-held grants/decisions.
- Tenant membership and policy checks resolve against canonical principals and fail closed when the required principal cannot be resolved.
- Replacing the human IdP in the future would not require changing MCTL's authorization model.
Status
Complete. The canonical manifest moves to lifecycle: completed in a separate .github PR. The items under non-blocking follow-ups continue outside this epic.
Roadmap control-plane
Canonical graph: roadmap/epics/unified-identity.yaml (completion.mode: allRequired).
Activity
mashkovd commentedon Oct 4, 2026
Human sign-in inventory and ZITADEL plan (2026-10-04)
Sources (all read-only):
mainStatus before this inventory: ZITADEL (
auth.mctl.ai) was the only sign-in for Argo CD and Forgejo. Since then, Grafana and Argo Workflows have also moved: mctl-gitops#1590, #1594, #1595, #1597, and #1598 (Secure cookie).ops.mctl.ai(Argo CD)groupsmaps toargocd/rbac/tenants/<tenant>.csv; opt-in"argocd"git.mctl.ai(Forgejo)__Host-session cookie namesecrets.mctl.ai(Vault)auth/oidc); tokens remainadminsmaps to the admin policy; a tenant gets read on its own paths; opt-in"vault"grafana.mctl.aiadminsgot Admin, every other Dex user got Viewer of the single, cluster-wide orgworkflows.mctl.aiadminsmaps toargo-workflows-admin(all namespaces);<tenant>maps tosso-team-<tenant>, bound in its own namespace only; opt-in"workflows"authorizationcookie name is hard-coded (#1560 residual)app.mctl.ai(Backstage portal)/api/oidc-provider) issues tenant groups fromtenant_members, keyed by GitHub logintenant_members)temporal.mctl.ai,claude-remote.mctl.ai,admins-/labs-/ovk-openclaw.mctl.ai/api/oidc-provider/forward-auth?tenant=<t>&service=<s>. It checks the host registration, a session (GitHub), and a role in the tenant. Tenants:admins,labs,labs,admins/labs/ovkgroupscheckapi.mctl.ai(mctl-api)DEX_ISSUER_URL), its own MCP OAuth JWT (upstream GitHub), static service tokens. A ZITADEL provider is already configured (MCTL_OIDC_PROVIDERS)coolify.mctl.ai(coolify-mcp)tg.mctl.ai(mctl-telegram)agent.mctl.aiprojects.mctl.aiacademy.mctl.aigenericOAuthseerrsense.mctl.aialice.mctl.aimctl.ai(web)rewards.mctl.ai, pairdeskdocs.mctl.ai,ui.mctl.ai, tenant appserpact-*already use ZITADEL, #1501)auth.mctl.aisessionscookie name is hard-coded (#1560)Removing Dex entirely
Dex (
ops.mctl.ai/api/dex) now serves only one consumer: mctl-api, which uses it to verify tokens. Itsmctl-apiclient has no callback route in mctl-api; it only provides an audience.Order of removal. On HOLD until the owner has done the live checks on Grafana and Workflows, and mctl-api#377 has landed:
DEX_ISSUER_URLexplicitly to empty. It defaults to the Dex URL. Do this only after mctl-api#377, or tenant access over tokens is lost.argocd/templates/dex.yaml,dex-config.yaml) and thedex:/server.dex.*values inargocd/values.yaml. Clean up the leftoverargocd-dex-serverobjects.argocd/templates/argocd-github-oauth.yaml: keepwebhook.github.secret(#970). Drop onlyargo-workflows-sso-client-secretandbackstage-oidc-client-*.ops.mctl.ai/api/dex/callback.platform/argo-workflows/sso, today shared by all three Dex clients and by Grafana. Then drop theoidc-client-secretkey fromgrafana-admin-secretand theargo-workflows-ssoExternalSecret.mashkovd commentedon Oct 4, 2026
Owner live checks, 2026-10-04:
mashkovd commentedon Oct 4, 2026
Grafana persistence (mctl-gitops#1601 slice 1) is done. Grafana now keeps its state in its own database on shared-pg (mctl-gitops#1604, #1608). Rows survived a pod restart, and the owner's sign-in after the switch persists as GrafanaAdmin. Grafana remains admins-only.
mashkovd commentedon Oct 4, 2026
Vault human sign-in through ZITADEL (2026-10-04)
Humans now sign in to Vault (
secrets.mctl.ai) through ZITADEL. Both sides are declarative: zitadel-iac configures ZITADEL, and a new in-cluster OpenTofu Job,vault-human-auth-iac, configures Vault.vault-human-auth-iac, the hand-off Secret, and the Job's narrow Vault policy (bootstrapped once by the owner)Vault(rolesadmins,<tenant>), opt-in tenant grants ("vault": true), thevaultweb client (code + PKCE). The sharedgroupsaction now covers Argo CD and Vault.auth/oidc("MCTL login"), rolezitadel, external groupshuman-admins(→ existingadmin) andhuman-tenant-<tenant>. First rollout was plan-only.create update; human tokens capped at 1hsecret/teams/<tenant>/(no destroy, no metadata write;<svc>/databaseread-only)Verified:
groupsclaim carries only the roles for its own client.human-admins→admin;Open:
auth/oidc→auth/mctlremount and the<username>-<sub>display name are approved and driven by a separate track.githubauth method stays as the documented fallback until the owner decides otherwise.Runbook:
docs/runbooks/vault-human-auth.md.[-]Epic: MCTL Unified Identity and Agent Identity Broker[/-][+]Epic: MCTL Unified Identity on ZITADEL and Agent Identity[/+]mashkovd commentedon Oct 18, 2026
Completed. All nine required work items are closed; the canonical manifest is
lifecycle: completed(#198,6f35424f). Roadmap publication run 39117400061 (captured 2026-10-18T10:00:48Z) reportsunified-identityhealthy, required 9/9, blocking [], drift 0, error 0. Non-blocking follow-ups continue outside the epic: mctlhq/mctl-api#629, mctlhq/mctl-api#684, mctlhq/mctl-gitops#1501, mctlhq/mctl-gitops#1504.mashkovd commentedon Oct 18, 2026
Post-completion evidence: mctl-api 4.100.1 in production (2026-10-18)
#685 shipped the CIMD hardening (Closes mctlhq/mctl-api#684). It was released as 4.100.1 rather than 4.101.0, because every commit in it is
fix:, which makes this a patch under semver.Release and rollout
4.100.1.release-deployrun 39125872339 succeeded. Bump commit440a0b8a8(chore: bump admins/mctl-api to 4.100.1).mctl-apipods runghcr.io/mctlhq/mctl-api:4.100.1and are ready (started 12:08Z). No"level":"ERROR"lines in the first 15 minutes.CIMD is still disabled
OAUTH_CIMD_*env var, soOAUTH_CIMD_ENABLEDdefaults to false./.well-known/oauth-authorization-serverdoes not advertiseclient_id_metadata_document_supported./oauth/authorizewith an HTTPS-URLclient_idreturns400.No regression in the OAuth and DCR flows
POST /oauth/registerissues adcr_…client./oauth/authorizefor that client returns200with the sign-in page, offering both "Sign in with GitHub" and "Sign in with MCTL account".mctl_whoamireturns the owner with admin rights and the expected teams.Roadmap projection
roadmap-publishrun 39126814481, evaluator revision6f35424f.unified-identity:allRequired9/9 complete,blocking: [], healthhealthy, drift 0, errors 0. Manifestlifecycle: completed.Non-blocking follow-ups
mashkovd commentedon Oct 18, 2026
Follow-up: on the owner's request the same line was re-released as mctl-api 4.101.0, a minor bump with no code change since 4.100.1.
Release-As: 4.101.0), then release PR #689, then tag4.101.0.release-deployrun 39130068735, bump9c7787b42. Both pods run4.101.0(2/2 ready, 13:09Z) with 0 ERROR lines.OAUTH_CIMD_*env, no discovery field, an HTTPSclient_idgets400.mctl_whoami.mashkovd commentedon Oct 18, 2026
Full ZITADEL-callback sign-in verified live on mctl-api 4.101.0 (2026-10-18 13:11Z). It is the same OAuth code as 4.100.0 and 4.100.1.
tools/mctl-token/mctl_token.py login, which registered a new DCR clientdcr_7661a610…(log lineOAuth client registered), and chose "Sign in with MCTL account".oauth: ZITADEL sign-in principal_id=prn_01M39HRB7JRXZ2PHW5VVB695YZ login=mashkovd zitadel_subject=393619200037357269 result=issued./api/v1/identity/selftoactor.principal_id=prn_01M39HRB7JRXZ2PHW5VVB695YZ,kind=human.So the post-merge path of #624 (
finishAuthorizationfrom the ZITADEL callback) issues a code for a pre-registered DCR client without regression. This was not a required item; it is recorded as evidence.mashkovd commentedon Oct 18, 2026
mctl-api 4.101.1 is in production and carries the agreed #375/#687 hardening: #690 (netsafe
2001::/23and3fff::/20, plus the CIMD limits documentation), which closed mctlhq/mctl-api#687.4.101.1. gitopsrelease-deployrun 39131616174, bump8601c8f97. Pods 2/2 ready on4.101.1, 0 ERROR lines.OAUTH_CIMD_*env, no discovery field, an HTTPSclient_idgets400; DCR registration and authorize work; the ZITADEL-issued bearer token still resolves toprn_01M39HRB7JRXZ2PHW5VVB695YZ.allRequired9/9 complete. The epic stays closed as completed.