Repository navigation
CIMD: production rollout plan (ChatGPT, Codex, VS Code, Claude) #693
Copy link
Copy link
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
Tracking issue for turning CIMD (#375, shipped dark in 4.100.0) on in production. CIMD stays disabled until every step below is done. None of this blocks Unified Identity (mctlhq/.github#91, closed as completed).
Why enabling it is not a one-line change
Findings from 2026-10-18:
OAUTH_CIMD_ENABLEDwas a global capability advertisement (now split, see below). With it on, discovery returnsclient_id_metadata_document_supported: trueto every client. ChatGPT prefers CIMD whenever a server advertises it. A CIMD-capable client whose host is not inOAUTH_CIMD_ALLOWED_HOSTS, or whose document we refuse, then gets400with no DCR fallback. That includes our own claude.ai connector when it reconnects.- Real documents run through
parseDocumenton main:
| Client | Document | Result on 4.101.1 |
|---|---|---|
| ChatGPT | https://chatgpt.com/oauth/client.json |
refused: token_endpoint_auth_method=private_key_jwt (#694) |
| Codex | https://chatgpt.com/oauth/codex/client.json |
refused: one entry is http://localhost/callback (fixed by the Codex PR) |
| VS Code | https://vscode.dev/oauth/client-metadata.json |
accepted (no-store, so the 30 s floor applies) |
| Claude | https://claude.ai/oauth/mcp-oauth-client-metadata |
accepted |
- Cluster preflight PASSED. From inside a
mctl-apipod (wget,Go-http-client/1.1UA), all four documents return200with nocf-mitigated. Caveat: Go's TLS fingerprint differs from wget's, so Cloudflare heuristics are not fully ruled out until a real resolver fetch.
Steps
- Codex compatibility (fix(cimd): drop unusable redirect entries instead of the document #695, merged
5dd4e73d, not released yet). Drop well-formed redirect entries outside the allowlist instead of refusing the document. Exact match against the accepted set only, with no localhost and no normalisation. A refused redirect is logged, clipped, together with the dropped entries. Still open: whichredirect_urispelling real Codex sends (see the comment below). -
private_key_jwtfor CIMD clients (ChatGPT). Separate security feature: CIMD: support private_key_jwt client authentication (ChatGPT) #694. - Split the flag into accepting and advertising (feat(oauth): split CIMD into OAUTH_CIMD_ACCEPT and OAUTH_CIMD_ADVERTISE #697, merged
25357249, not released yet). Shipped asOAUTH_CIMD_ACCEPTandOAUTH_CIMD_ADVERTISE: strict booleans, ADVERTISE requires ACCEPT, and the retiredOAUTH_CIMD_ENABLEDrefuses startup when set. Proposed:OAUTH_CIMD_ACCEPT(the server resolves URLclient_ids) andOAUTH_CIMD_ADVERTISE(discovery announces it).- Production could then run ACCEPT=true and ADVERTISE=false for manual protocol checks without inviting every client to switch.
- Open question: ACCEPT alone still changes behaviour for URL-shaped
client_ids (no fallback), so the "Before turning it on" check indocs/federation.mdapplies to ACCEPT, not only to ADVERTISE.
- First e2e on a non-production OAuth endpoint (preview or staging), with both flags on and
OAUTH_CIMD_ALLOWED_HOSTS=claude.ai,vscode.dev. Prove the chainclient_kind=cimd→ consent page →prn_principal →oauth.grant_issuedaudit entry (client in parameters). Then repeat with ChatGPT and Codex once the two items above have landed. Needs a decision on which non-production endpoint can complete a ZITADEL sign-in. - Production rollout, as a separate owner-approved gitops PR:
- the allowlist names every CIMD client actually in use, exact hosts only (at least
chatgpt.com,claude.ai,vscode.dev); - ChatGPT is the first production smoke;
- rollback is one gitops PR that removes the flag.
- the allowlist names every CIMD client actually in use, exact hosts only (at least
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request
Activity
mashkovd commentedon Oct 18, 2026
Codex e2e check, from the #695 review: once the document parses, Codex resolves to
[http://127.0.0.1/callback]only. Before the production step, confirm on a real Codex authorize request which spelling it sends asredirect_uri. If it sendshttp://localhost:<port>/callback, the CIMD branch refuses with no fallback to the loopback rule, and the next decision is whether to acceptlocalhostonly when the document itself lists it.mashkovd commentedon Oct 18, 2026
#695 (Codex compatibility) and #697 (the ACCEPT/ADVERTISE split) shipped in mctl-api 4.103.0.
4.103.0. gitopsrelease-deployrun 39137363468, bump6e6ac2c79. 4.102.0 was taken by feat: orchestrate explicit full customer account deletion #666, released separately.OAUTH_CIMD_*env is set.client_id_metadata_document_supported, and the ChatGPTclient_idon/authorizegets400. DCR registration and authorize work, and an existing bearer token validates throughmctl_whoami.Next steps: #694 (
private_key_jwt), then the non-production e2e.mashkovd commentedon Oct 18, 2026
ChatGPT end-to-end run against a local mctl-api: passed
The run used the code now released as 4.105.0, against a local mctl-api reached through a cloudflared quick tunnel:
OAUTH_UPSTREAM=zitadelon themctl-api-devZITADEL app (gitops#1981) withOAUTH_ZITADEL_REDIRECT_BASE_URL=http://localhost:8080(feat(oauth): optional ZITADEL redirect base for local e2e runs #706);OAUTH_CIMD_ACCEPT/ADVERTISE=trueandOAUTH_CIMD_ALLOWED_HOSTS=chatgpt.com;TENANT_AUTHZ_MODE=principal.Production was not touched: CIMD stays off there, and the
api.mctl.aimetadata does not advertise it.Proven
https://chatgpt.com/oauth/<app-id>/client.json,token_endpoint_auth_method=private_key_jwt, RS256, jwkshttps://chatgpt.com/oauth/jwks.jsonoauth: ZITADEL sign-in ... result=consent_required→POST /oauth/consent302 back tochatgpt.com/connector/oauth/...→ auditoauth.grant_issued(client_kind: cimd) with the canonicalprn_POST /oauth/token200 fromopenai-connectors-oauth/1.0; the refresh session row hasauth_method=private_key_jwtand the canonicalprincipal_idPOST /mcp200 fromopenai-mcp/1.0.0;mctl_whoamianswered from the tunnel host (Teams: [],Accessible namespaces: [admins]), so the answer came from the local server and not from productionOAUTH_TOKEN_TTL=2m. ChatGPT's next call gotPOST /mcp401, thenPOST /oauth/token200 (refresh grant,private_key_jwt), then a retriedPOST /mcp200. In the database, the old row hasrotated_atset, and the new row hasparent_token_hashpointing at it withauth_method=private_key_jwtkeptFound and fixed
form-action 'self'alone, Chrome blocked the 302 that follows the consent POST. The grant was issued, but the code never reached ChatGPT. Without this fix CIMD cannot be enabled.Harness-only, not an mctl-api bug
invalid Host headerwhen a non-loopbackHostarrives on a loopback connection. That happened because the local proxy dialled127.0.0.1. Pods receive on the pod IP, so production does not hit it.Remaining for this issue
Enabling CIMD in production is a separate, owner-approved gitops PR with
OAUTH_CIMD_ALLOWED_HOSTS=chatgpt.com.mashkovd commentedon Oct 18, 2026
Production rollout done
CIMD went live on 2026-10-18 at 21:33Z through mctlhq/mctl-gitops#1991 (
14b54891d) on mctl-api 4.105.0:OAUTH_CIMD_ACCEPT=true;OAUTH_CIMD_ADVERTISE=true;OAUTH_CIMD_ALLOWED_HOSTS=chatgpt.com,claude.ai,vscode.dev.Discovery now returns
client_id_metadata_document_supported: true.Production smoke
oauth.grant_issuedconsent_required→ consentclient_kind: cimd,client_id: https://chatgpt.com/oauth/<app>/client.jsonauth_method=private_key_jwt, canonicalprn_consent_required→ consentclient_kind: cimd,client_id: https://claude.ai/oauth/mcp-oauth-client-metadataprn_mctl_whoamianswered with the full team setissued(no consent)client_kind: dcrReconnecting an existing connector keeps its stored DCR
client_id. Only a newly created connector picks CIMD, so existing users are not moved.VS Code is on the allowlist but has not been exercised in production.
Rollback: set
OAUTH_CIMD_ADVERTISEto"false", and clients return to DCR on their next sign-in.This issue was closed early, at 14:44Z, by a closing keyword in #696, before the rollout steps were done. With this rollout the steps are now actually complete.