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]>
This commit is contained in:
savitha-qs
2026-08-06 16:48:09 -07:00
committed by GitHub
co-authored by Savitha Ganapathi
parent 890ada3e15
commit 80705999dd
6 changed files with 93 additions and 2 deletions
+4 -1
View File
@@ -501,7 +501,10 @@ In order to run Tornjak with simple HTTP Connection only, make sure you don't cr
| `nodeAttestor.gcpIIT.metadataValueMaxSize` | Sets the maximum metadata value size considered by the plugin for selectors | `0` |
| `nodeAttestor.gcpIIT.agentPathTemplate` | A URL path portion format of Agent's SPIFFE ID. Describe in text/template format. | `""` |
| `nodeAttestor.x509POP.enabled` | Enable the x509_popg node attestor | `false` |
| `nodeAttestor.x509POP.mode` | What mode to set the plugin to. Currently only spiffe mode is supported | `spiffe` |
| `nodeAttestor.x509POP.mode` | Plugin mode: spiffe (exchange) or externalPKI (enrollment CA bundle) | `spiffe` |
| `nodeAttestor.x509POP.caBundle` | CA bundle for externalPKI mode. Provide inline PEM contents or reference an existing ConfigMap. | |
| `nodeAttestor.x509POP.caBundle.bundle` | PEM CA bundle contents. When set, the chart creates and mounts a ConfigMap. | `""` |
| `nodeAttestor.x509POP.caBundle.existingConfigMap` | Name of a ConfigMap containing a `ca-bundle.pem` key with the PEM CA bundle. | `""` |
| `nodeAttestor.x509POP.spiffePrefix` | What prefix to use when mode is spiffe | `/spire-exchange/k8s${HELM_ADD_CLUSTER_NAME}/` |
| `nodeAttestor.x509POP.agentPathTemplate` | Override the default agent path template | `""` |
| `nodeAttestor.x509POP.maxIntermediates` | Maximum number of intermediate certificates allowed in the certificate chain | `4` |
@@ -281,6 +281,12 @@ plugins:
{{- if or (eq (.enabled | toString) "true") $root.Values.spireIdentityExchange.enabled }}
x509pop:
plugin_data:
{{- if eq .mode "externalPKI" }}
mode: external_pki
ca_bundle_path: "/run/spire/data/x509pop-ca-bundle.pem"
max_intermediates: {{ .maxIntermediates }}
max_rsa_key_size: {{ .maxRSAKeySize }}
{{- else }}
mode: {{ .mode }}
spiffe_prefix: {{ include "spire-server.identity-exchange-spiffe-prefix" $root | quote }}
max_intermediates: {{ .maxIntermediates }}
@@ -298,6 +304,7 @@ plugins:
{{- $cn = printf "/%s" (include "spire-lib.cluster-name" $root) }}
{{- end }}
agent_path_template: {{ replace "${HELM_ADD_CLUSTER_NAME}" $cn $agentPathTemplate | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- with .Values.nodeAttestor.awsIID }}
@@ -408,6 +408,14 @@ spec:
mountPath: /tmp-direct-hashes
{{- end }}
{{- end }}
{{- if and .Values.nodeAttestor.x509POP.enabled (eq .Values.nodeAttestor.x509POP.mode "externalPKI") }}
{{- with .Values.nodeAttestor.x509POP }}
- name: x509pop-ca-bundle
mountPath: /run/spire/data/x509pop-ca-bundle.pem
subPath: ca-bundle.pem
readOnly: true
{{- end }}
{{- end }}
{{- if or .Values.federation.tls.certManager.enabled .Values.federation.tls.externalSecret.enabled }}
- name: bundle-endpoint-tls
mountPath: /bundle-endpoint-tls
@@ -646,6 +654,17 @@ spec:
name: {{ include "spire-server.fullname" . }}-tpm-direct-hash
{{- end }}
{{- end }}
{{- if and .Values.nodeAttestor.x509POP.enabled (eq .Values.nodeAttestor.x509POP.mode "externalPKI") }}
{{- with .Values.nodeAttestor.x509POP }}
- name: x509pop-ca-bundle
configMap:
{{- if .caBundle.bundle }}
name: {{ $fullname }}-x509pop-ca
{{- else if .caBundle.existingConfigMap }}
name: {{ .caBundle.existingConfigMap }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.federation.tls.certManager.enabled }}
- name: bundle-endpoint-tls
secret:
@@ -0,0 +1,10 @@
{{- if and .Values.nodeAttestor.x509POP.enabled .Values.nodeAttestor.x509POP.caBundle.bundle }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "spire-server.fullname" . }}-x509pop-ca
namespace: {{ include "spire-server.namespace" . }}
data:
ca-bundle.pem: |
{{ .Values.nodeAttestor.x509POP.caBundle.bundle | nindent 4 }}
{{- end }}
+7 -1
View File
@@ -1239,8 +1239,14 @@ nodeAttestor:
x509POP:
## @param nodeAttestor.x509POP.enabled Enable the x509_popg node attestor
enabled: false
## @param nodeAttestor.x509POP.mode What mode to set the plugin to. Currently only spiffe mode is supported
## @param nodeAttestor.x509POP.mode Plugin mode: spiffe (exchange) or externalPKI (enrollment CA bundle)
mode: spiffe
## @extra nodeAttestor.x509POP.caBundle CA bundle for externalPKI mode. Provide inline PEM contents or reference an existing ConfigMap.
caBundle:
## @param nodeAttestor.x509POP.caBundle.bundle [nullable] PEM CA bundle contents. When set, the chart creates and mounts a ConfigMap.
bundle: ""
## @param nodeAttestor.x509POP.caBundle.existingConfigMap [nullable] Name of a ConfigMap containing a `ca-bundle.pem` key with the PEM CA bundle.
existingConfigMap: ""
## @param nodeAttestor.x509POP.spiffePrefix What prefix to use when mode is spiffe
spiffePrefix: "/spire-exchange/k8s${HELM_ADD_CLUSTER_NAME}/"
## @param nodeAttestor.x509POP.agentPathTemplate Override the default agent path template
+46
View File
@@ -187,6 +187,52 @@ spire-server:
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, `