Authentik Decommission Implementation Plan
Authentik Decommission Implementation Plan
Section titled “Authentik Decommission Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: Use
dev-flow:subagent-driven-development(recommended) ordev-flow:executing-plansto implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Remove Authentik from the cluster entirely — deployment, database, DNS, Vault paths, tf/authentik module, and the main-cluster-authentik TFC workspace — replacing its embedded-outpost forward-auth with a shared oauth2-proxy, without breaking any live auth path.
Architecture: Keycloak (fzymgc realm) is already the IdP for all OIDC apps, k3s, and MCP DCR. This plan (a) stands up one shared oauth2-proxy (keycloak-oidc) fronted by Traefik forwardAuth with per-route ?allowed_groups=<app>-users overrides to preserve per-app least-privilege on 5 internal dashboards, (b) tears down every Authentik capability in dependency order (proxies → MCP/Tandoor leftovers → LDAP → flows → identity → deployment), and (c) sweeps docs/memory/CLAUDE.md. Each phase is one PR; HCP TFC applies on merge, ArgoCD syncs manifests.
Tech Stack: Terraform (keycloak provider ~>5.8, vault, authentik provider — being removed), HCP Terraform (agent workspaces), ArgoCD + Kustomize, Traefik Middleware/IngressRoute CRDs, oauth2-proxy chart, External Secrets Operator, Vault KV-v2, CNPG.
Source of truth: design spec docs/engineering/specs/2026-07-03-authentik-decommission-design.md (design bead hl-0ahs).
Model intent (for plan-to-beads, Rule 5): mechanical file-delete / TF-forget tasks (T0, T4, T6, T7 Step 3, T8, T10) → model:sonnet. Live-auth cutover and irreversible teardown (T1–T3 oauth2-proxy stand-up + dashboard cutover; T9 deployment/DB/DNS teardown = point of no return) → model:opus, operator-driven. T5 (Tandoor) → model:sonnet, gated on hl-nknn. T7’s HyperDX-alerting build (Steps 1-2) is filed as its own observability child bead → model:sonnet.
Ground rules (read once)
Section titled “Ground rules (read once)”- GitOps only. Never
kubectl applyor manually trigger a TFC run. Each task ends by committing to the phase branch and opening a PR; merge tomaintriggers the TFC plan/apply and ArgoCD sync. “Verify apply” = read the TFC run +argocd app get/ smoke test, not a manual apply. - Terraform preview is read-only.
terraform planlocally (or the Terraform MCPget_run_details) previews; it does not apply. - Vault dueling-writer rule (from
hl-5g05memory):tf/authentikandtf/keycloakboth declaredvault_kv_secret_v2.<app>atsecret/fzymgc-house/cluster/<app>. Plain-deleting thetf/authentikresource destroys the shared secret. Always forget viaremoved { from = ...; lifecycle { destroy = false } }, never a bare delete. Confirm the plan shows “Plan to remove” (forget), never “destroy”, for anyvault_kv_secret_v2. - Standing replacement cascade (memory
hl-je6h):main-cluster-keycloakplans carry a baseline ~9-to-add / 9-to-destroy from the provider~>5.8cascade. A keycloak PR’s real footprint is the delta above that baseline. Don’t panic at 9/9. - Split-horizon CA (memory
hl-9lcj):id.fzymgc.houseserves the internal ICA1 chain in-cluster; oauth2-proxy (Go) needs a combined ICA1+Mozilla bundle. Failures are restart-latent — smoke-test a pod restart, not just first boot. - Each phase is gated: do not start phase N+1 until phase N’s PR is merged and verified.
Files touched (decomposition map)
Section titled “Files touched (decomposition map)”Created
tf/keycloak/oauth2_proxy.tf— 5 flat<app>-usersgroups + operator bindings, one confidentialoauth2-proxyclient,groupsscope binding, Vault secret write.argocd/cluster-app/templates/oauth2-proxy.yaml— ArgoCDApplication(oauth2-proxy chart).argocd/app-configs/oauth2-proxy/kustomization.yamlargocd/app-configs/oauth2-proxy/namespace.yamlargocd/app-configs/oauth2-proxy/secrets.yaml— ExternalSecret (client + cookie secret).argocd/app-configs/oauth2-proxy/ca-bundle.pem— vendored combined ICA1+Mozilla bundle, surfaced via aconfigMapGeneratorinkustomization.yaml(matches miniflux/agent-memory).argocd/app-configs/oauth2-proxy/middlewares.yaml— oneoauth-auth-<app>Middleware per dashboard + sharedoauth-errors.docs/adr/hl-0ahs-decommission-authentik.md— decommission ADR (auto-filed by capture-adrs).
Modified (middleware swap — hubble & agentgateway keep Middleware and IngressRoute in separate files)
argocd/app-configs/hubble-ui/ingress-route.yaml(swap ref) + deletetraefik-middleware.yaml+ drop fromhubble-ui/kustomization.yamlargocd/app-configs/agentgateway/ui-ingress.yaml(swap ref) + deletemodern-auth.yaml+ drop fromagentgateway/kustomization.yamlargocd/app-configs/temporal-server/ingress.yaml(inline Middleware — partial edit)argocd/app-configs/service-ingresses/traefik-dashboard.yaml(combined — partial edit)argocd/app-configs/service-ingresses/longhorn-dashboard.yaml(combined — partial edit)
Deleted (teardown, dependency-ordered — all authentik_group deletions deferred to T8 because users.tf references them cross-file):
- T0: 6 group-free OIDC
removed{}files; providers trimmed fromuptime-kuma.tf/mealie.tf(groups kept). - T3: proxies trimmed from
agentgateway.tf/temporal.tf/hubble-ui.tf(groups kept);argocd/app-configs/authentik/traefik-middleware.yaml. - T7:
enrollment-flow.tf,policies/; event rules trimmed fromnotifications.tf(security_alertsgroup kept). - T8:
users.tf+ every remaining group-holding file (groups.tf,kubernetes-groups.tf,argocd.tf, group-only remnants of mealie/uptime-kuma/agentgateway/temporal/hubble-ui/notifications);vault.tfis EDITED (groups removed) not deleted — itsdata.vault_kv_secret_v2.authentik(provider token) survives to T9. - T9:
git rm -r tf/authentik(residual;mcp_public_oauth.tfgone T4,tandoor.tfT5,nas.tfT6);argocd/cluster-app/templates/authentik.yaml,argocd/app-configs/authentik/,argocd/app-configs/cnpg/{db-authentik,pooler-authentik}.yaml;main-cluster-authentikentry intf/hcp-terraformlocal.all_workspaces; shared cnpg edits (kustomization.yaml−2 lines,postgres-cluster.yamldrop theauthentikmanaged role);tf/cloudflare(authentry invar.authentik_services+r2.tfauthentik_mediaresources);tf/uptime-kuma(authentik monitor + output);tf/vault(vault_policy.terraform_authentik_admin+vault_jwt_auth_backend_role.tfc_authentik+cluster/authentik*grants) — separatemain-cluster-cloudflare/vault/uptime-kumaPRs.
Task 0: Free wins — uptime-kuma proxy + OIDC removed{} leftovers
Section titled “Task 0: Free wins — uptime-kuma proxy + OIDC removed{} leftovers”Group-retention invariant (applies to T0/T3/T7):
tf/authentik/users.tfbinds humans toauthentik_groups declared across many files, and Terraform resolves those refs at plan time — so NOauthentik_groupmay be deleted untilusers.tfgoes in Task 8 (the repo documents this invault.tf/mealie.tfcomments). Earlier phases delete group-free files and edit group-holding files down to just their groups.
Files:
- Delete (verified 0 groups,
removed{}-only):tf/authentik/{clickstack,miniflux,karakeep,engram_ui,cloudflare-access,octopus}.tf - Edit (remove non-group resources, KEEP groups until T8):
tf/authentik/uptime-kuma.tf(keepuptime_kuma_users),tf/authentik/mealie.tf(keepmealie_users/mealie_admins) - Step 1: Confirm the 6 delete-files are group-free
removed{}-only
Run: rg -c 'resource "authentik_group"' tf/authentik/{clickstack,miniflux,karakeep,engram_ui,cloudflare-access,octopus}.tf; rg -L 'resource "authentik_(provider_oauth2|application|group)"' tf/authentik/{clickstack,miniflux,karakeep,engram_ui,cloudflare-access,octopus}.tf
Expected: 0 groups in each; files hold only removed{} blocks + data sources. (mealie.tf retains 2 groups, uptime-kuma.tf 1 — those are edited, not deleted.) If any of the 6 declares a group or live provider, STOP.
- Step 2: Edit uptime-kuma.tf and mealie.tf down to their groups
uptime-kuma is launcher-only (auth on Cloudflare Access — rg -n 'Cloudflare Access' tf/authentik/uptime-kuma.tf). Remove authentik_provider_proxy.uptime_kuma + authentik_application.uptime_kuma + authentik_policy_binding.uptime_kuma_access; keep authentik_group.uptime_kuma_users. In mealie.tf, remove the removed{} vault block + any data source; keep authentik_group.mealie_users/mealie_admins. Both group sets stay referenced by users.tf and are deleted in T8.
- Step 3: Delete the 6 group-free files
Run: git rm tf/authentik/{clickstack,miniflux,karakeep,engram_ui,cloudflare-access,octopus}.tf
(In jj: just rm them; the working copy auto-tracks the deletion.)
- Step 4: Preview — removals/forgets + uptime-kuma destroy, NO group destroys
Run: cd tf/authentik && terraform validate && terraform plan
Expected: validate succeeds (users.tf group refs still resolve). Plan: authentik_provider_oauth2.uptime_kuma + authentik_application.uptime_kuma + authentik_policy_binding.uptime_kuma_access destroy; removed{} blocks show vault_kv_secret_v2.<app> as “Plan to remove” (forget, NOT destroy). Zero authentik_group destroys; zero vault_kv_secret_v2 destroys. If a group or vault secret shows “destroy”, STOP.
- Step 5: Commit + PR
Commit chore(authentik): remove uptime-kuma proxy + migrated OIDC leftover files [hl-0ahs], push the phase branch, open PR. Per references/vcs-preamble.md use jj: jj bookmark create authentik-decommission-p0 -r @ && jj git push -b authentik-decommission-p0.
- Step 6: After merge — verify TFC applied cleanly
Check the main-cluster-authentik run applied; terraform plan on main shows no drift for these resources. The uptime-kuma app launcher entry disappears from the Authentik portal (cosmetic; auth still via Cloudflare Access at status.fzymgc.net).
Task 1: Keycloak side — per-app groups + oauth2-proxy client
Section titled “Task 1: Keycloak side — per-app groups + oauth2-proxy client”Files:
- Create:
tf/keycloak/oauth2_proxy.tf - Reference:
tf/keycloak/client_scopes.tf(existing sharedgroupsscope + membership mapper),tf/keycloak/karakeep.tf(pattern for a per-app group + Vault write) - Step 1: Write
tf/keycloak/oauth2_proxy.tf— the 5 flat groups
# Per-app groups + confidential client for the shared oauth2-proxy that replaces# Authentik's embedded-outpost forward-auth (design hl-0ahs §4). Per-app# least-privilege is enforced by the per-route ?allowed_groups=<app>-users# override on each dashboard's Traefik forwardAuth address, NOT a global flag.
locals { oauth2_proxy_dashboards = ["agentgateway", "temporal", "hubble", "traefik-dashboard", "longhorn"]}
resource "keycloak_group" "dashboard_users" { for_each = toset(local.oauth2_proxy_dashboards) realm_id = keycloak_realm.fzymgc.id name = "${each.key}-users"}
# FLAT groups, not nested. oauth2-proxy enforces one binary gate per route# (?allowed_groups=<app>-users) — there is no admin/user tier to model (that was# an Authentik policy-engine artifact). And with the shared mapper's# full_path=false, the groups claim carries only DIRECT memberships, so a nested# -admin child would NOT satisfy the -users gate anyway. Flat is simpler + correct.
# Bind the operator into every dashboard group so the gate has a member. WITHOUT# this, the per-route gate denies EVERYONE (mirrors keycloak_user_groups.# operator_karakeep). exhaustive=false so we don't clobber other memberships.# Add family members here as needed.resource "keycloak_user_groups" "operator_dashboards" { for_each = toset(local.oauth2_proxy_dashboards) realm_id = keycloak_realm.fzymgc.id user_id = data.keycloak_user.operator.id exhaustive = false group_ids = [keycloak_group.dashboard_users[each.key].id]}- Step 2: Add the confidential client + groups-scope binding to the same file
resource "keycloak_openid_client" "oauth2_proxy" { realm_id = keycloak_realm.fzymgc.id client_id = "oauth2-proxy" name = "oauth2-proxy (dashboard forward-auth)" enabled = true
access_type = "CONFIDENTIAL" standard_flow_enabled = true
# One redirect URI per protected dashboard host; oauth2-proxy callback is /oauth2/callback. valid_redirect_uris = [ "https://agentgateway.fzymgc.house/oauth2/callback", "https://temporal.fzymgc.house/oauth2/callback", "https://hubble.fzymgc.house/oauth2/callback", "https://traefik.k8s.fzymgc.house/oauth2/callback", "https://longhorn.fzymgc.house/oauth2/callback", ]}
# Attach the shared groups client-scope so the session carries group membership# for the per-route ?allowed_groups= checks (mirrors the OIDC apps).resource "keycloak_openid_client_default_scopes" "oauth2_proxy" { realm_id = keycloak_realm.fzymgc.id client_id = keycloak_openid_client.oauth2_proxy.id default_scopes = [ "profile", "email", "roles", "web-origins", "acr", "basic", keycloak_openid_client_scope.groups.name, # existing shared scope (client_scopes.tf:6) ]}Verify the exact scope resource name: rg -n 'keycloak_openid_client_scope\|groups' tf/keycloak/client_scopes.tf and match the reference (keycloak_openid_client_scope.groups.name).
- Step 3: Write the client secret + a generated cookie secret to Vault
resource "random_password" "oauth2_proxy_cookie" { length = 32 special = false}
resource "vault_kv_secret_v2" "oauth2_proxy" { mount = "secret" name = "fzymgc-house/cluster/oauth2-proxy/oidc" data_json = jsonencode({ client_id = keycloak_openid_client.oauth2_proxy.client_id client_secret = keycloak_openid_client.oauth2_proxy.client_secret cookie_secret = random_password.oauth2_proxy_cookie.result })}- Step 4: Format + validate
Run: cd tf/keycloak && terraform fmt && terraform validate
Expected: Success! The configuration is valid.
- Step 5: Preview plan — confirm delta above the 9/9 baseline
Run: terraform plan
Expected additions: 5 keycloak_group.dashboard_users, 5 keycloak_user_groups.operator_dashboards, 1 keycloak_openid_client.oauth2_proxy, 1 keycloak_openid_client_default_scopes.oauth2_proxy, 1 random_password, 1 vault_kv_secret_v2.oauth2_proxy — on top of the standing ~9-add/9-destroy provider cascade (memory hl-je6h). No unexpected destroys.
- Step 6: Commit + PR + verify after merge
Commit feat(keycloak): add oauth2-proxy client + 5 dashboard groups [hl-0ahs]. After merge, confirm in Keycloak the oauth2-proxy client exists and the 5 <app>-users groups appear (with the operator as a member of each); the Vault secret is readable: vault kv get secret/fzymgc-house/cluster/oauth2-proxy/oidc.
Task 2: Deploy the shared oauth2-proxy (CA bundle + ExternalSecret + chart)
Section titled “Task 2: Deploy the shared oauth2-proxy (CA bundle + ExternalSecret + chart)”Files:
- Create:
argocd/app-configs/oauth2-proxy/{namespace,kustomization,secrets,ca-bundle}.yaml - Create:
argocd/cluster-app/templates/oauth2-proxy.yaml - Reference:
argocd/app-configs/miniflux/(ICA1 CA-bundle + ESO pattern),argocd/cluster-app/templates/authentik.yaml(Application shape) - Step 1: Namespace + ExternalSecret
argocd/app-configs/oauth2-proxy/namespace.yaml:
apiVersion: v1kind: Namespacemetadata: name: oauth2-proxyargocd/app-configs/oauth2-proxy/secrets.yaml:
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: oauth2-proxy-secrets namespace: oauth2-proxyspec: refreshInterval: 15m secretStoreRef: name: vault kind: ClusterSecretStore target: name: oauth2-proxy-secrets creationPolicy: Owner data: - secretKey: client-id remoteRef: { key: fzymgc-house/cluster/oauth2-proxy/oidc, property: client_id } - secretKey: client-secret remoteRef: { key: fzymgc-house/cluster/oauth2-proxy/oidc, property: client_secret } - secretKey: cookie-secret remoteRef: { key: fzymgc-house/cluster/oauth2-proxy/oidc, property: cookie_secret }- Step 2: Combined ICA1 + Mozilla CA bundle (configMapGenerator — repo convention)
The repo surfaces the bundle via a configMapGenerator over a vendored .pem, NOT a static ConfigMap manifest (see argocd/app-configs/miniflux/kustomization.yaml:24-29 and argocd/app-configs/agent-memory/kustomization.yaml). Vendor it: cp argocd/app-configs/agent-memory/engram-ca-bundle.pem argocd/app-configs/oauth2-proxy/ca-bundle.pem — it already contains ICA1 fullchain + Mozilla roots (ICA1-only fails public discovery; Mozilla-only fails the split-horizon host; refresh both together from curl.se per the miniflux comment). Step 4’s kustomization.yaml surfaces it as ConfigMap oauth2-proxy-ca-bundle, key ca.crt.
- Step 3: ArgoCD Application (oauth2-proxy chart, pinned latest GA)
Confirm the latest GA chart version first: helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests && helm search repo oauth2-proxy/oauth2-proxy --versions | head -3. Pin it in argocd/cluster-app/templates/oauth2-proxy.yaml targetRevision. Chart values (via helm.valuesObject):
config: # chart schema is config.existingSecret (NOT top-level); keys must be # client-id / client-secret / cookie-secret (matches the ExternalSecret above). existingSecret: oauth2-proxy-secrets configFile: | provider = "keycloak-oidc" oidc_issuer_url = "https://id.fzymgc.house/realms/fzymgc" email_domains = ["*"] cookie_domains = [".fzymgc.house"] whitelist_domains = [".fzymgc.house"] reverse_proxy = true set_xauthrequest = true provider_ca_files = ["/etc/ssl/oauth2-proxy/ca.crt"]extraArgs: # authorization is per-route via ?allowed_groups=; do NOT set a global --allowed-groupMount the oauth2-proxy-ca-bundle ConfigMap (key ca.crt) at /etc/ssl/oauth2-proxy/. Verify the provider_ca_files key name against the pinned chart’s values.yaml (helm show values oauth2-proxy/oauth2-proxy --version <v> | rg -i ca); if the chart exposes it differently, use SSL_CERT_FILE env → the combined bundle path.
- Step 4: Kustomization
argocd/app-configs/oauth2-proxy/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources: - namespace.yaml - secrets.yaml - middlewares.yaml # created in Task 3configMapGenerator: - name: oauth2-proxy-ca-bundle files: - ca.crt=ca-bundle.pem options: disableNameSuffixHash: true- Step 5: Local render check
Run: kubectl --context fzymgc-house kustomize argocd/app-configs/oauth2-proxy
Expected: renders Namespace + ExternalSecret + ConfigMap without error (middlewares.yaml added in Task 3 — either stub it now or add to kustomization in Task 3).
- Step 6: Commit + PR + verify pod health AND a restart
Commit feat(oauth2-proxy): deploy shared Keycloak forward-auth proxy [hl-0ahs]. After merge + ArgoCD sync: kubectl -n oauth2-proxy get pods → Running; then delete the pod (kubectl -n oauth2-proxy delete pod -l app=oauth2-proxy) and confirm it comes back Ready — this catches the restart-latent CA-trust trap before any dashboard depends on it. Check logs for TLS errors against id.fzymgc.house.
Task 3: Cut dashboards over to oauth2-proxy (one at a time), then drop Authentik proxies
Section titled “Task 3: Cut dashboards over to oauth2-proxy (one at a time), then drop Authentik proxies”Files:
- Create:
argocd/app-configs/oauth2-proxy/middlewares.yaml - Modify (swap the
middlewares:ref on the IngressRoute):argocd/app-configs/hubble-ui/ingress-route.yaml,argocd/app-configs/agentgateway/ui-ingress.yaml,argocd/app-configs/temporal-server/ingress.yaml(inline),argocd/app-configs/service-ingresses/{traefik-dashboard,longhorn-dashboard}.yaml - Delete + drop from kustomization:
argocd/app-configs/hubble-ui/traefik-middleware.yaml(fromhubble-ui/kustomization.yaml),argocd/app-configs/agentgateway/modern-auth.yaml(fromagentgateway/kustomization.yaml) - Edit after cutover (remove proxy provider/app/bindings, KEEP the
<app>_users/<app>_admingroups until T8):tf/authentik/agentgateway.tf,temporal.tf,hubble-ui.tf - Delete:
argocd/app-configs/authentik/traefik-middleware.yaml - Step 1: Author the per-app + shared middlewares
argocd/app-configs/oauth2-proxy/middlewares.yaml — one auth middleware per dashboard (differing only in the ?allowed_groups= query) + a shared errors middleware:
apiVersion: traefik.io/v1alpha1kind: Middlewaremetadata: { name: oauth-auth-hubble, namespace: oauth2-proxy }spec: forwardAuth: address: "http://oauth2-proxy.oauth2-proxy.svc/oauth2/auth?allowed_groups=hubble-users" trustForwardHeader: true authResponseHeaders: [X-Auth-Request-User, X-Auth-Request-Groups, X-Auth-Request-Email]---apiVersion: traefik.io/v1alpha1kind: Middlewaremetadata: { name: oauth-errors, namespace: oauth2-proxy }spec: errors: status: ["401-403"] service: { name: oauth2-proxy, port: 80 } # adjust to the chart's Service name/port query: "/oauth2/sign_in?rd={url}"Repeat the oauth-auth-<app> block for temporal (allowed_groups=temporal-users), agentgateway (agentgateway-users), traefik-dashboard (traefik-dashboard-users), longhorn (longhorn-users). Bare group names, NO leading slash — the shared groups mapper sets full_path = false (client_scopes.tf), so the groups claim carries hubble-users, not /hubble-users; with the keycloak-oidc provider a slash matches nothing and denies everyone (the exact mealie.tf/vault.tf footgun). Verify the oauth2-proxy Service name/port: kubectl -n oauth2-proxy get svc.
- Step 2: Cut over dashboard #1 (hubble) — swap the middleware reference
hubble’s Middleware and IngressRoute are in separate files. In argocd/app-configs/hubble-ui/ingress-route.yaml (the IngressRoute), change the middlewares: list entry (currently - name: modern-auth) to the cross-namespace pair { name: oauth-auth-hubble, namespace: oauth2-proxy } + { name: oauth-errors, namespace: oauth2-proxy } — object form only (the @kubernetescrd string form is NOT valid in a native IngressRoute middlewares: list). Then delete the now-unused Authentik middleware file argocd/app-configs/hubble-ui/traefik-middleware.yaml and remove its line from argocd/app-configs/hubble-ui/kustomization.yaml (line 7), or the Kustomize build breaks.
- Step 3: Verify hubble cutover (the “test”)
After merge + sync: browse https://hubble.fzymgc.house as a hubble-users member → Keycloak login → dashboard loads. As a non-member (or a Keycloak user not in hubble-users) → 403 from oauth2-proxy. curl -sI https://hubble.fzymgc.house unauthenticated → 302 to /oauth2/sign_in, not to auth.fzymgc.house.
Expected: authenticated member OK; non-member denied; no redirect to Authentik.
- Step 4: Roll the remaining 4 dashboards
Repeat Steps 2-3, minding each dashboard’s file layout: traefik-dashboard.yaml + longhorn-dashboard.yaml (both service-ingresses/, combined Middleware+IngressRoute — partial-edit in place); temporal-server/ingress.yaml (inline Middleware — partial-edit in place); agentgateway (separate files, like hubble) — swap the middlewares: ref in argocd/app-configs/agentgateway/ui-ingress.yaml, delete argocd/app-configs/agentgateway/modern-auth.yaml, and drop it from agentgateway/kustomization.yaml (line 23). One PR per dashboard is acceptable; verify each before the next.
- Step 5: Remove the Authentik proxy providers (keep their groups until T8)
Edit tf/authentik/{agentgateway,temporal,hubble-ui}.tf to remove authentik_provider_proxy.* + authentik_application.* + authentik_policy_binding.*, but KEEP authentik_group.*_users/*_admin (still referenced by users.tf until T8). Delete argocd/app-configs/authentik/traefik-middleware.yaml. cd tf/authentik && terraform validate && terraform plan → expected: destroy 3 authentik_provider_proxy + 3 authentik_application + their policy bindings; zero authentik_group destroys; validate passes. No vault_kv_secret_v2 destroys.
- Step 6: Commit + PR + final verify
Commit feat(oauth2-proxy): cut hubble/temporal/agentgateway/traefik/longhorn off Authentik forward-auth [hl-0ahs]. After merge, all 5 dashboards authenticate via oauth2-proxy; rg -rn 'outpost.goauthentik.io/auth/traefik' argocd/ returns nothing.
Task 4: MCP OAuth leftover — verify Keycloak issuer, delete mcp_public_oauth.tf
Section titled “Task 4: MCP OAuth leftover — verify Keycloak issuer, delete mcp_public_oauth.tf”Files:
-
Delete:
tf/authentik/mcp_public_oauth.tf -
Step 1: Verify no live consumer references the Authentik MCP issuer
Run: rg -rn 'application/o/mcp' argocd/ ; rg -rn 'issuer' argocd/app-configs/agentgateway argocd/cluster-app/templates/agent-memory.yaml
Expected: issuers are https://id.fzymgc.house/realms/fzymgc (Keycloak). No Authentik /application/o/mcp-public/ issuer. Cross-check via ClickStack: token-issuer telemetry shows only the Keycloak issuer over the last 24h.
- Step 2: Delete the file + preview
git rm tf/authentik/mcp_public_oauth.tf; cd tf/authentik && terraform plan
Expected: destroy authentik_provider_oauth2.mcp_public + authentik_application.mcp_public (+ any vault_kv_secret_v2 shown as forget, not destroy — check).
- Step 3: Commit + PR + verify MCP still works
Commit chore(authentik): remove MCP OAuth leftover — consumers on Keycloak DCR [hl-0ahs]. After merge: exercise a Claude Code / agentgateway MCP connection to engram → still authenticates (via Keycloak mcp-public + DCR, ADR hl-ll5p). If it breaks, a consumer was still on Authentik — revert and re-audit.
Task 5: Tandoor OIDC — gated on hl-nknn
Section titled “Task 5: Tandoor OIDC — gated on hl-nknn”Files:
-
Delete:
tf/authentik/tandoor.tf(only afterhl-nknncloses) -
Step 1: Gate check
Run: bd show hl-nknn
Expected: status closed (Tandoor retired). If not closed, STOP — this task is blocked; skip to Task 6 and return here after hl-nknn. Deleting tandoor.tf while Tandoor is live destroys its login.
- Step 2: Confirm Tandoor is actually gone from the cluster
Run: kubectl get ns tandoor 2>&1; rg -rn 'tandoor' argocd/cluster-app/templates/
Expected: namespace gone / Application removed. If Tandoor still runs, hl-nknn isn’t truly done — STOP.
- Step 3: Delete + preview + commit
git rm tf/authentik/tandoor.tf; terraform plan → destroy authentik_provider_oauth2.tandoor + application + policy bindings; vault_kv_secret_v2.tandoor shown as forget (or destroy is acceptable here only if Tandoor’s Vault path is fully retired by hl-nknn — confirm). Commit chore(authentik): remove Tandoor OIDC provider (Tandoor retired, hl-nknn) [hl-0ahs].
Task 6: NAS LDAP removal (hl-k8kp)
Section titled “Task 6: NAS LDAP removal (hl-k8kp)”Files:
- Delete:
tf/authentik/nas.tf(LDAP provider bits) - Modify:
argocd/app-configs/monitoring-otel-scraper/scrape-collector-values.yaml(drop theak-outpost-ldapscrape job, line ~182) - Step 1: Confirm the NAS no longer binds the Authentik LDAP outpost
Run: scripts/nas-playbook.sh check (per docs/operations/nas.md) that the NAS auth source is not the Authentik LDAP outpost (ak-outpost-ldap.authentik.svc:9300). Expected: NAS uses local/other auth, LDAP unused (hl-k8kp premise).
- Step 2: Remove the LDAP provider + outpost + scrape job
Delete the authentik_provider_ldap (and its outpost) from tf/authentik/nas.tf; remove the ak-outpost-ldap job_name block (line ~182) from scrape-collector-values.yaml. cd tf/authentik && terraform plan → destroy authentik_provider_ldap + the LDAP outpost.
- Step 3: Commit + PR + verify
Commit chore(authentik): decommission NAS LDAP integration [hl-k8kp]. After merge: kubectl -n authentik get pods shows no ak-ldap outpost pod; the Prometheus/OTel scraper no longer targets :9300.
Task 7: Drop enrollment + notification flows; file the security-alert replacement bead
Section titled “Task 7: Drop enrollment + notification flows; file the security-alert replacement bead”Files:
-
Delete:
tf/authentik/enrollment-flow.tf,tf/authentik/notifications.tf,tf/authentik/policies/ -
Step 1: File the security-alert replacement bead FIRST (blocks this task’s notification delete)
Run: bd create --type=task --title="Rebuild Authentik security-event alerting as ClickStack/HyperDX TILE alerts" --description="Replace the 5 Authentik security-alerts email notifications (login_failed, suspicious_request, password_set, config_audit, new-sign-in) with ClickStack/HyperDX TILE alerts over Keycloak otel_logs -> the security-alerts Pushover contact. This is the cluster's live alerting path (hl-qjh3/hl-toju DONE); Grafana+Prometheus are sunset (hl-nlxl). Unblocks hl-iawe SMTP retirement." --labels="authentik,keycloak,observability,hl-qjh3" --priority=2 --parent hl-0ahs
Then bd dep add hl-0ahs <new-id> is not needed (parent link suffices); note the new ID here.
- Step 2: Build the replacement alert rules (in the new bead’s PR, before deleting notifications.tf)
Create ClickStack/HyperDX TILE alerts on the otel_logs Keycloak stream for the 5 event types → the security-alerts Pushover contact, mirroring the existing TILE alerts from hl-qjh3.5 / hl-toju (NOT Grafana — it is sunset per hl-nlxl). Verify one fires (trigger a failed Keycloak login, confirm the Pushover alert). This is the new bead’s deliverable; Step 3 waits on it.
- Step 3: Delete enrollment flow + notification rules (keep the security_alerts group until T8)
git rm tf/authentik/enrollment-flow.tf; git rm -r tf/authentik/policies. Edit tf/authentik/notifications.tf to remove the authentik_event_rule/event_transport/policy_event_matcher/policy_binding/policy_expression resources, but KEEP authentik_group.security_alerts (still referenced by users.tf until T8; its alerting function is already replaced by the Step-2 HyperDX TILE alerts). terraform validate && terraform plan → destroy the flow/stage/invitation/event resources; zero authentik_group destroys.
- Step 4: Commit + PR + verify
Commit chore(authentik): drop enrollment + notification flows (alerting moved to ClickStack/HyperDX) [hl-0ahs]. After merge, confirm no Authentik email notifications remain and the HyperDX TILE alerts are the live alerting path.
Task 8: Identity teardown — delete tf/authentik users/groups
Section titled “Task 8: Identity teardown — delete tf/authentik users/groups”Files:
- Delete:
tf/authentik/users.tf+ every remaining group-holding file — post T0/T3/T7:groups.tf,kubernetes-groups.tf,argocd.tf, and the group-only remnants ofmealie.tf,uptime-kuma.tf,agentgateway.tf,temporal.tf,hubble-ui.tf,notifications.tf(rg -l 'resource "authentik_group"' tf/authentik/, minusvault.tf). - Edit (NOT delete):
tf/authentik/vault.tf— remove the 2authentik_groups, KEEPdata.vault_kv_secret_v2.authentik(theprovider "authentik"token source interraform.tf; it dies with the module at T9). - Step 1: Verify all humans exist in Keycloak
In Keycloak admin: confirm sean (sub ab096608-…), dorothy, sophia exist in the fzymgc realm with expected group membership. dorothy/sophia auto-provision via the GitHub IDP (github_idp.tf, ADR hl-sqtn, sync_mode = IMPORT) — if either hasn’t logged in yet, have them log in once first. Expected: all three present.
- Step 2: Delete users.tf + all group files atomically — but EDIT vault.tf (dual-role), don’t delete it
users.tf is the only thing still referencing the retained groups, so it and every group-holding file must change in the same atomic PR (delete groups before users.tf → dangling-ref error; delete users.tf first → groups unreferenced — one PR avoids both).
vault.tf carve-out: it is a MIXED file — it holds the two authentik_group resources AND data "vault_kv_secret_v2" "authentik", which terraform.tf’s provider "authentik" block reads for its API token. Deleting it whole breaks the provider (static “undeclared data source” error). So edit vault.tf to remove only authentik_group.vault_users/vault_admin, keeping the data source (it dies with the module at T9). Delete the rest:
Run: git rm tf/authentik/users.tf $(rg -l 'resource "authentik_group"' tf/authentik/ | grep -v '/vault\.tf$')
Then edit tf/authentik/vault.tf down to just the data "vault_kv_secret_v2" "authentik" block.
The prevent_destroy guards on users.tf/groups.tf do NOT block this — that guard only protects a resource still present in config; removing the block entirely drops the guard, so Terraform destroys cleanly.
- Step 3: Preview + confirm no Vault secret destroys
Run: cd tf/authentik && terraform plan
Expected: destroy the 3 users + ~22 groups. data.vault_kv_secret_v2.authentik (the provider token source) is a data source — unaffected until Task 9. No vault_kv_secret_v2 resource destroys.
- Step 4: Commit + PR + verify SSO unbroken
Commit chore(authentik): delete redundant identity (users/groups now Keycloak-owned) [hl-0ahs]. After merge, all humans still authenticate to every app via Keycloak (Authentik identities were already redundant).
Task 9: Deployment + infrastructure teardown (point of no return)
Section titled “Task 9: Deployment + infrastructure teardown (point of no return)”Files:
- Delete:
argocd/cluster-app/templates/authentik.yaml,argocd/app-configs/authentik/,argocd/app-configs/cnpg/{db-authentik,pooler-authentik}.yaml, remainingtf/authentik/*(module) - Edit (shared files / other workspaces — surgical):
argocd/app-configs/cnpg/kustomization.yaml(−2 lines),argocd/app-configs/cnpg/postgres-cluster.yaml(dropauthentikmanaged role),tf/hcp-terraform/workspaces.tf(dropmain-cluster-authentik),tf/cloudflare/{variables,tunnel,r2}.tf,tf/uptime-kuma/{main,outputs}.tf,tf/vault/{policy-terraform-workspaces,jwt-hcp-terraform}.tf - Step 1: Gate — confirm Tasks 4–8 are all merged/closed
Run: ls tf/authentik/*.tf 2>&1; bd show hl-nknn; bd list --status open --parent hl-0ahs
Expected: mcp_public_oauth.tf gone (T4); tandoor.tf gone + hl-nknn closed (T5); nas.tf LDAP gone (T6); the security-alert-replacement bead (filed in T7 Step 1) closed with HyperDX TILE alerts live (T7); enrollment-flow.tf gone + notifications.tf event rules removed (T7; the file + its security_alerts group go in T8); users.tf/groups.tf/kubernetes-groups.tf gone (T8). If ANY is still live/open, STOP — this phase destroys the pod they all depend on.
- Step 2: Remove
auth.fzymgc.houserouting — Traefik IngressRoute + Cloudflare tunnel/DNS
The DNS record + tunnel ingress are generated from var.authentik_services — a plain rg 'auth\.fzymgc\.house' tf/ finds nothing (the literal is built by "${svc}.fzymgc.house" interpolation in tf/cloudflare/tunnel.tf:37). Two edits in two workspaces:
- k8s (ArgoCD): the Traefik IngressRoute
argocd/app-configs/authentik/ingress-route.yamlis removed by Step 3’sgit rm -r argocd/app-configs/authentik. - Cloudflare (
main-cluster-cloudflareworkspace, separate PR): empty theauthentry fromvariable "authentik_services"default (tf/cloudflare/variables.tf:111-115) — or delete the variable and its two consumers intf/cloudflare/tunnel.tf(ingress-ruleforat line 37 +resource "cloudflare_dns_record" "authentik_services"at line 104).terraform planintf/cloudflare→ destroyscloudflare_dns_record.authentik_services["auth"]+ the tunnel ingress rule. - Step 2b: Tear down the Authentik R2 media integration (
tf/cloudflare/r2.tf)
tf/cloudflare/r2.tf (hl-7qlb) backs Authentik media (app/source icons + admin backups) with an R2 bucket; once Authentik is gone it is orphaned. Backup decision (irreversible, like the CNPG DB): icons are disposable, but confirm no wanted admin backup remains in the bucket; if unsure, sync it out first. Then delete from tf/cloudflare/r2.tf: cloudflare_r2_bucket.authentik_media, cloudflare_r2_custom_domain.authentik_media (media.auth.fzymgc.net), cloudflare_account_token.authentik_media_r2 (revokes the RW token), vault_kv_secret_v2.authentik_media_storage (cluster/authentik/media-storage), and the two output blocks. In tf/vault/policy-terraform-workspaces.tf remove the dedicated cluster/authentik/media-storage grant (~lines 230-261). Lands with the main-cluster-cloudflare + main-cluster-vault PRs.
- Step 3: Remove the ArgoCD Application + CNPG database, managed role, and shared-file refs
git rm argocd/cluster-app/templates/authentik.yaml; git rm -r argocd/app-configs/authentik; git rm argocd/app-configs/cnpg/db-authentik.yaml argocd/app-configs/cnpg/pooler-authentik.yaml. Then edit the two shared cnpg files (deleting the referenced files without these edits breaks the Kustomize build AND strands a live role):
argocd/app-configs/cnpg/kustomization.yaml: remove the- db-authentik.yaml+- pooler-authentik.yamllines (11-12), orkubectl kustomize argocd/app-configs/cnpgfails (cluster-wide ArgoCD sync break — the app is shared by every DB tenant).argocd/app-configs/cnpg/postgres-cluster.yaml: remove theauthentikentry fromspec.managed.roles(line ~64:- name: authentik+passwordSecret: main-authentik-credentials).db-authentik.yamlowned that secret; leaving the role strands a livelogin: truePostgres role with its password gone → unreconcilable managed-role state on the sharedmaincluster.
Before merge, confirm a Velero backup of the authentik namespace / CNPG DB exists (velero backup get | rg authentik). Render-check: kubectl kustomize argocd/app-configs/cnpg must still build.
- Step 4: Delete the
tf/authentikmodule + its Vault policy/role
git rm -r tf/authentik. In tf/vault (main-cluster-vault workspace) remove, by name: vault_policy.terraform_authentik_admin (policy-terraform-workspaces.tf:91-182, incl. the cluster/authentik data/metadata path blocks — the cluster/authentik/media-storage grant is a SEPARATE policy, already removed in Step 2b) and vault_jwt_auth_backend_role.tfc_authentik (jwt-hcp-terraform.tf:38-53, binds workspace:main-cluster-authentik → terraform-authentik-admin). Clean the now-dead cluster/postgres/users/main-authentik credential path if TF-managed. Confirm: rg -n 'authentik' tf/vault/ shows only comment mentions afterward.
- Step 5: Remove the TFC workspace (workspaces-as-code)
In tf/hcp-terraform, delete the main-cluster-authentik entry from local.all_workspaces (workspaces.tf). Per tf/CLAUDE.md: an operator runs terraform apply in tf/hcp-terraform (Local exec, manual-apply) to destroy the workspace + its TFC_VAULT_* vars. Do NOT delete it in the HCP UI.
- Step 5b: Remove the Uptime Kuma monitor of auth.fzymgc.house
tf/uptime-kuma (main-cluster-uptime-kuma workspace) has uptimekuma_monitor_http.authentik (main.tf:25, url https://auth.fzymgc.house/if/admin/) — once auth.fzymgc.house is gone it alerts forever. Remove that resource + its authentik = uptimekuma_monitor_http.authentik.id line in outputs.tf:6. Separate main-cluster-uptime-kuma PR.
- Step 6: Commit + PR + verify Authentik is gone
Commit chore(authentik): tear down deployment, database, DNS, module, and TFC workspace [hl-0ahs]. After merge + apply: kubectl get ns authentik → NotFound; kubectl -n postgres get cluster → no authentik DB; curl -sI https://auth.fzymgc.house → no route; the main-cluster-authentik workspace is gone from HCP.
Task 10: Documentation / memory / CLAUDE.md sweep
Section titled “Task 10: Documentation / memory / CLAUDE.md sweep”Files: (~27 files; Step 3/4’s live rg -il authentik is authoritative — see spec §7)
- Modify:
CLAUDE.md,tf/CLAUDE.md,docs/operations/mcp-gateway-clients.md,docs/operations/{hubble-ui,temporal}.md,docs/reference/{services,secrets,network,technologies,index}.md,docs/architecture/{overview,index}.md,docs/getting-started/*,docs/index.md,README.md,docs/operations/{index,hcp-terraform,vault,clickstack,octopus,cilium-migration}.md - Delete:
docs/operations/authentik.md - Create:
docs/adr/hl-0ahs-decommission-authentik.md(if not already filed by capture-adrs) - Step 1: Root + tf CLAUDE.md
CLAUDE.md line 14: Infrastructure as code (Vault, Authentik) → (Vault, Keycloak). tf/CLAUDE.md: remove the main-cluster-authentik workspace-table row (line ~54) and the MCP note referencing it (line ~61).
- Step 2: Rewrite the operational docs
Delete docs/operations/authentik.md (or replace with a 2-line “decommissioned 2026, see ADR hl-0ahs” stub). In docs/operations/mcp-gateway-clients.md rewrite the stale “engram uses Authentik OAuth” / “Authentik mcp-public” lines to Keycloak. In docs/operations/{hubble-ui,temporal}.md rewrite the auth section to describe the shared oauth2-proxy + per-route ?allowed_groups= gate.
- Step 3: Prune reference/architecture/getting-started
rg -il authentik docs/ README.md → for each remaining file, remove Authentik rows / auth.fzymgc.house / cluster/authentik secret references. Also prune stale code comments/labels the teardown leaves behind: argocd/cluster-app/templates/hubble-ui.yaml header comment, argocd/app-configs/k8s-oidc-rbac/kustomization.yaml (app.kubernetes.io/part-of: authentik label), tf/cluster-bootstrap/README.md, and tf/keycloak/argocd.tf’s now-stale “ArgoCD keeps using Authentik until…” comment (the cutover is done — its own tf/authentik/argocd.tf says “verified green”). Keep historical docs/adr/*, docs/engineering/specs/*, docs/engineering/plans/* mentions (they are history).
- Step 4: Verify the sweep is complete
Run: rg -il authentik docs/ README.md CLAUDE.md tf/CLAUDE.md tf/ argocd/ | rg -v 'engineering/(specs|plans)|/adr/'
Expected: no output (every non-historical reference — including code comments/labels — gone).
- Step 5: Update engram memory
Per engram:curating-memory: supersede the “Authentik decommissioning in-progress” records (they now describe a completed state) and confirm the durable pattern memory (e972552b: shared oauth2-proxy + per-route allowed_groups + CA-bundle) is accurate as-shipped. Store any as-built deltas (e.g., final chart version, exact provider_ca_files key).
- Step 6: Commit + PR + close the epic
Commit docs(authentik): remove all references post-decommission [hl-0ahs]. After merge, rg -il authentik is history-only. Close hl-0ahs and its children.
Self-review coverage note
Section titled “Self-review coverage note”Every spec §2 area maps to a task: area 1→T0, 2→T1-T3, 3→T4, 4→T5, 5→T6, 6→T8, 7→T7, 8→T7, 9→T9; the §7 doc sweep→T10. Spec §9 decisions are all encoded (security alerts→T7 Step 1-2; traefik/longhorn→T3; auth.fzymgc.house grep→T9 Step 2). No open questions carried in.