Skip to content

OIDC federation for Talos

kubernetes/apps/network/oidc-talos publishes this cluster's OIDC discovery document and JSON Web Key Set so Microsoft Entra ID, or any other OIDC-aware cloud, can validate Kubernetes service-account tokens. Only two static JSON endpoints are exposed publicly; the apiserver itself is never reached from outside the cluster.

Cross-repo coupling

The apiserver's service-account-issuer flag, set in talos/patches/controller/cluster.yaml, must equal https://oidc-talos.${SECRET_DOMAIN}. Azure's federated credential, a terraform-entra variable named sentinel_syslog_oidc_issuer, must point at the same URL. The variable name is a holdover from PR #3239, which introduced the in-cluster syslog shipper to Microsoft Sentinel and this OIDC endpoint in the same change; the federated-identity variable kept the sentinel-syslog name even though other workloads use the same issuer now.

From a committed JWKS to a live fetch

The app originally shipped with the cluster's public signing keys committed straight into the ConfigMap, fetched by hand with kubectl get --raw /openid/v1/jwks when the app was built. PR #3263 replaced that with an init container, fetch-jwks, that populates an emptyDir from the live apiserver before nginx starts, and a native sidecar, refresh-jwks, that re-fetches hourly. A committed key block would be wrong after a fresh bootstrap that reuses the app's name, and it would go stale if the cluster's service-account signing key ever rotated. The live fetch avoids both. A failed refresh keeps serving the last JWKS it fetched successfully rather than failing closed.

A follow-up, PR #3265, turned on automountServiceAccountToken at the pod level. app-template disables the automount by default, so the fetch and refresh containers had no token or CA bundle to authenticate with, and crash-looped on the first deploy. The previous pod kept serving the JWKS throughout, because the rolling update held on the not-ready replacement, so the endpoint saw no downtime.

A dedicated ServiceAccount instead of the cluster-wide binding

rbac.yaml binds system:service-account-issuer-discovery to a ServiceAccount scoped to this app, rather than relying on the cluster's built-in system:serviceaccounts discovery binding. That keeps the fetch and refresh containers working even on a cluster bootstrapped from scratch, before any cluster-wide binding exists.

Flux postBuild and the inline shell scripts

The fetch and refresh scripts double every $ in their $(cat ...) command substitutions, because Flux's postBuild.substitute step runs as a text substitution over the whole rendered manifest and would otherwise treat $(...) as something to expand. The scripts also address the apiserver as kubernetes.default.svc rather than ${KUBERNETES_SERVICE_HOST}: writing that environment variable in ${...} form would look like an unset Flux substitution variable and get replaced with an empty string.