From 4f8ac5af060b1609cc0217bf3fb97cc88ca9b6a4 Mon Sep 17 00:00:00 2001 From: Guillermo Gaston Date: Sat, 13 Jun 2026 21:25:43 -0400 Subject: [PATCH] Configure jwt_issuer in SPIRE OIDC Provider (#829) The SPIRE OIDC Discovery Provider binary supports a top-level `jwt_issuer` configuration key. When set, the provider returns that exact string as the `issuer` field in the OIDC discovery document (`.well-known/openid-configuration`) regardless of how the request was routed. When unset, it derives `issuer` from the inbound HTTP Host header. This chart did not render that key into the OIDC provider config, leaving the discovery document Host-derived even when the operator had a fixed issuer in mind. Why this matters OpenID Connect Discovery requires the discovery doc's `issuer` to be byte-equal to the JWT `iss` claim. Conformant verifiers reject the chain when the two differ. Production OIDC consumers routinely reach the discovery endpoint at a URL different from the canonical issuer: - a load balancer, ingress, or NodePort exposes the provider on an IP or host different from the canonical issuer name; - TLS terminates at a hostname different from the one advertised to clients; - the discovery URL is fetched by an internal service (e.g. the API server in a private cluster) over a different DNS view than external clients use; - a pinned issuer URL is contractually required and must survive infrastructure changes that move the actual service endpoint. In all of these the JWT's `iss` claim is a logical, stable URL; the discovery doc must report that same value, or downstream verifiers reject the tokens. Current chart behavior The chart already has `global.spire.jwtIssuer` (and a subchart-local `jwtIssuer`), resolved by the `spire-lib.jwt-issuer` helper to `global.spire.jwtIssuer` -> subchart-local `jwtIssuer` -> `https://oidc-discovery.`. The spire-server subchart writes that helper's result unconditionally as `jwt_issuer:` into the server's config -- this controls the `iss` claim of every JWT-SVID the server mints. In the OIDC subchart, however, the same helper was only used for two things: 1. as the strict-mode assertion gate (fails the render when the resolved value is the `example.org` default); 2. as the default source for `config.jwtDomain` (the Host allow-list). It was never written into the rendered OIDC provider configuration file. The asymmetry means the chart shipped a structurally invalid OIDC setup by default: the spire-server signs JWTs with `iss = `, while the OIDC discovery endpoint advertises whatever Host header was used to reach it. The only way to correct that today is to patch the rendered ConfigMap out of band (`kubectl patch`, a CMP, a kustomize post-renderer), which defeats the purpose of the chart. Backward compatibility and behavior changes Operators who set `global.spire.jwtIssuer` will see one additional `jwt_issuer:` line in the rendered OIDC ConfigMap. The discovery doc's `issuer` will start returning that pinned value instead of being Host-derived, bringing the chain into spec compliance; this is a fix for any spec-compliant verifier that previously rejected tokens. Operators who only set the OIDC subchart-local `jwtIssuer` see the same fix applied via the helper's fallback chain. Operators with nothing set will see the new line default to `https://oidc-discovery.`, matching what the spire-server config already emits today. Signed-off-by: Guillermo Gaston Co-authored-by: kfox1111 --- charts/spire/README.md | 6 +++- .../spiffe-oidc-discovery-provider/README.md | 2 +- .../templates/configmap.yaml | 2 ++ .../values.yaml | 2 +- charts/spire/values.yaml | 2 +- tests/unit/spire_test.go | 31 +++++++++++++++++++ 6 files changed, 41 insertions(+), 4 deletions(-) diff --git a/charts/spire/README.md b/charts/spire/README.md index 5716fa7..1da10ab 100644 --- a/charts/spire/README.md +++ b/charts/spire/README.md @@ -79,6 +79,10 @@ kubectl delete crds clusterfederatedtrustdomains.spire.spiffe.io clusterspiffeid We only support upgrading one major/minor version at a time. Version skipping isn't supported. Please see for details. +### 0.30.X + +- The OIDC discovery issuer is now set automatically. We do not anticipate any negative impact; however, please verify your OIDC provider's integration with other services during your upgrade testing. + ### 0.26.X - The notifier.k8sBundle plugin has been deprecated in favor of bundlePublisher.k8sConfigMap. The only features it does not provide are the settings `apiServiceLabel` and `webhookLabel`. If you are using either of these two features, set the chart to use the notifier.k8sBundle plugin again, and let us know. We don't think anyone is using these features. @@ -277,7 +281,7 @@ Now you can interact with the Spire agent socket from your own application. The | `global.k8s.clusterDomain` | Cluster domain name configured for Spire install | `cluster.local` | | `global.spire.bundleConfigMap` | A configmap containing the Spire bundle | `""` | | `global.spire.clusterName` | The name of the k8s cluster for Spire install | `example-cluster` | -| `global.spire.jwtIssuer` | The issuer for Spire JWT tokens. Defaults to oidc-discovery.$trustDomain if unset | `""` | +| `global.spire.jwtIssuer` | Issuer URL used for both the JWT-SVID `iss` claim minted by spire-server and the `issuer` field advertised by the OIDC discovery document. Defaults to oidc-discovery.$trustDomain if unset | `""` | | `global.spire.trustDomain` | The trust domain for Spire install | `example.org` | | `global.spire.upstreamServerAddress` | Set what address to use for the upstream server when using nested spire | `""` | | `global.spire.caSubject.country` | Country for Spire server CA | `""` | diff --git a/charts/spire/charts/spiffe-oidc-discovery-provider/README.md b/charts/spire/charts/spiffe-oidc-discovery-provider/README.md index 05c6c63..fc5c9e4 100644 --- a/charts/spire/charts/spiffe-oidc-discovery-provider/README.md +++ b/charts/spire/charts/spiffe-oidc-discovery-provider/README.md @@ -79,7 +79,7 @@ A Helm chart to install the SPIFFE OIDC discovery provider. | `insecureScheme.nginx.image.tag` | Overrides the image tag whose default is the chart appVersion. Only used when TLS is disabled. | `1.30.0-alpine` | | `insecureScheme.nginx.ipMode` | IP modes supported by the cluster. Must be one of [ipv4, ipv6, both] | `both` | | `insecureScheme.nginx.resources` | Resource requests and limits | `{}` | -| `jwtIssuer` | Path to JWT issuer. Defaults to oidc-discovery.$trustDomain if unset | `""` | +| `jwtIssuer` | Issuer URL used for the `issuer` field advertised by the OIDC discovery document. Defaults to oidc-discovery.$trustDomain if unset | `""` | | `config.logLevel` | The log level, valid values are "debug", "info", "warn", and "error" | `info` | | `config.logFormat` | The log format, valid values are "text" and "json" | `text` | | `config.jwtDomain` | The JWT domain. Defaults to oidc-discovery.$jwtIssuer URL-parsed host if unset | `""` | diff --git a/charts/spire/charts/spiffe-oidc-discovery-provider/templates/configmap.yaml b/charts/spire/charts/spiffe-oidc-discovery-provider/templates/configmap.yaml index c4f9c1a..db56d8c 100644 --- a/charts/spire/charts/spiffe-oidc-discovery-provider/templates/configmap.yaml +++ b/charts/spire/charts/spiffe-oidc-discovery-provider/templates/configmap.yaml @@ -56,6 +56,8 @@ server_path_prefix: {{ .Values.config.serverPathPrefix | quote }} set_key_use: true {{- end }} +jwt_issuer: {{ include "spire-lib.jwt-issuer" . | quote }} + {{- if eq .Values.bundleSource "ConfigMap" }} file: path: /bundle/bundle.spiffe diff --git a/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml b/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml index 69f2179..5c1546f 100644 --- a/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml +++ b/charts/spire/charts/spiffe-oidc-discovery-provider/values.yaml @@ -203,7 +203,7 @@ insecureScheme: # cpu: 100m # memory: 64Mi -## @param jwtIssuer Path to JWT issuer. Defaults to oidc-discovery.$trustDomain if unset +## @param jwtIssuer Issuer URL used for the `issuer` field advertised by the OIDC discovery document. Defaults to oidc-discovery.$trustDomain if unset jwtIssuer: "" config: diff --git a/charts/spire/values.yaml b/charts/spire/values.yaml index ceca886..91172c3 100644 --- a/charts/spire/values.yaml +++ b/charts/spire/values.yaml @@ -17,7 +17,7 @@ global: bundleConfigMap: "" ## @param global.spire.clusterName The name of the k8s cluster for Spire install clusterName: example-cluster - ## @param global.spire.jwtIssuer The issuer for Spire JWT tokens. Defaults to oidc-discovery.$trustDomain if unset + ## @param global.spire.jwtIssuer Issuer URL used for both the JWT-SVID `iss` claim minted by spire-server and the `issuer` field advertised by the OIDC discovery document. Defaults to oidc-discovery.$trustDomain if unset jwtIssuer: "" ## @param global.spire.trustDomain The trust domain for Spire install trustDomain: example.org diff --git a/tests/unit/spire_test.go b/tests/unit/spire_test.go index 1c79419..97ffb36 100644 --- a/tests/unit/spire_test.go +++ b/tests/unit/spire_test.go @@ -189,4 +189,35 @@ spire-server: Expect(notes).Should(ContainSubstring("Installed")) }) }) + Describe("spiffe-oidc-discovery-provider.jwtIssuer", func() { + It("auto-derives jwt_issuer from global.spire.jwtIssuer and matches spire-server", func() { + objs, err := ValueStringRender(chart, ` +global: + spire: + jwtIssuer: https://canonical.example.com +`) + Expect(err).Should(Succeed()) + oidcCM := objs["spire/charts/spiffe-oidc-discovery-provider/templates/configmap.yaml"] + Expect(oidcCM).Should(ContainSubstring(`"jwt_issuer": "https://canonical.example.com"`)) + serverCM := objs["spire/charts/spire-server/templates/configmap.yaml"] + Expect(serverCM).Should(ContainSubstring(`"jwt_issuer": "https://canonical.example.com"`)) + }) + It("propagates the subchart-local jwtIssuer to jwt_issuer", func() { + objs, err := ValueStringRender(chart, ` +spiffe-oidc-discovery-provider: + jwtIssuer: https://legacy.example.com +`) + Expect(err).Should(Succeed()) + oidcCM := objs["spire/charts/spiffe-oidc-discovery-provider/templates/configmap.yaml"] + Expect(oidcCM).Should(ContainSubstring(`"jwt_issuer": "https://legacy.example.com"`)) + }) + It("defaults to oidc-discovery. when nothing is set and strict mode is disabled", func() { + objs, err := ValueStringRender(chart, ``) + Expect(err).Should(Succeed()) + oidcCM := objs["spire/charts/spiffe-oidc-discovery-provider/templates/configmap.yaml"] + Expect(oidcCM).Should(ContainSubstring(`"jwt_issuer": "https://oidc-discovery.example.org"`)) + serverCM := objs["spire/charts/spire-server/templates/configmap.yaml"] + Expect(serverCM).Should(ContainSubstring(`"jwt_issuer": "https://oidc-discovery.example.org"`)) + }) + }) })