Files
certctl/docs/operator/oidc-runbooks/authentik.md
T
shankar0123 2893f9b48e auth-bundle-2 Phase 11: 6 per-IdP OIDC runbooks + index + docs/README wiring
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.
2026-05-10 15:49:56 +00:00

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 (or default-provider-authorization-implicit-consent if 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 certctl for 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 certctl provider 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 groups with scope groups and 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 for certctl-viewers (and optionally certctl-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-engineersr-operator, certctl-viewersr-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).