From 071123ae43c2c66debff8acba4753abbae8696c5 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Sat, 29 Aug 2026 15:05:42 -0500 Subject: [PATCH] fix(headlamp): rename OIDC secret keys to OIDC_* so chart args expand Headlamp OIDC login failed with `unsupported protocol scheme ""`. The kubernetes-sigs/headlamp chart `envFrom`s the headlamp-oidc Secret verbatim and renders args like `-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)`, so the Secret keys must be the exact env var names. The keys were camelCase (clientID/issuerURL/scopes/callbackURL/clientSecret), following the mistaken `.example`, so `OIDC_ISSUER_URL` was never defined. Kubernetes left the literal `$(OIDC_ISSUER_URL)` in the arg and Headlamp's OIDC discovery hit `Get "$(OIDC_ISSUER_URL)/.well-known/openid-configuration": unsupported protocol scheme ""`. Rename the SOPS Secret keys to OIDC_CLIENT_ID / OIDC_CLIENT_SECRET / OIDC_ISSUER_URL / OIDC_SCOPES / OIDC_CALLBACK_URL (values unchanged, still SOPS/age-encrypted). Fix the `.example` and README so the correct, load-bearing key names are documented and the camelCase footgun can't recur. Co-Authored-By: Claude Opus 4.8 --- platform/headlamp/README.md | 7 +++++- .../headlamp-oidc-credentials.sops.yaml | 24 +++++++++--------- ...eadlamp-oidc-credentials.sops.yaml.example | 25 +++++++++++++------ platform/headlamp/helmrelease.yaml | 10 +++++--- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/platform/headlamp/README.md b/platform/headlamp/README.md index 8fc0012..f2972aa 100644 --- a/platform/headlamp/README.md +++ b/platform/headlamp/README.md @@ -39,8 +39,13 @@ This keeps the apiserver OIDC config untouched (preferred over widening `--api-a [`../keycloak/keycloak-realm-clients.sops.yaml`](../keycloak/keycloak-realm-clients.sops.yaml) (`headlamp-client-secret`, injected into the realm import via the `HEADLAMP_CLIENT_SECRET` placeholder) and mirrored in this namespace's `headlamp-oidc-credentials.sops.yaml` - (`clientSecret`), which the chart `config.oidc.externalSecret` envFrom's. Same cross-namespace + (`OIDC_CLIENT_SECRET`), which the chart `config.oidc.externalSecret` envFrom's. Same cross-namespace mirror pattern as grafana/gitea. +- **Key names are load-bearing.** The chart `envFrom`s this Secret verbatim, so its keys become the + container env vars referenced by the chart's OIDC args (`-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)`, + etc.). They must be exactly `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ISSUER_URL`, `OIDC_SCOPES` + (and `OIDC_CALLBACK_URL`) — **not** camelCase. A misnamed key leaves `$(OIDC_ISSUER_URL)` + unexpanded and OIDC login fails with `unsupported protocol scheme ""`. ## Break-glass diff --git a/platform/headlamp/headlamp-oidc-credentials.sops.yaml b/platform/headlamp/headlamp-oidc-credentials.sops.yaml index 88e99bd..c266222 100644 --- a/platform/headlamp/headlamp-oidc-credentials.sops.yaml +++ b/platform/headlamp/headlamp-oidc-credentials.sops.yaml @@ -5,11 +5,11 @@ metadata: namespace: headlamp type: Opaque stringData: - clientID: ENC[AES256_GCM,data:PXrq7Qc830U=,iv:t/cXsXYyQGT7zDUL2LT0cJX/B0a69Efr9wz//wacEzw=,tag:Pnqx2MQvebI6mHXTfd4IvQ==,type:str] - issuerURL: ENC[AES256_GCM,data:A6YtkQCpqwEeDInEKZgW0E/7WiNiVHxR2rvcv0ZWuuy25N1v9xOMAiaELS55DvJF+JvMiyc=,iv:9VVQoAz27X8eW1XsSUdZAY9tdlSnCfXHnYqYH5/N5gs=,tag:gLZGYm4n1pxAT0EezWY0sg==,type:str] - scopes: ENC[AES256_GCM,data:bps1T/YTCRN44u+ERVP2vJ2r1PE=,iv:4+6KdXbfVQUp4NYgAkHFRyIpcr9YoQw91G8o/UmEDTA=,tag:bs0JKsveYekxHB45d0Qk0g==,type:str] - callbackURL: ENC[AES256_GCM,data:ViCO0/9pAxzIN3wzQ7MeOJaZEc3yddQ56SD7/ie3IaL0LP7zkjSIlkZ2hP9wJMqokmETCA==,iv:1X+DbHxnN4yEFzQBjIymOfGPGRbFA+OlJ+bUZ1ZLD0Q=,tag:3UAcnbXdoN0Fed74zNg5bg==,type:str] - clientSecret: ENC[AES256_GCM,data:SXzue9IzhyNsr2MFxDfEBNCgOTL6XLYqv0E0Rl6YOO62+B0b0V8dVmLKjBsK7VS+P6pXJQNZNDfFfRFC/3f9aA==,iv:sChj2ZDG2XapK7G9xIZ4qH8PvJbSjWVQqy3CEoD8LDU=,tag:3y4EbrlFt1EmzAadMTBN4g==,type:str] + OIDC_CLIENT_ID: ENC[AES256_GCM,data:dXAzoFfar4A=,iv:dHOJFcS6tpvtJes1c++nU6BbHyRlmGuLpncucbkJ4xA=,tag:lFI+KJPid2f/2r/ZjYlFTg==,type:str] + OIDC_ISSUER_URL: ENC[AES256_GCM,data:Er1ljhH62WahtEJkNvxk1BA2jz28OOCZmmyjkbVN82e/TFM7O+FB/FPjWUaW3lhd+dK16ro=,iv:Z3aP7+T+ytItYpbxjoYymLNGa7m8KGic4WqIQrrBSGE=,tag:/Rvhp5+r7AXIj2uDgL/dPg==,type:str] + OIDC_SCOPES: ENC[AES256_GCM,data:WoGaQKY9BFsi5/kDlRMXatINtmE=,iv:kTN+C176I8o5ftzZiKR10s9gl9VvBsOCJhFIZzj9gm4=,tag:ZyRIykD1h5YBq4grd6FB/w==,type:str] + OIDC_CALLBACK_URL: ENC[AES256_GCM,data:SZdpxEuW1BJccLyRGjuIDWBu6KpySARv/6M0mWtzc9jS+v64Lgr4VWKwgW0tdZrH5jM9rw==,iv:UbLwgfz7J0SIWbcQ4csqxv5B3enTmfHJzSDw/lmqAEI=,tag:lge2AuoQXK/u3uZbWvGnLQ==,type:str] + OIDC_CLIENT_SECRET: ENC[AES256_GCM,data:r0pb73gat8E7BFLPbdhguLh8G5PUT7PlJJeIcv7QPdMnN4iIB8MedMwxd77DkbxHSR7bav2ohQczjnbJBA4tPA==,iv:O0RbMFz4UD1sy8mbY7SXWCSGNF+lapDTL7tpphEcx7c=,tag:bEyuEs/lsK28fJeYDpYiOg==,type:str] sops: kms: [] gcp_kms: [] @@ -19,14 +19,14 @@ sops: - recipient: age175l8mdpdfu7hq0aml3exs3gykarwjmuvr23sq56s3wjejnyk44wsp4mfzg enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3RjJ5MGJ1cEJxMXRXTkZ5 - YURYaU5LTk1rT0kvOE5xMTZMcDVmVjA0TTFVCm1rTzBYaktSVE02TGdKZjBCaEda - cFQzZjZUTmZEWUs1cTljZk5NUDdRSjgKLS0tIFBjNXlNbFhSMy9yRm1TOE0yeUxr - dXlZUHdoVWpDS2I3TFBsOVdxc0NadDAKqemweg0FDcUxc93I8t6+o708TWjseh6f - W0r+NjeEYwcb9iJBGqkO04jP6Z0+QGFdAuQZB8w8AsPYMNTRNTwDwg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1UDV1RzRWeGZvdWxFQlgz + dCtjTU8wZzR5Tks5dmYreFliREZGaDdXMVVjCkVPY2lZby9LNzJMelJkTlE1Yks2 + cXN5YmhlaUtwN3ZjQUo5R0xnQWNQYm8KLS0tIDBMSUMxZ1pHaDRnUWs5RGtRNlhI + N0I4ekhqMWVWUFZoeGU4SERpYXVDQUkKUUwY6ZxEiWGdqV8s3wbnbPF9SkZJoxfy + 3MkNQ7rZHSILFTPVaZSErNTI21vqQr3lG24+yKLfNhAZBNyB5lvedQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-08-28T16:39:25Z" - mac: ENC[AES256_GCM,data:QtrqHMgaldW05Zag5AuH44+zBcyDJXTWhnuDVoV6UveMGSep/u4L/paMvA6ubuyZN07qfEv0SZVgybFh2EWKw/7dhE4Wbp0sttnrZ62J3TTnE3U6y9MbkLIuaM5+19Wfi7xVQ9Ovb9c1gELgEpCIo5KDSquA3yEvu1JBwHwmA2s=,iv:CAR7LqRItL5czUeCmr1QtTKlEqAFoiHBtHq24e6zNZs=,tag:YAciIDYGryK8kXQpay+etg==,type:str] + lastmodified: "2026-08-29T20:04:33Z" + mac: ENC[AES256_GCM,data:Haj8wMqDuIdtBopP02L1Z63+T8f0fcuSDpF5jnjgCB1+uzX+d4ylqpcVP5r8Ug8M/2ay2nYWoJBWzL0Y1iHcnDP7/T60M8hT46cBYPrDh3MqY0rWF5GwpjJxL9tStoSlDTYICeilAPqo2Vw4UM1CqnZv+th40rz5/R4MwRsU04w=,iv:5HPvtnF6nnDYDaT84r1JWf+ZH1X5/L1Aiim2lgOucH4=,tag:VM/2FXNW7sdjCk/k2AVl+g==,type:str] pgp: [] encrypted_regex: ^(data|stringData)$ version: 3.9.4 diff --git a/platform/headlamp/headlamp-oidc-credentials.sops.yaml.example b/platform/headlamp/headlamp-oidc-credentials.sops.yaml.example index b0f2d18..b35f57e 100644 --- a/platform/headlamp/headlamp-oidc-credentials.sops.yaml.example +++ b/platform/headlamp/headlamp-oidc-credentials.sops.yaml.example @@ -4,9 +4,11 @@ # # WHAT THIS IS: the OIDC config Headlamp uses to authenticate against the Keycloak `homelab` realm's # `headlamp` client. The Headlamp chart's `config.oidc.externalSecret` (helmrelease.yaml) envFrom's -# THIS Secret, so ALL OIDC env (clientID/clientSecret/issuerURL/scopes/callbackURL) live here, -# SOPS-encrypted, referenced by name only (R-IDP-04). The chart maps these keys to the container's -# OIDC_CLIENT_ID / OIDC_CLIENT_SECRET / OIDC_ISSUER_URL / OIDC_SCOPES / OIDC_CALLBACK_URL env vars. +# THIS Secret, so ALL OIDC env (OIDC_CLIENT_ID/OIDC_CLIENT_SECRET/OIDC_ISSUER_URL/OIDC_SCOPES/OIDC_CALLBACK_URL) live here, +# SOPS-encrypted, referenced by name only (R-IDP-04). The chart `envFrom`s this Secret VERBATIM, so +# the KEYS ARE the container env var names — they MUST be exactly OIDC_CLIENT_ID / OIDC_CLIENT_SECRET +# / OIDC_ISSUER_URL / OIDC_SCOPES / OIDC_CALLBACK_URL (the chart's `-oidc-*` args reference those +# names via $(...)). There is NO camelCase→OIDC_ mapping; a mismatched key breaks OIDC login. # # The `clientSecret` value MUST equal the `headlamp-client-secret` placed in # platform/keycloak/keycloak-realm-clients.sops.yaml (which the realm import injects into the @@ -27,10 +29,17 @@ metadata: namespace: headlamp type: Opaque stringData: + # KEY NAMES ARE LOAD-BEARING. The chart does NOT rename these — it `envFrom`s this Secret + # verbatim, so each key becomes a container env var of the SAME name, and the chart's OIDC args + # reference them by exact name: `-oidc-client-id=$(OIDC_CLIENT_ID)`, + # `-oidc-client-secret=$(OIDC_CLIENT_SECRET)`, `-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)`, + # `-oidc-scopes=$(OIDC_SCOPES)`. If a key is misnamed (e.g. camelCase `issuerURL`), the env var + # `OIDC_ISSUER_URL` is undefined, K8s leaves the literal string `$(OIDC_ISSUER_URL)` in the arg, + # and Headlamp's OIDC discovery fails with `unsupported protocol scheme ""`. Use these EXACT keys. # Non-secret OIDC config (kept in the encrypted Secret because the chart envFrom's a single Secret). - clientID: headlamp - issuerURL: https://keycloak.home.keithjackson.dev/realms/homelab - scopes: "openid email profile" - callbackURL: https://headlamp.home.keithjackson.dev/oidc-callback + OIDC_CLIENT_ID: headlamp + OIDC_ISSUER_URL: https://keycloak.home.keithjackson.dev/realms/homelab + OIDC_SCOPES: "openid email profile" + OIDC_CALLBACK_URL: https://headlamp.home.keithjackson.dev/oidc-callback # The confidential client secret — MUST match keycloak-realm-clients headlamp-client-secret. - clientSecret: REPLACE-ME + OIDC_CLIENT_SECRET: REPLACE-ME diff --git a/platform/headlamp/helmrelease.yaml b/platform/headlamp/helmrelease.yaml index b462c6d..4021de4 100644 --- a/platform/headlamp/helmrelease.yaml +++ b/platform/headlamp/helmrelease.yaml @@ -58,10 +58,12 @@ spec: config: inCluster: true oidc: - # OIDC config (clientID / clientSecret / issuerURL / scopes / callbackURL) is supplied via an - # EXISTING SOPS-encrypted Secret consumed by name only (R-IDP-04) — the chart envFrom's it. - # (The chart's `externalSecret` path reads OIDC_* env from this Secret; nothing sensitive is - # rendered into the HelmRelease.) + # OIDC config (OIDC_CLIENT_ID / OIDC_CLIENT_SECRET / OIDC_ISSUER_URL / OIDC_SCOPES / + # OIDC_CALLBACK_URL) is supplied via an EXISTING SOPS-encrypted Secret consumed by name only + # (R-IDP-04) — the chart envFrom's it. The Secret keys ARE the env var names (no renaming), + # and the chart's `-oidc-*` args reference them via $(...), e.g. + # `-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)`; a camelCase key would leave that arg unexpanded + # and break OIDC login (`unsupported protocol scheme ""`). Nothing sensitive is rendered here. externalSecret: enabled: true name: headlamp-oidc