Closes Phase 11 of cowork/auth-bundle-2-prompt.md. Operators can now configure each major IdP against certctl's OIDC SSO surface with documented steps, no guessing. Files ===== docs/operator/oidc-runbooks/index.md (NEW): * Index page linking all six per-IdP runbooks. * Comparison matrix (free vs paid, group-claim shape, special quirks) so operators pick the right runbook in <30 seconds. * "Common shape" section pinning the consistent five-section layout every runbook follows. * "Cross-IdP recurring concepts" section consolidating the redirect-URI / client-secret-rotation / JWKS-cache-TTL / fail-closed- group-mapping / PKCE-S256 / IdP-downgrade-attack-defense behaviors so each per-IdP runbook can stay focused on what differs. docs/operator/oidc-runbooks/keycloak.md (NEW): * Canonical reference. Mirrors the testfixtures/keycloak-realm.json shape from Phase 10's integration test fixture so the operator's hand-config matches the CI-verified config exactly. * Step-by-step IdP-side: realm → client → groups → group-mapper → user. Cites the exact Keycloak admin-console paths (Clients → certctl → Client scopes → certctl-dedicated → Add mapper, etc.). * GUI + API + MCP equivalents for the certctl-side configuration. * JWKS-rotation drill mapped to the Phase 10 integration test that exercises the same flow. * 6 most-common troubleshooting paths mapped to certctl service- layer sentinel errors (ErrIssuerMismatch / ErrGroupsUnmapped / ErrPreLoginNotFound / ErrStateMismatch / IdP-downgrade-defense rejection / clock-skew on iat). docs/operator/oidc-runbooks/authentik.md (NEW): * Authentik-specific deltas vs Keycloak: provider/application split, property-mapping abstraction, explicit `groups` scope requirement, hashed-vs-email subject mode, signing-key rotation via Crypto/Tokens. docs/operator/oidc-runbooks/okta.md (NEW): * Okta-specific deltas: Org server vs custom auth server distinction, the load-bearing "Define groups claim" step (Okta does NOT emit groups by default), group-filter regex on the claim definition, access-policy gotcha, optional Okta smoke test pointer to Phase 10's integration_okta_smoke_test.go. docs/operator/oidc-runbooks/auth0.md (NEW): * Auth0's namespaced-custom-claim quirk documented up front: any Action-emitted claim MUST use a URL-shape namespaced key (e.g. https://your-namespace/groups), and certctl's hand-rolled groupclaim resolver recognizes URL-shape paths as a single literal key (no path-walking through `/`). Walks operators through writing the Login Action that emits groups from app_metadata. Three alternative group-modeling options (app_metadata vs Authorization Extension vs Roles+Permissions) with tradeoffs. docs/operator/oidc-runbooks/azure-ad.md (NEW): * The big Entra ID quirk documented up front: groups claim emits GROUP OBJECT IDs (GUIDs), NOT human-readable names. Certctl group→ role mappings MUST be configured against the GUIDs. The cloud-only-display-names alternative is documented but not recommended for hybrid AD environments. Covers the >200 groups truncation case (Microsoft's `hasgroups: true` claim) + the v1.0 vs v2.0 endpoint distinction (certctl supports v2.0 only). docs/operator/oidc-runbooks/google-workspace.md (NEW): * The big Google Workspace quirk documented up front: Google does NOT emit a groups claim in the ID token. Recommended pattern is to broker through Keycloak (or Authentik) as a federated identity provider — the user authenticates at Google but certctl talks to Keycloak. Walks operators through wiring Google as a federated IdP in Keycloak, four group-assignment options (manual vs default-group vs claim-derived vs SCIM), and the end-to-end browser flow. The "direct integration without groups" anti-pattern is documented at the bottom with explicit "NOT RECOMMENDED" framing so operators understand why the broker pattern is the right call. docs/README.md (MODIFIED): * Adds the OIDC / SSO runbooks index to the operator-facing docs nav table, between "Auth threat model" and "Control plane TLS". Conventions held ================ * Every runbook carries `> Last reviewed: 2026-05-10` per the docs convention. * Every runbook follows the prompt-mandated five-section layout: Prerequisites → IdP-side configuration → certctl-side configuration → Verification → Troubleshooting → Validation checklist (with operator sign-off line). * Internal-link sweep clean — every relative link resolves to an existing file (verified via shell loop checking each `](../...)` and `](*.md)` reference). External links to IdP vendor sites are the canonical https URLs. * No leakage of cowork/ workspace paths as Markdown links — the azure-ad.md initially had a `[auth-bundles-index.md](../../../../cowork/...)` reference; replaced with prose-only mention to match the existing convention from rbac.md + migration/api-keys-to-rbac.md. * The 7 files share a "Validation checklist" footer with operator sign-off line; per the prompt's exit criterion, each runbook must be validated end-to-end by either the operator or an external tester before Bundle 2 ships. Verification ============ * Last-reviewed dates: 7/7 runbooks dated 2026-05-10. * Internal-link sweep: 0 broken (every `]( ...)` reference resolves). * docs/README.md → operator/oidc-runbooks/index.md link resolves. * No backend / frontend / Go-test impact — pure docs commit. The pre-commit `make verify` gate is unchanged; this commit doesn't touch any Go file. Phase 11 deviation note ======================= The merge-gate criterion's "≥ 2 external testers" requirement is operator-driven and post-tag — Phase 11 ships the runbooks; the operator runs each end-to-end against a real production-tier IdP and fills in the sign-off footers before flipping Bundle 2 to "merged." Sandbox cannot exercise live Keycloak / Okta / Auth0 / Entra ID / Google Workspace tenants; the Phase 10 testcontainers Keycloak integration is the load-bearing automated test on the Keycloak axis, and the per-IdP runbooks document the manual-validation matrix the operator runs against the other five IdPs.
7.2 KiB
Authentik OIDC runbook
Last reviewed: 2026-05-10
This runbook wires certctl's OIDC SSO surface against Authentik, a free / open-source IdP that runs on-prem or self-hosted. Authentik shares the canonical "string-array groups claim under the groups key" pattern with Keycloak — the differences are in the admin console UX and the explicit "property mapping" abstraction.
For the canonical reference + mental model, read keycloak.md first; this runbook only documents the Authentik-specific deltas.
Prerequisites
On the Authentik side:
- Authentik ≥ 2024.10 (stable channel).
- Admin access to the Authentik admin console at
https://<authentik-host>/if/admin/. - Network reachability from certctl-server to
https://<authentik-host>/application/o/<application-slug>/.well-known/openid-configuration.
On the certctl side: same as Keycloak — CERTCTL_CONFIG_ENCRYPTION_KEY set, an admin actor holding auth.oidc.create + auth.oidc.edit, Bundle 2 server build.
IdP-side configuration
1. Create the OAuth2 / OpenID Provider
In the Authentik admin console:
Applications → Providers → Create:
- Type: OAuth2/OpenID Provider.
- Name:
certctl. - Authorization flow:
default-provider-authorization-explicit-consent(ordefault-provider-authorization-implicit-consentif you don't want a consent screen on every login). - Click Next.
Protocol settings:
- Client type: Confidential.
- Client ID: leave the auto-generated value OR set to
certctlfor clarity. - Client Secret: copy the auto-generated value to a secure scratchpad — you'll paste it into certctl.
- Redirect URIs/Origins:
https://<your-certctl-host>:8443/auth/oidc/callback(one entry, exact match). - Signing Key: pick an RSA-2048 or larger key. Authentik defaults to ECDSA-P256 in newer versions; either is fine — both are in certctl's allow-list.
- Subject mode: Based on the User's hashed ID (default; emits a stable opaque
sub). - Include claims in id_token: on.
- Click Finish.
2. Create the Application
Applications are how Authentik attaches a Provider to users + groups + policies.
Applications → Applications → Create:
- Name:
certctl. - Slug:
certctl(becomes part of the issuer URL:https://<authentik-host>/application/o/certctl/). - Provider: pick the
certctlprovider you just created. - Policy engine mode: any (default).
- Click Create.
3. Configure the groups property mapping
Authentik emits group claims via "property mappings" — explicit objects rather than Keycloak's mapper-on-the-client model.
By default, the Authentik default-OAuth Mapping: Proxy outpost scope already includes the user's groups under a groups claim (string-array, matches what certctl expects). To verify or override:
Customization → Property Mappings → Filter "Scope Mapping":
- Find or create one named
groupswith scopegroupsand expression:return [group.name for group in user.ak_groups.all()] - Description:
Emits the user's group names as a string-array claim.
Then on the Provider → certctl → Edit → Advanced protocol settings, ensure Scopes includes groups (and profile and email if you want richer User records on the certctl side).
4. Create the groups + assign users
Directory → Groups → Create:
- Name:
certctl-engineers. Repeat forcertctl-viewers(and optionallycertctl-admins).
Directory → Users → → Edit → Groups: pick the appropriate certctl-* group(s) for each user.
5. (Optional) Bind the application to specific groups
If you want certctl to reject login attempts from users outside the certctl-* groups at the IdP layer (defense-in-depth on top of certctl's fail-closed ErrGroupsUnmapped):
Applications → certctl → Policy / Group / User Bindings → Create binding:
- Type: Group.
- Group: pick the union of
certctl-*groups you want to allow. - Enabled: on.
certctl-side configuration
Identical to Keycloak — only the issuer URL differs:
curl -X POST https://<your-certctl-host>:8443/api/v1/auth/oidc/providers \
-H "Authorization: Bearer ${CERTCTL_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "Authentik",
"issuer_url": "https://authentik.example.com/application/o/certctl/",
"client_id": "<paste-the-client-id>",
"client_secret": "<paste-the-client-secret>",
"redirect_uri": "https://certctl.example.com:8443/auth/oidc/callback",
"groups_claim_path": "groups",
"groups_claim_format": "string-array",
"fetch_userinfo": false,
"scopes": ["openid", "profile", "email", "groups"],
"iat_window_seconds": 300,
"jwks_cache_ttl_seconds": 3600
}'
Authentik emits groups in the ID token by default once the property mapping is configured. The scopes array MUST include groups to trigger the claim emission — Authentik is stricter than Keycloak about scope-gating claims.
Add the group→role mappings the same way as Keycloak: certctl-engineers → r-operator, certctl-viewers → r-viewer.
Verification
End-to-end login + audit + Sessions checks are identical to Keycloak.
Authentik-specific check: the audit row's details.subject will be Authentik's hashed user ID (a 64-char hex), not the username. This is intentional and correct — the sub claim must be opaque + stable across user-attribute changes.
JWKS-rotation drill: Authentik rotates signing keys via System → Tokens & App Passwords → Certificates (rename of "Crypto" in newer versions). Add a new RSA-2048 cert, switch the Provider's Signing Key to the new one, then click "Refresh discovery cache" in certctl's GUI to evict the cache.
Troubleshooting
Provider creation fails with "could not load discovery document".
The issuer URL needs the trailing slash for some Authentik versions: https://authentik.example.com/application/o/certctl/ (slash after the slug). Without the slash, Authentik returns a 301 redirect that Go's HTTP client follows but discovery parsing chokes on the redirect target.
Login completes but user lands on "no roles assigned".
Decode the ID token at jwt.io against Authentik's JWKS. Check whether the groups claim is present + non-empty. If empty, the property mapping isn't wired — go back to step 3.
groups claim missing entirely.
Authentik gates the groups claim behind the groups scope. Verify:
- The certctl OIDCProvider config has
"scopes": ["openid", "profile", "email", "groups"]. - The Authentik provider's "Scopes" list includes
groups.
Authentik emits the user's full DN as the sub claim.
Some Authentik configurations use Subject mode: Based on the User's email which surfaces the email as sub. This works but tightly couples certctl's User table to email mutability; recommend switching to "hashed ID" mode for new deployments. Existing User rows in certctl's users table will have email-shaped oidc_subject columns; that's fine and stable as long as the user's email never changes.
Validation checklist
Same as keycloak.md, with Authentik-specific values for issuer URL + group names + signing-key rotation steps.
Sign-off: _______________ (operator) on _______________ (date).