Files
helm-charts-hardened/tests/unit/spire_test.go
T
savitha-qsandSavitha Ganapathi 80705999dd feat(spire-server): support x509pop externalPKI ca bundle (#908)
* feat(spire-server): support x509pop externalPKI ca bundle

Add externalPKI mode support to the x509pop node attestor configuration.
Allows operators to configure CA bundles for external PKI-based node
attestation via two approaches:
- Inline PEM content (chart creates and manages ConfigMap)
- Reference to existing ConfigMap with ca-bundle.pem key

Includes volume/volumeMount definitions for CA bundle mounting at
/run/spire/data/x509pop-ca-bundle.pem and unit tests for both modes.

Signed-off-by: Savitha Ganapathi <[email protected]>

* refactor: simplify x509pop externalPKI template guard logic

Remove nested conditional guard for ca_bundle_path rendering. When
externalPKI mode is enabled, ca_bundle_path is always rendered; if no
CA bundle is provided, SPIRE will fail at startup with a clear error.

Drop unit tests pending fix to the unit test framework (which currently
has issues loading values from chart, forcing overly-defensive template
guards for test compatibility). Tests can be re-added once framework is
fixed.

Signed-off-by: Savitha Ganapathi <[email protected]>

* refactor: simplify x509pop volume/volumeMount guard logic

Remove nested caBundle existence checks from volume and volumeMount
guard conditions. When externalPKI mode is enabled, volume/volumeMount
are created; if no CA bundle is provided, SPIRE fails at startup with
clear error (missing mount).

Signed-off-by: Savitha Ganapathi <[email protected]>

* refactor: reorder if/with clauses for clarity

Move if condition checks to outer scope before entering with blocks.
This is more idiomatic Helm pattern and avoids unnecessary context
switching if condition fails.

Signed-off-by: Savitha Ganapathi <[email protected]>

* refactor: simplify conditionals to match chart patterns

Replace complex toString/eq comparisons with simpler boolean checks that
match existing patterns in the chart (e.g., federation.tls.certManager.enabled).

Changes:
- .enabled checks: remove toString wrapping, use simple boolean test
- .mode checks: remove toString, use simple eq comparison
- .caBundle checks: simplify from 'ne (... | default "") ""' to simple boolean test

This aligns with chart conventions and avoids tripping broken unit test
framework that struggles with complex conditionals.

Signed-off-by: Savitha Ganapathi <[email protected]>

* test: resurrect x509POP unit tests with simplified conditionals

Re-add unit tests for externalPKI mode now that template conditionals
have been simplified to match chart patterns. Simplified conditionals
should be less fragile with unit test framework.

Tests cover:
- externalPKI with chart-managed CA bundle (inline)
- externalPKI with existing ConfigMap reference

Signed-off-by: Savitha Ganapathi <[email protected]>

* docs: regenerate spire-server README for x509pop caBundle params

Updated parameter documentation for nodeAttestor.x509POP section to
include new caBundle configuration options (inline bundle and existing
ConfigMap reference).

Auto-generated documentation based on @param comments in values.yaml.

Signed-off-by: Savitha Ganapathi <[email protected]>

---------

Signed-off-by: Savitha Ganapathi <[email protected]>
Co-authored-by: Savitha Ganapathi <[email protected]>
2026-08-06 16:48:09 -07:00

369 lines
12 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
}
merged, err := helmutil.CoalesceValues(chart, v)
if err != nil {
return nil, err
}
testChart := *chart
testChart.Values = merged
var activeDeps []*helmchart.Chart
for _, dep := range testChart.Dependencies() {
if dep.Name() != "spire-identity-exchange" {
activeDeps = append(activeDeps, dep)
}
}
testChart.SetDependencies(activeDeps...)
ro := helmutil.ReleaseOptions{Name: "spire", Namespace: "spire-server", Revision: 1, IsUpgrade: false, IsInstall: true}
v, err = helmutil.ToRenderValues(&testChart, merged, ro, helmutil.DefaultCapabilities)
if err != nil {
return nil, err
}
objs, err := helmengine.Render(&testChart, 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-server.UpstreamAuthority.ejbca", func() {
It("plugin set ok", func() {
objs, err := ValueStringRender(chart, `
spire-server:
upstreamAuthority:
ejbca:
enabled: true
hostname: ejbca.example.org:8443
caName: SpireIntermediateCA
endEntityProfileName: SpireEEP
certificateProfileName: SpireIntermediateCACP
secret:
data:
caCert: dummy-ca
`)
Expect(err).Should(Succeed())
notes := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(notes).Should(ContainSubstring("\"ejbca\": {"))
Expect(notes).Should(ContainSubstring("SpireIntermediateCA"))
Expect(notes).Should(ContainSubstring("ca_cert_path"))
})
})
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.x509POP", func() {
It("renders externalPKI mode with chart-managed ca bundle", func() {
objs, err := ValueStringRender(chart, `
spire-server:
nodeAttestor:
k8sPSAT:
enabled: false
x509POP:
enabled: true
mode: externalPKI
caBundle:
bundle: |
-----BEGIN CERTIFICATE-----
MIIB...
-----END CERTIFICATE-----
`)
Expect(err).Should(Succeed())
serverCM := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(serverCM).Should(ContainSubstring(`"mode": "external_pki"`))
Expect(serverCM).Should(ContainSubstring(`"ca_bundle_path": "/run/spire/data/x509pop-ca-bundle.pem"`))
Expect(objs).Should(HaveKey("spire/charts/spire-server/templates/x509pop-configmap.yaml"))
serverResource := objs["spire/charts/spire-server/templates/server-resource.yaml"]
Expect(serverResource).Should(ContainSubstring("x509pop-ca-bundle"))
Expect(serverResource).Should(ContainSubstring("/run/spire/data/x509pop-ca-bundle.pem"))
})
It("renders externalPKI mode with existing ConfigMap reference", func() {
objs, err := ValueStringRender(chart, `
spire-server:
nodeAttestor:
k8sPSAT:
enabled: false
x509POP:
enabled: true
mode: externalPKI
caBundle:
existingConfigMap: my-enrollment-ca
`)
Expect(err).Should(Succeed())
serverCM := objs["spire/charts/spire-server/templates/configmap.yaml"]
Expect(serverCM).Should(ContainSubstring(`"mode": "external_pki"`))
Expect(serverCM).Should(ContainSubstring(`"ca_bundle_path": "/run/spire/data/x509pop-ca-bundle.pem"`))
Expect(objs["spire/charts/spire-server/templates/x509pop-configmap.yaml"]).ShouldNot(ContainSubstring("kind: ConfigMap"))
serverResource := objs["spire/charts/spire-server/templates/server-resource.yaml"]
Expect(serverResource).Should(ContainSubstring("name: my-enrollment-ca"))
})
})
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"`))
})
})
Describe("spire-server.kubeConfigs", func() {
secretTmpl := "spire/charts/spire-server/templates/kubeconfig-secret.yaml"
serverTmpl := "spire/charts/spire-server/templates/server-resource.yaml"
It("inline entry generates a Secret and a projected volume source referencing it", func() {
objs, err := ValueStringRender(chart, `
spire-server:
kubeConfigs:
clustera:
kubeConfig: |
apiVersion: v1
kind: Config
`)
Expect(err).Should(Succeed())
Expect(objs[secretTmpl]).Should(ContainSubstring("kind: Secret"))
Expect(objs[serverTmpl]).Should(ContainSubstring("projected:"))
Expect(objs[serverTmpl]).Should(ContainSubstring("path: clustera"))
})
It("externalSecret entry wires a projected source and skips the generated Secret", func() {
objs, err := ValueStringRender(chart, `
spire-server:
kubeConfigs:
clusterb:
externalSecret:
name: my-ext-secret
`)
Expect(err).Should(Succeed())
Expect(objs[secretTmpl]).ShouldNot(ContainSubstring("kind: Secret"))
Expect(objs[serverTmpl]).Should(ContainSubstring("name: my-ext-secret"))
Expect(objs[serverTmpl]).Should(ContainSubstring("path: clusterb"))
})
})
Describe("spire-server.externalServerSubject", func() {
It("binds the external server's downstream RBAC to a ServiceAccount subject", func() {
objs, err := ValueStringRender(chart, `
spire-server:
externalServer: true
externalServerSubject:
kind: ServiceAccount
name: spire-external
namespace: spire-ext
`)
Expect(err).Should(Succeed())
roles := objs["spire/charts/spire-server/templates/roles.yaml"]
Expect(roles).Should(ContainSubstring("kind: ServiceAccount"))
Expect(roles).Should(ContainSubstring(`name: "spire-external"`))
Expect(roles).Should(ContainSubstring(`namespace: "spire-ext"`))
})
It("binds the external server's downstream RBAC to a Group subject", func() {
objs, err := ValueStringRender(chart, `
spire-server:
externalServer: true
externalServerSubject:
kind: Group
name: spire-admins
`)
Expect(err).Should(Succeed())
roles := objs["spire/charts/spire-server/templates/roles.yaml"]
Expect(roles).Should(ContainSubstring("apiGroup: rbac.authorization.k8s.io"))
Expect(roles).Should(ContainSubstring("kind: Group"))
Expect(roles).Should(ContainSubstring(`name: "spire-admins"`))
})
})
})