feat(ai): enable LiteLLM UI SSO through Authentik - #1473
Merged
Conversation
First resource in this stack that is created rather than adopted, so it is the only place a plan should show an addition. Credential handling is deliberately the inverse of the imported providers. There, client_secret is left undeclared because declaring it would rotate a live secret; here there is no live secret to protect and LiteLLM has to be told what it is, so both halves are generated and surfaced through outputs for the one hop OpenTofu cannot make itself. Neither value is written into Git. The redirect URI was read out of the running v1.98.0 image rather than from documentation: ui_sso.py builds it as get_custom_url(PROXY_BASE_URL) + "/sso/callback". The provider's scope mappings are openid/email/profile, which is byte-for-byte LiteLLM's default GENERIC_SCOPE, so a scope mismatch cannot be the cause of a failed login.
Non-secret SSO configuration goes on the LiteLLMProxy CR as plain env so it stays readable in Git; only the two credential halves pass through the ExternalSecret. The two sides are coupled and the coupling is load-bearing: LiteLLM derives its callback as get_custom_url(PROXY_BASE_URL) + "/sso/callback", so PROXY_BASE_URL here and the allowed redirect URI in terraform/authentik/litellm.tofu must move together or every login fails at the provider with an opaque redirect_uri error. Both files say so. The client id and secret are generated by OpenTofu rather than typed by hand, so the value on the Authentik provider and the value LiteLLM presents cannot drift apart. Getting them from there to here needs one hop OpenTofu cannot make itself: tofu output -> a hand-made Secret -> PushSecret -> 1Password -> this ExternalSecret. The push targets a dedicated single-vault SecretStore for the same reason as the Authentik one: a write through the shared ClusterSecretStore resolves by vault priority rather than to a vault you named. Endpoints were taken from the live .well-known/openid-configuration. authorize/token/userinfo are instance-global in Authentik; only issuer and jwks carry the application slug.
… Envoy Measured 2026-08-27: every request from OpenTofu's S3 client through the envoy-internal gateway fails with SignatureDoesNotMatch, while the same credentials against the RGW Service succeed. This is a defect in the merged backend.tofu, which pointed at https://s3.sklab.dev. The cause is which headers get signed. aws-sdk-go-v2 puts accept-encoding, amz-sdk-invocation-id and amz-sdk-request in SignedHeaders; boto3 and minio-go sign none of them. Envoy rewrites one of those in transit, so RGW recomputes a different signature. That is why nothing caught it: VolSync (restic/minio-go) and every other S3 client keep working through the same route, and a boto3 ListObjectsV2 against this exact bucket returns 200 while tofu gets 403. Also adds the two RGW state-key fields to the PushSecret. secrets.vals.yaml already referenced them, so without this a tofu init against the real backend could not authenticate.
A namespaced SecretStore may only reference a Secret in its own namespace; ESO's admission webhook rejects anything else outright. The 1Password Connect token lives in `security`, so the namespaced store worked there by coincidence (namespace matched) and was rejected immediately when ai/litellm needed the same push target. Replaces both namespaced copies with one ClusterSecretStore next to the existing 1Password store. It still exists for the same reason as before: the shared `onepassword` store lists three vaults with priorities, which is right for reads and wrong for writes, since a push through it resolves by that ordering rather than to a vault you named. This is the general form of a review finding from #1458, which flagged the redundant namespace field. That finding was right about the rule and wrong that the security store was broken; the rule only bites once a second namespace needs the same store.
Documents what actually happened rather than what was planned: the apply result, the four post-apply verification checks, the write credential and the evidence it cannot delete, and the manual hop that carries a generated credential from tofu output into the cluster. Also records that the state backend is not reachable through the gateway and needs a port-forward, with the header-signing reason, so the next person does not rediscover it against a SignatureDoesNotMatch.
The stack now holds live state and has two least-privilege credentials, so the AGENTS entry saying nothing had been applied was stale. Two gotchas worth stating once because both fail silently. The tofu state backend cannot be reached through the gateway: aws-sdk-go-v2 signs headers Envoy rewrites, so RGW rejects the signature while every other S3 client in the cluster keeps working. And a kubectl invoked through the mise shims inherits KUBECONFIG from .mise.toml, pointing at a file a fresh worktree does not have, which with stderr suppressed shows up as empty output rather than an error. Also generalises the ESO write-store rule: the single-vault store is cluster-scoped because a namespaced SecretStore may only reference a Secret in its own namespace.
Aviator-Coding
force-pushed
the
fm/homeops-litellm-sso
branch
from
August 28, 2026 11:11
32892e7 to
199ab4f
Compare
Contributor
--- kubernetes/apps/base/security/external-secrets/stores/onepassword Kustomization: security/onepassword-store ClusterSecretStore: security/onepassword-automation
+++ kubernetes/apps/base/security/external-secrets/stores/onepassword Kustomization: security/onepassword-store ClusterSecretStore: security/onepassword-automation
@@ -0,0 +1,23 @@
+---
+apiVersion: external-secrets.io/v1
+kind: ClusterSecretStore
+metadata:
+ labels:
+ app.kubernetes.io/name: onepassword-store
+ kustomize.toolkit.fluxcd.io/name: onepassword-store
+ kustomize.toolkit.fluxcd.io/namespace: security
+ name: onepassword-automation
+ namespace: security
+spec:
+ provider:
+ onepassword:
+ auth:
+ secretRef:
+ connectTokenSecretRef:
+ key: token
+ name: onepassword-secret
+ namespace: security
+ connectHost: http://onepassword-connect.security.svc.cluster.local:8080
+ vaults:
+ Automation: 1
+
--- kubernetes/apps/base/security/authentik/app Kustomization: security/authentik SecretStore: security/onepassword-automation
+++ kubernetes/apps/base/security/authentik/app Kustomization: security/authentik SecretStore: security/onepassword-automation
@@ -1,22 +0,0 @@
----
-apiVersion: external-secrets.io/v1
-kind: SecretStore
-metadata:
- labels:
- app.kubernetes.io/name: authentik
- kustomize.toolkit.fluxcd.io/name: authentik
- kustomize.toolkit.fluxcd.io/namespace: security
- name: onepassword-automation
- namespace: security
-spec:
- provider:
- onepassword:
- auth:
- secretRef:
- connectTokenSecretRef:
- key: token
- name: onepassword-secret
- connectHost: http://onepassword-connect.security.svc.cluster.local:8080
- vaults:
- Automation: 1
-
--- kubernetes/apps/base/security/authentik/app Kustomization: security/authentik PushSecret: security/authentik-terraform-credentials
+++ kubernetes/apps/base/security/authentik/app Kustomization: security/authentik PushSecret: security/authentik-terraform-credentials
@@ -19,22 +19,27 @@
remoteRef:
property: CODER_CLIENT_ID
remoteKey: authentik-terraform
secretKey: CODER_CLIENT_ID
- match:
remoteRef:
- property: OPEN_WEBUI_CLIENT_ID
- remoteKey: authentik-terraform
- secretKey: OPEN_WEBUI_CLIENT_ID
- - match:
- remoteRef:
property: PGADMIN_CLIENT_ID
remoteKey: authentik-terraform
secretKey: PGADMIN_CLIENT_ID
+ - match:
+ remoteRef:
+ property: TF_STATE_ACCESS_KEY_ID
+ remoteKey: authentik-terraform
+ secretKey: TF_STATE_ACCESS_KEY_ID
+ - match:
+ remoteRef:
+ property: TF_STATE_SECRET_ACCESS_KEY
+ remoteKey: authentik-terraform
+ secretKey: TF_STATE_SECRET_ACCESS_KEY
refreshInterval: 1h
secretStoreRefs:
- - kind: SecretStore
+ - kind: ClusterSecretStore
name: onepassword-automation
selector:
secret:
name: authentik-terraform-credentials
--- kubernetes/apps/base/ai/litellm/app Kustomization: ai/litellm ExternalSecret: ai/litellm
+++ kubernetes/apps/base/ai/litellm/app Kustomization: ai/litellm ExternalSecret: ai/litellm
@@ -13,23 +13,27 @@
- extract:
key: litellm
- extract:
key: cloudnative-pg
- extract:
key: ai-keys
+ - extract:
+ key: litellm-sso
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword
target:
name: litellm-secret
template:
data:
ANTHROPIC_API_KEY: '{{ .ANTHROPIC_API_KEY }}'
DATABASE_URL: postgresql://{{ .POSTGRES_DB_USER_NAME }}:{{ .POSTGRES_DB_USER_PASSWORD
}}@postgres-17-rw.database.svc.cluster.local:5432/{{ .POSTGRES_DB_NAME }}
+ GENERIC_CLIENT_ID: '{{ .LITELLM_SSO_CLIENT_ID }}'
+ GENERIC_CLIENT_SECRET: '{{ .LITELLM_SSO_CLIENT_SECRET }}'
INIT_POSTGRES_DBNAME: '{{ .POSTGRES_DB_NAME }}'
INIT_POSTGRES_HOST: postgres-17-rw.database.svc.cluster.local
INIT_POSTGRES_PASS: '{{ .POSTGRES_DB_USER_PASSWORD }}'
INIT_POSTGRES_SUPER_PASS: '{{ .POSTGRES_SUPER_PASS }}'
INIT_POSTGRES_USER: '{{ .POSTGRES_DB_USER_NAME }}'
LITELLM_MASTER_KEY: '{{ .LITELLM_MASTER_KEY }}'
--- kubernetes/apps/base/ai/litellm/app Kustomization: ai/litellm LiteLLMProxy: ai/litellm
+++ kubernetes/apps/base/ai/litellm/app Kustomization: ai/litellm LiteLLMProxy: ai/litellm
@@ -14,12 +14,28 @@
key: LITELLM_MASTER_KEY
name: litellm-secret
applyMode: file
env:
- name: MAX_STRING_LENGTH_PROMPT_IN_DB
value: '1000000'
+ - name: GENERIC_AUTHORIZATION_ENDPOINT
+ value: https://auth./application/o/authorize/
+ - name: GENERIC_TOKEN_ENDPOINT
+ value: https://auth./application/o/token/
+ - name: GENERIC_USERINFO_ENDPOINT
+ value: https://auth./application/o/userinfo/
+ - name: PROXY_BASE_URL
+ value: https://litellm.
+ - name: GENERIC_SCOPE
+ value: openid email profile litellm_role
+ - name: GENERIC_USER_ROLE_ATTRIBUTE
+ value: litellm_role
+ - name: AUTO_REDIRECT_UI_LOGIN_TO_SSO
+ value: 'true'
+ - name: PROXY_LOGOUT_URL
+ value: https://auth./application/o/litellm/end-session/
envFrom:
- secretRef:
name: litellm-secret
generalSettings:
maximum_spend_logs_retention_period: 30d
store_model_in_db: false
--- kubernetes/apps/base/ai/litellm/app Kustomization: ai/litellm PushSecret: ai/litellm-sso-credentials
+++ kubernetes/apps/base/ai/litellm/app Kustomization: ai/litellm PushSecret: ai/litellm-sso-credentials
@@ -0,0 +1,30 @@
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+ labels:
+ app.kubernetes.io/name: litellm
+ kustomize.toolkit.fluxcd.io/name: litellm
+ kustomize.toolkit.fluxcd.io/namespace: ai
+ name: litellm-sso-credentials
+ namespace: ai
+spec:
+ data:
+ - match:
+ remoteRef:
+ property: LITELLM_SSO_CLIENT_ID
+ remoteKey: litellm-sso
+ secretKey: LITELLM_SSO_CLIENT_ID
+ - match:
+ remoteRef:
+ property: LITELLM_SSO_CLIENT_SECRET
+ remoteKey: litellm-sso
+ secretKey: LITELLM_SSO_CLIENT_SECRET
+ refreshInterval: 1h
+ secretStoreRefs:
+ - kind: ClusterSecretStore
+ name: onepassword-automation
+ selector:
+ secret:
+ name: litellm-sso-credentials
+
|
Two things the second live apply forced, neither catchable by validate. authentik_flow's terraform id is the SLUG, so pointing a provider's invalidation_flow at .id returns 400 "not a valid UUID". The stage binding already used .uuid; the provider reference did not. Fixed and documented, because the two attributes look interchangeable and are not. open-webui was deleted from Authentik by the captain on 2026-08-27, confirmed in the audit log: akadmin issued both DELETEs, while the only tofu action that day was a PUT from tofu-writer, which holds no delete permission at all. The deletion was intentional, so its resources, import blocks and client-id variable are removed and the two instances dropped from state, rather than letting the next plan recreate the application with a client secret that would match nothing. Records the write role now holding fourteen permissions including the model-level change_flow, and why every future apply should be checked against a before/after flow snapshot rather than terraform's own summary.
The flow id-vs-uuid distinction, the S3 backend silently falling back to real AWS when AWS_ENDPOINT_URL_S3 is unset, and the standing obligation to verify applies against a database snapshot now that the write role holds model-level change_flow.
This was referenced Aug 28, 2026
Aviator-Coding
added a commit
that referenced
this pull request
Aug 29, 2026
…#1481) * docs(ci): document scripts/ci python regression tests and their CI status Wiring them into validate.yaml is blocked pending a decision: litellm-auto-router-test.py fails against tip of main (externalsecret allow-list predates the litellm-sso 1Password item added in d159d7f). See homeops-ci-pytest-wiring task report for detail. * fix(ci): admit litellm-sso item in ExternalSecret allow-list test litellm-sso is created and populated by OpenTofu (terraform/authentik/litellm.tofu) via a PushSecret to wire LiteLLM's UI SSO through Authentik, landed in d159d7f (PR #1473). The test's allow-list didn't know about it yet; a fifth, unrecognised item still fails this test. * ci(github-action): run scripts/ci python regression tests in validate.yaml Wires the 13 hand-written scripts/ci/*-test.py regression tests, each pinning a specific captain decision, into a new python-tests job so a PR that breaks one of these invariants no longer goes green silently. Follows the existing job pattern: its own filter step reusing the scripts/ci/** path filter, mise-managed tool setup, then a loop over scripts/ci/*-test.py so any future test file is picked up automatically without touching this workflow. * no-mistakes(review): Prefer native promtool over podman in CI * no-mistakes(document): Sync validate docs for python-tests job * no-mistakes: apply CI fixes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Goal: log into the LiteLLM UI at litellm.sklab.dev through Authentik SSO, delivered as a follow-up PR to the merged #1458 plus the first gated live apply of the OpenTofu Authentik stack. Captain addendum 2026-08-27.
HARD SAFETY GATE context: Authentik is the live SSO for the whole cluster including the public gateway's ExtAuth. The captain authorized (1) minting a READ-ONLY credential earlier, (2) minting a least-privilege WRITE credential, and (3) ONE live tofu apply, gated on the plan being additions-only for the new LiteLLM resources with zero changes or destroys to existing objects. The plan showed 4 in-place changes on existing providers, so per the addendum I STOPPED and escalated rather than self-authorizing; the captain then explicitly relaxed the gate after reviewing my per-resource set-comparison proof that the changes were membership-identical property_mappings REORDERING (the documented first-apply import artifact), and instructed me to apply.
Scope delivered:
Defect found and fixed in the MERGED #1458: backend.tofu pointed at https://s3.sklab.dev and OpenTofu's S3 client cannot authenticate through Envoy (SignatureDoesNotMatch). Cause established by measurement: aws-sdk-go-v2 signs accept-encoding, amz-sdk-invocation-id and amz-sdk-request; boto3 and minio-go sign none of them, so Envoy's rewrite breaks only this client - boto3 ListObjectsV2 on the same bucket returns 200 while tofu gets 403, and VolSync keeps working through the same route. Backend now points at the RGW Service via an operator port-forward. Also added the two TF_STATE_* fields to the PushSecret, which secrets.vals.yaml already referenced but nothing populated.
Also refactored the Automation push store from two namespaced SecretStores to one ClusterSecretStore, because a namespaced SecretStore may only reference a Secret in its own namespace (ESO admission rejects otherwise) and the Connect token lives in security, so ai/litellm could not use a namespaced store at all. This is the general form of a review finding from #1458.
APPLY RESULT (authorized, executed): Apply complete! Resources: 9 imported, 4 added, 4 changed, 0 destroyed. Post-apply evidence, all live: fresh tofu plan is empty (-detailed-exitcode returns 0) using the READ-ONLY token; echo.sklab.dev still 302s to Authentik with scope "ak_proxy profile email entitlements openid" so all five proxy scopes survived the reorder; open-webui authorize still 302s to default-authentication-flow; the litellm application exists (provider 70, redirect https://litellm.sklab.dev/sso/callback); and the client_id in 1Password Automation/litellm-sso is byte-identical to the one on the Authentik provider.
Acceptance: CI green. The kubernetes half only takes effect once this PR merges and Flux applies it; the terraform half is already applied and is harmless while unused, which is the intended ordering.
FOLLOW-UP (2026-08-28), after the captain approved the broader flow grant and the second apply:
What Changed
litellm_rolescope mapping, and LiteLLM-only invalidation flow in OpenTofu; generates client credentials, drops the intentionally deleted open-webui resources, and points the state backend at RGW directly (not through Envoy).GENERIC_*,PROXY_BASE_URL, auto-redirect, logout URL), ExternalSecret client id/secret from 1Passwordlitellm-sso, and a PushSecret path via a cluster-scoped AutomationClusterSecretStore.Risk Assessment
Testing
Ran the three focused CI scripts plus live HTTPS probes: Authentik already exposes the litellm issuer with litellm_role and matching authorize/token/userinfo/end-session endpoints; echo still 302s with openid/email/profile/ak_proxy/entitlements; tofu validate and stack/CI contracts are green; LiteLLM UI remains the pre-merge non-SSO surface until Flux applies the Kubernetes env, which matches the intended apply ordering.
Evidence: Evidence summary (OIDC, ExtAuth scopes, pre-merge UI)
Evidence: Live Authentik OIDC discovery for litellm
Evidence: echo.sklab.dev ExtAuth 302 with five proxy scopes
Evidence: LiteLLM UI pre-merge state and end-session hop
Evidence: Compact live public surface JSON
{ "oidc": { "http": 200, "issuer": "https://auth.sklab.dev/application/o/litellm/", "scopes_supported": ["email", "profile", "litellm_role", "openid"], "end_session_endpoint": "https://auth.sklab.dev/application/o/litellm/end-session/" }, "echo_extauth": { "http": 302, "scopes": ["ak_proxy", "email", "entitlements", "openid", "profile"] }, "litellm_ui": { "http": 200, "note": "pre-merge: Flux has not applied GENERIC_* SSO env yet" } }Evidence: litellm-sso-test.py full results
Evidence: tofu-authentik-stack-test.py full results
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 8 issues found → auto-fixed (5) ✅
scripts/ci/tofu-authentik-stack-test.py:107- Stack tests still model open-webui as adopted after the intentional removal: EXPECTED_IMPORTS keeps open-webui provider/app entries (lines 107, 110), _resource_addrs/_all_resource_addrs hardcode for_each keys including open-webui (lines 225 and 257) instead of reading locals, and test_stack_hcl_model still requires variable open_webui_client_id (line 545). applications.tofu/imports.tofu/variables.tofu dropped those objects, so any run of this suite fails and the helper invents addresses that are not in HCL. Align EXPECTED_IMPORTS and variable checks with coder+pg-admin only, and derive for_each keys from the parsed locals map (or a single shared expected-keys constant).scripts/ci/tofu-authentik-stack-test.py:846- test_runbook_acceptance_surface still requires the pre-second-apply gate: substring BLOCKED plus INTERNAL_USER_VIEW_ONLY, and comments say litellm_role/logout remain gated (lines 843-854). docs/authentik/terraform.md status (and the user intent) state the second apply already landed, scopes_supported includes litellm_role, and a fresh plan is empty—those strings are gone from the runbook—so this test fails against the delivered docs. Rewrite assertions to the post-apply contract (role mapping present, invalidation flow present, plan empty / second-apply complete, open-webui removed) instead of freezing the blocked-apply narrative.AGENTS.md:136- AGENTS.md still claims litellm_role + invalidation flow are PENDING/BLOCKED, first SSO stays view-only, and a fresh plan is NOT empty (line 136), and still describes open-webui as an imported managed app with a pending second apply (line 142). User intent requires the opposite delivered state: second apply done, litellm_role and litellm-invalidation-flow live, plan empty with -detailed-exitcode 0, open-webui removed from the stack. This is the operator-facing knowledge base for Authentik/SSO; leaving the blocked-apply story in place will drive wrong plans and false incident response after merge. Update both NOTES bullets to match docs/authentik/terraform.md status and the completed second apply (including writer permission count and open-webui removal).kubernetes/apps/base/ai/litellm/README.md:59- App README still says first SSO login stays view-only until the pending litellm_role apply lands. Intent and the Authentik runbook status say litellm_role is already applied and scopes_supported includes it, so after this PR merges Flux users should land as proxy_admin via GENERIC_SCOPE/GENERIC_USER_ROLE_ATTRIBUTE. Stale view-only wording will make a successful admin SSO login look broken. Replace with the live claim path (litellm_role -> proxy_admin) and point at the completed apply evidence.docs/authentik/terraform.md:132- Section 2 Import strategy still lists open-webui provider/app import IDs as current stack imports (lines 132, 136) even though the status blurb and HCL removed them. Historical inventory in section 1 can keep the 2026-08-26 snapshot, but the import table is written as what imports.tofu contains now and will push operators to re-add a deleted live SSO app. Mark those rows removed/state-rm'd or drop them from the current import table.scripts/ci/litellm-sso-test.py:199- New assertions grep implementation/docs text rather than executable behavior: regex over litellm.tofu for the redirect URL (lines 199-210), raw substring checks on outputs.tofu (lines 212-220), and tofu-authentik-stack-test.py runbook exact-phrase gates (e.g. first-plan sentence, BLOCKED, INTERNAL_USER_VIEW_ONLY). Per the test-quality rule these are source-content-only. Keep live HTTP / kustomize / typed HCL model checks; replace file greps with semantic parses (hcl2/YAML) or observable coupling already covered by live OIDC + CR env comparison.scripts/ci/litellm-sso-test.py:319- test_premerge_litellm_has_no_sso_redirect fails if live /ui/ Location points at Authentik (lines 331-334). That is only valid before Flux applies AUTO_REDIRECT_UI_LOGIN_TO_SSO; after merge the same check becomes a false failure on the intended end state. /sso/login already allows 404 or 302—make the /ui/ check conditional the same way (accept pre-merge non-redirect or post-merge Authentik hop) or drop the absence assertion once the branch is the delivery vehicle for enabling SSO.terraform/authentik/scopes.tofu:3- Header still says there are zero hand-written property mappings on the instance. litellm.tofu now creates authentik_property_mapping_provider_scope.litellm_role (unmanaged). Update the comment so future agents do not treat all mappings as blueprint-only data sources.🔧 Fix: Align SSO docs and tests to delivered state
4 issues (3 warnings, 1 info) still open:
kubernetes/apps/base/security/authentik/app/pushsecret.yaml:38- open-webui was removed from the OpenTofu stack (variables, imports, locals, vals), but PushSecret still pushes OPEN_WEBUI_CLIENT_ID (lines 38-41) and docs/authentik/terraform.md section 4 still lists it as a current authentik-terraform field (line 253). That keeps a deleted app's client_id in the live Automation item and makes the hand-made source Secret still require a dead key. Drop the PushSecret match (and the section 4 row) so the credential surface matches coder + pg-admin only.docs/authentik/terraform.md:44- CI live-plan content moved to section 9, but several CI pointers still say section 8 (now the LiteLLM credential hop): docs intro line 44, secrets section line 290, terraform/authentik/README.md line 14, and secrets-ci.vals.yaml line 33. Operators following those links land on tofu output -> 1Password instead of terraform-diff / Connect CI. Retarget those CI references to section 9; leave true credential-hop refs (outputs.tofu, pushsecret-sso, AGENTS.md LiteLLM secret path) on section 8.docs/authentik/terraform.md:124- Section 2 still asserts present-tense "Every resource the stack declares is paired with an import block" and frames the first plan as nine creates of live objects only. LiteLLM is deliberately create-only with no import (litellm.tofu). That absolute claim fights the delivered model and can push someone to add import blocks for already-managed create-path resources. Scope the sentence to adopted resources and note create-only LiteLLM separately.terraform/authentik/README.md:37- README still says "The second-apply gate for the role mapping and invalidation flow is in docs..." after both applies landed and the runbook status is empty-plan. Rephrase to delivered second-apply evidence rather than an open gate.🔧 Fix: Drop open-webui PushSecret; fix SSO docs xrefs
4 issues (3 warnings, 1 info) still open:
.github/workflows/terraform-diff.yaml:21- Round-2 claimed to retarget CI pointers to section 9, but live CI docs still send operators to section 8 (now the LiteLLM credential hop): terraform-diff.yaml header/comments/warning (lines 21, 77, 132, 145), validate.yaml header/job comment (lines 27, 239), .github/workflows/README.md (~162), scripts/ci/tofu-validate.sh (lines 8, 26), and the embedded warning string in scripts/ci/terraform-ci-workflows-test.py (line 276) which must stay byte-identical to the workflow. Retarget every CI/live-plan reference to section 9; leave true credential-hop refs (outputs.tofu, pushsecret-sso) on section 8.terraform/authentik/applications.tofu:1- Present-tense inventory still counts the pre-removal surface after open-webui was dropped: applications.tofu header says "The three OIDC applications" (only coder + pg-admin remain), variables.tofu says "The three client_id variables below" (line 19; only coder_client_id + pgadmin_client_id), docs/authentik/terraform.md section 9 still says AUTHENTIK_TOKEN/the three client ids (line 657), and the Never-run-destroy blurb still says it "deletes four applications and four providers" (line 587) even though destroy now also removes create-only LiteLLM resources and no longer covers open-webui. Update present-tense counts; keep dated 2026-08-26 historical inventory wording as-is.scripts/ci/terraform-ci-workflows-test.py:462- test_runbook_section8_contract still adds source-content-only greps of runbook prose for AWS_ENDPOINT_URL_S3 and the RGW Service DNS (lines 462-463). The executable contract is already asserted against secrets.vals.yaml / secrets-ci.vals.yaml / backend.tofu in both this file and tofu-authentik-stack-test.py. Drop the new runbook substring checks; keep semantic vals/HCL endpoint coupling only.terraform/tofu.md:34- Convention still says each secrets.vals.yaml holds only ref+op:// references, but authentik now carries a deliberate plain AWS_ENDPOINT_URL_S3 (local port-forward vs CI Service DNS). Soften the absolute claim so the next stack does not treat the plain endpoint as a style violation.🔧 Fix: Retarget CI xrefs; fix inventory counts
2 warnings still open:
terraform/authentik/imports.tofu:3- imports.tofu header still claims present-tense "Every resource it declares is therefore paired with an import block" and frames a first plan without imports as "9 creates" (lines 3-8). LiteLLM is deliberate create-only with no import (litellm.tofu), and the live import surface is now 7 adopted objects after open-webui removal. The runbook section 2 claim was already scoped to adopted resources; this HCL header still fights that model and can push someone to add import blocks for create-path resources. Scope it to adopted objects and note create-only LiteLLM separately.scripts/ci/terraform-ci-workflows-test.py:438- test_runbook_section8_contract (retargeted to section 9) is still pure source-content greps of runbook prose: required substring list OP_CONNECT_TOKEN/secrets-ci.vals.yaml/terraform-diff.yaml/etc. (lines 446-458) plus fork/reuse phrase checks. Endpoint greps were removed, but the remaining assertions still only prove docs wording, not CI behavior. The executable contract is already covered by workflow YAML semantic checks and secrets-ci/vals coupling in this same file. Drop the prose greps (or the whole test) rather than keeping a docs-wording tripwire.🔧 Fix: Fix imports header; drop runbook prose test
3 issues (2 warnings, 1 info) still open:
terraform/tofu.md:104- Commands section still sends live read-only CI plans to docs/authentik/terraform.md §8, but §8 is now the LiteLLM credential hop and CI lives in §9. Same class of misdirection fixed elsewhere (workflows, secrets-ci, AGENTS.md). Retarget this CI pointer to §9; leave genuine credential-hop refs on §8.docs/reference.md:25- Docs index still describes authentik/terraform.md as "CI read-only plans (§8)". This PR renumbered CI to section 9 and made §8 the credential hop, so the index now points operators at the wrong section. Update to §9 for CI (keep §7 for the apply gate).terraform/tofu.md:55- File-organization table still says secrets.vals.yaml is "references only", while ground rule 4 already documents the deliberate plain AWS_ENDPOINT_URL_S3 exception. Soften the table cell so it does not reassert the absolute claim.🔧 Fix: Retarget remaining CI docs xrefs to §9
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
python3 scripts/ci/litellm-sso-test.py(live OIDC discovery, PROXY_BASE_URL/redirect coupling, ExternalSecret/PushSecret path, echo ExtAuth scopes, UI SSO state, kustomize SSO surface)python3 scripts/ci/tofu-authentik-stack-test.py(tofu validate, workflow shape, HCL model, Automation ClusterSecretStore, delivered SSO contract including open-webui removal and flow uuid)python3 scripts/ci/terraform-ci-workflows-test.py(terraform-diff/publish workflow contracts and credentialless validate path)curllive public surface checks against auth.sklab.dev litellm OIDC, echo.sklab.dev ExtAuth, litellm.sklab.dev UI, and litellm end-session✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.