Files
helm-charts-hardened/tests/unit/spire_test.go
T
Guillermo Gastonandkfox1111 4f8ac5af06 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.<trustDomain>`. 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 = <resolved issuer>`,
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.<trustDomain>`,
matching what the spire-server config already emits today.

Signed-off-by: Guillermo Gaston <[email protected]>
Co-authored-by: kfox1111 <[email protected]>
2026-06-14 01:25:43 +00:00

224 lines
7.2 KiB
Go

package unit_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
helmchart "helm.sh/helm/v3/pkg/chart"
helmloader "helm.sh/helm/v3/pkg/chart/loader"
helmutil "helm.sh/helm/v3/pkg/chartutil"
helmengine "helm.sh/helm/v3/pkg/engine"
)
func ValueStringRender(chart *helmchart.Chart, values string) (map[string]string, error) {
v, err := helmutil.ReadValues([]byte(values))
if err != nil {
return nil, err
}
ro := helmutil.ReleaseOptions{Name: "spire", Namespace: "spire-server", Revision: 1, IsUpgrade: false, IsInstall: true}
v, err = helmutil.ToRenderValues(chart, v, ro, helmutil.DefaultCapabilities)
if err != nil {
return nil, err
}
objs, err := helmengine.Render(chart, v)
return objs, err
}
var _ = Describe("Spire", func() {
chart, err := helmloader.Load("../../charts/spire")
Expect(err).Should(Succeed())
Describe("spire-server.upstream.cert-manager", func() {
It("issuerName when set is passed through", func() {
objs, err := ValueStringRender(chart, `
spire-server:
upstreamAuthority:
certManager:
enabled: true
issuerName: abc123
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("abc123"))
})
})
Describe("spire-server.customPlugin.tpm", func() {
It("plugin set ok", func() {
objs, err := ValueStringRender(chart, `
spire-server:
customPlugins:
nodeAttestor:
tpm:
plugin_cmd: /bin/tpm_attestor_server
plugin_checksum: 97442358ae946e3fb8f2464432b8c23efdc0b5d44ec1eea27babe59ef646cc2f
plugin_data: {}
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("tpm"))
})
})
Describe("spire-server.unsupportedBuiltInPlugins", func() {
It("plugin set ok", func() {
objs, err := ValueStringRender(chart, `
spire-server:
unsupportedBuiltInPlugins:
nodeAttestor:
join_token:
plugin_data: {}
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("join_token"))
})
})
Describe("spire-server.keyManager.aws_kms", func() {
It("plugin set ok", func() {
objs, err := ValueStringRender(chart, `
spire-server:
keyManager:
awsKMS:
enabled: true
region: us-west-2
plugin_data: {}
disk:
enabled: false
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("\"aws_kms\": {"))
})
})
Describe("spire-server.UpstreamAuthority.aws_pca", func() {
It("plugin set ok", func() {
objs, err := ValueStringRender(chart, `
spire-server:
upstreamAuthority:
awsPCA:
enabled: true
region: us-west-2
plugin_data: {}
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("\"aws_pca\": {"))
})
})
Describe("spire-agent.customPlugin.tpm", func() {
It("plugin set ok", func() {
objs, err := ValueStringRender(chart, `
spire-agent:
nodeAttestor:
k8sPSAT:
enabled: false
customPlugins:
nodeAttestor:
tpm:
plugin_cmd: /bin/tpm_attestor_agent
plugin_checksum: bb7be714c27452231a6c7764b65912ce0cdeb66ff2a2c688d3e88bd0bd17d138
plugin_data: {}
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-agent/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("tpm"))
})
})
Describe("spire-server.unsupportedBuiltInPlugins", func() {
It("plugin set ok", func() {
objs, err := ValueStringRender(chart, `
spire-agent:
nodeAttestor:
k8sPSAT:
enabled: false
unsupportedBuiltInPlugins:
nodeAttestor:
join_token:
plugin_data: {}
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-agent/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("join_token"))
})
})
Describe("spire-server.disabled", func() {
It("spire server off", func() {
objs, err := ValueStringRender(chart, `
spire-server:
enabled: false
`)
Expect(err).Should(Succeed())
notes := objs["spire/templates/NOTES.txt"]
Expect(notes).Should(ContainSubstring("Installed"))
})
})
Describe("spire-server.nodeAttestor.awsIID.verifyOrganization", func() {
It("emits verify_organization in server config JSON", func() {
objs, err := ValueStringRender(chart, `
spire-server:
nodeAttestor:
k8sPSAT:
enabled: false
awsIID:
enabled: true
verifyOrganization:
enabled: true
managementAccountId: "111122223333"
assumeOrgRole: "spire-server-org-validator"
managementAccountRegion: "us-east-1"
orgAccountMapTTL: "5m"
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring(`verify_organization`))
Expect(notes).Should(ContainSubstring(`management_account_id`))
Expect(notes).Should(ContainSubstring(`111122223333`))
Expect(notes).Should(ContainSubstring(`spire-server-org-validator`))
Expect(notes).Should(ContainSubstring(`us-east-1`))
Expect(notes).Should(ContainSubstring(`5m`))
})
})
Describe("spire-server.credentialComposer.uniqueID", func() {
It("spire server uniqueid credential composer", func() {
objs, err := ValueStringRender(chart, `
spire-server:
credentialComposer:
uniqueID:
enabled: true
`)
Expect(err).Should(Succeed())
notes := objs["spire/templates/NOTES.txt"]
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.<trustDomain> 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"`))
})
})
})