Add JWT-SVID exec-auth source for kubeConfigs entries (#907)

Add jwtSVIDExec as a fourth exactly-one kubeConfigs source: the chart
generates an exec-credential kubeconfig that authenticates to an external
cluster with short-lived SPIFFE JWT-SVIDs instead of a static credential.

Signed-off-by: sabsari <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
sabsari
2026-08-11 06:40:05 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent a481bab3f0
commit 648e0e45e5
7 changed files with 140 additions and 5 deletions
@@ -634,5 +634,11 @@ In order to run Tornjak with simple HTTP Connection only, make sure you don't cr
| `tests.bash.image.pullPolicy` | The image pull policy | `IfNotPresent` |
| `tests.bash.image.tag` | Overrides the image tag whose default is the chart appVersion | `latest@sha256:90041f375e30f41aa7e0390075d8a69dc61900771d52fa98d63ee5d03d866a58` |
| `kubeConfigs` | Manage additional kubeconfig files to talk to external Kubernetes clusters | `{}` |
| `jwtSVIDExecConfig.image.registry` | The OCI registry to pull the exec credential plugin image from | `ghcr.io` |
| `jwtSVIDExecConfig.image.repository` | The repository within the registry | `spiffe/k8s-spiffe-workload-jwt-exec-auth` |
| `jwtSVIDExecConfig.image.pullPolicy` | The image pull policy | `IfNotPresent` |
| `jwtSVIDExecConfig.image.tag` | Overrides the image tag | `0.2.0` |
| `jwtSVIDExecConfig.pluginPath` | The path of the plugin binary inside the plugin image, staged for exec by the kubeConfigs consumers | `/ko-app/cmd` |
| `jwtSVIDExecConfig.spiffeID` | The SPIFFE ID the plugin mints a JWT-SVID for; a full spiffe:// URI, or a "/"-prefixed path expanded with the chart trust domain (e.g. /spire-root); required when any kubeConfigs entry uses jwtSVIDExec | `/spire-root` |
| `spireIdentityExchange.enabled` | Enable the server side of the SPIRE Identity Exchange system | `false` |
| `spike.enabled` | Enable the server side of SPIKE | `false` |
@@ -173,6 +173,14 @@ Auto-generation preserves trailing numbers from cluster names or uses hash for u
subPath: {{ . }}
readOnly: true
{{- end }}
{{- if hasKey . "kubeConfig" }}
{{- $entry := default dict (index .Values.kubeConfigs .kubeConfig) }}
{{- if hasKey $entry "jwtSVIDExec" }}
- name: plugins
mountPath: /plugins
readOnly: true
{{- end }}
{{- end }}
- name: spire-controller-manager-tmp
mountPath: /tmp
subPath: {{ printf "spire-controller-manager%s" .suffix }}
@@ -186,6 +186,63 @@ Name of the chart-generated Secret holding the inline kubeConfigs entries.
{{ include "spire-server.fullname" . }}-kubeconfigs
{{- end }}
{{/*
Path of the staged jwt-svid exec plugin binary inside the shared plugins volume. Used both as the
init-container copy target and as the exec kubeconfig command, so the two must stay in sync.
*/}}
{{- define "spire-server.jwt-svid-exec-binary-path" -}}
/plugins/jwt-svid-exec
{{- end }}
{{- define "spire-server.jwt-svid-exec-kubeconfig" -}}
{{- $jwtSVIDExec := .jwtSVIDExec -}}
{{- $root := .root -}}
{{- $spiffeID := $root.Values.jwtSVIDExecConfig.spiffeID -}}
{{- if not $spiffeID -}}
{{- fail "jwtSVIDExecConfig.spiffeID is required when a kubeConfigs entry uses jwtSVIDExec" -}}
{{- end -}}
{{- $chartTD := include "spire-lib.trust-domain" $root -}}
{{- if hasPrefix "/" $spiffeID -}}
{{- $spiffeID = printf "spiffe://%s%s" $chartTD $spiffeID -}}
{{- else if hasPrefix "spiffe://" $spiffeID -}}
{{- $idTD := $spiffeID | trimPrefix "spiffe://" | splitList "/" | first -}}
{{- if ne $idTD $chartTD -}}
{{- fail (printf "jwtSVIDExecConfig.spiffeID trust domain %q must match the chart trust domain %q" $idTD $chartTD) -}}
{{- end -}}
{{- else -}}
{{- fail (printf "jwtSVIDExecConfig.spiffeID %q must be a spiffe:// URI or a path starting with \"/\"" $spiffeID) -}}
{{- end -}}
apiVersion: v1
kind: Config
clusters:
- name: cluster
cluster:
server: {{ $jwtSVIDExec.server | quote }}
certificate-authority-data: {{ $jwtSVIDExec.certificateAuthorityData | quote }}
users:
- name: spiffe
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: {{ include "spire-server.jwt-svid-exec-binary-path" $root }}
interactiveMode: Never
env:
- name: SPIFFE_JWT_SOURCE
value: "server-admin-api"
- name: SPIRE_SERVER_SOCKET
value: "unix:///tmp/spire-server/private/api.sock"
- name: SPIFFE_ID
value: {{ $spiffeID | quote }}
- name: SPIFFE_JWT_AUDIENCE
value: {{ $jwtSVIDExec.audience | default "k8s" | quote }}
contexts:
- name: cluster
context:
cluster: cluster
user: spiffe
current-context: cluster
{{- end }}
{{- define "spire-server.serviceAccountAllowedList" }}
{{- $releaseNamespace := include "spire-server.agent-namespace" . }}
{{- if ne (len .Values.nodeAttestor.k8sPSAT.serviceAccountAllowList) 0 }}
@@ -6,8 +6,9 @@
{{- if hasKey $value "kubeConfig" }}{{ $present = append $present "kubeConfig" }}{{- end }}
{{- if hasKey $value "kubeConfigBase64" }}{{ $present = append $present "kubeConfigBase64" }}{{- end }}
{{- if hasKey $value "externalSecret" }}{{ $present = append $present "externalSecret" }}{{- end }}
{{- if hasKey $value "jwtSVIDExec" }}{{ $present = append $present "jwtSVIDExec" }}{{- end }}
{{- if ne (len $present) 1 }}
{{- fail (printf "kubeConfigs entry %q must set exactly one of kubeConfig, kubeConfigBase64, or externalSecret (got: %v)" $name $present) }}
{{- fail (printf "kubeConfigs entry %q must set exactly one of kubeConfig, kubeConfigBase64, externalSecret, or jwtSVIDExec (got: %v)" $name $present) }}
{{- end }}
{{- if hasKey $value "externalSecret" }}
{{- if not $value.externalSecret.name }}
@@ -30,6 +31,8 @@ data:
{{- range $name, $value := $inline }}
{{- if hasKey $value "kubeConfig" }}
{{ $name }}: {{ $value.kubeConfig | b64enc }}
{{- else if hasKey $value "jwtSVIDExec" }}
{{ $name }}: {{ include "spire-server.jwt-svid-exec-kubeconfig" (dict "jwtSVIDExec" $value.jwtSVIDExec "root" $root) | b64enc }}
{{- else }}
{{ $name }}: {{ $value.kubeConfigBase64 | nospace }}
{{- end }}
@@ -65,7 +65,11 @@
{{- end }}
{{- end }}
{{- $pluginsToLoad := include "spire-lib.extract_custom_plugin_images" . | fromYamlArray }}
{{- $pluginLoaderNeeded := or .Values.credentialComposer.cel.enabled .Values.spireIdentityExchange.enabled (gt (len $pluginsToLoad) 0) }}
{{- $jwtExecNeeded := false }}
{{- range $name, $value := .Values.kubeConfigs }}
{{- if hasKey $value "jwtSVIDExec" }}{{ $jwtExecNeeded = true }}{{- end }}
{{- end }}
{{- $pluginLoaderNeeded := or .Values.credentialComposer.cel.enabled .Values.spireIdentityExchange.enabled (gt (len $pluginsToLoad) 0) $jwtExecNeeded }}
{{- if not .Values.externalServer }}
apiVersion: apps/v1
{{- if eq .Values.kind "statefulset" }}
@@ -176,6 +180,23 @@ spec:
mountPath: /plugins
imagePullPolicy: {{ .Values.credentialComposer.spireIdentityExchange.image.pullPolicy }}
{{- end }}
{{- if $jwtExecNeeded }}
- name: init-jwt-svid-exec
securityContext:
{{- include "spire-lib.securitycontext" . | nindent 12 }}
image: {{ template "spire-lib.image" (dict "appVersion" $.Chart.AppVersion "image" .Values.jwtSVIDExecConfig.image "global" .Values.global) }}
# Use the previously copied busybox to stage the exec credential plugin binary where the kubeConfigs consumers can fork it.
command:
- /plugins/busybox
- sh
- -ec
- |
/plugins/busybox cp -a {{ .Values.jwtSVIDExecConfig.pluginPath }} {{ include "spire-server.jwt-svid-exec-binary-path" . }}
volumeMounts:
- name: plugins
mountPath: /plugins
imagePullPolicy: {{ .Values.jwtSVIDExecConfig.image.pullPolicy }}
{{- end }}
{{- range $idx, $plugin := $pluginsToLoad }}
- name: {{ printf "init-plugin-%d" $idx }}
securityContext:
+28 -3
View File
@@ -1622,9 +1622,11 @@ tests:
tag: latest@sha256:90041f375e30f41aa7e0390075d8a69dc61900771d52fa98d63ee5d03d866a58
## @param kubeConfigs [object] Manage additional kubeconfig files to talk to external Kubernetes clusters
## Each entry sets exactly one of kubeConfig, kubeConfigBase64, or externalSecret. Use externalSecret to
## reference a kubeconfig from an externally-managed Secret instead of embedding it in values;
## entries may reference different Secrets and mix with inline ones.
## Each entry sets exactly one of kubeConfig, kubeConfigBase64, externalSecret, or jwtSVIDExec. Use externalSecret
## to reference a kubeconfig from an externally-managed Secret instead of embedding it in values; entries may
## reference different Secrets and mix with inline ones. Use jwtSVIDExec to have the chart generate an
## exec-credential kubeconfig that authenticates to the target cluster with short-lived SPIFFE JWT-SVIDs fetched
## at call time by the exec plugin (see jwtSVIDExecConfig).
kubeConfigs: {}
# clustera:
# kubeConfig: |
@@ -1636,6 +1638,29 @@ kubeConfigs: {}
# externalSecret:
# name: my-kubeconfigs-secret # name of the externally-managed Secret to read from
# key: clusterc # optional, defaults to the entry name
# clusterd:
# jwtSVIDExec:
# server: https://clusterd-api.example.com:6443 # target apiserver URL
# certificateAuthorityData: LS0tLS1CRUdJ... # apiserver CA bundle, base64-encoded PEM (kubeconfig certificate-authority-data)
# audience: k8s # optional, JWT-SVID audience the target expects (default k8s)
## @param jwtSVIDExecConfig.image.registry The OCI registry to pull the exec credential plugin image from
## @param jwtSVIDExecConfig.image.repository The repository within the registry
## @param jwtSVIDExecConfig.image.pullPolicy The image pull policy
## @param jwtSVIDExecConfig.image.tag Overrides the image tag
## @param jwtSVIDExecConfig.pluginPath The path of the plugin binary inside the plugin image, staged for exec by the kubeConfigs consumers
## @param jwtSVIDExecConfig.spiffeID The SPIFFE ID the plugin mints a JWT-SVID for; a full spiffe:// URI, or a "/"-prefixed path expanded with the chart trust domain (e.g. /spire-root); required when any kubeConfigs entry uses jwtSVIDExec
## Global wiring shared by every kubeConfigs entry that uses jwtSVIDExec. The plugin binary is staged from this
## image into the shared plugins volume, and every such entry mints a JWT-SVID for spiffeID from the SPIRE Server
## admin API socket, which is already mounted into the kubeConfigs consumers, so no agent Workload API socket is required.
jwtSVIDExecConfig:
image:
registry: ghcr.io
repository: spiffe/k8s-spiffe-workload-jwt-exec-auth
pullPolicy: IfNotPresent
tag: "0.2.0"
pluginPath: /ko-app/cmd
spiffeID: "/spire-root"
spireIdentityExchange:
## @param spireIdentityExchange.enabled Enable the server side of the SPIRE Identity Exchange system
+15
View File
@@ -333,6 +333,21 @@ spire-server:
Expect(objs[serverTmpl]).Should(ContainSubstring("name: my-ext-secret"))
Expect(objs[serverTmpl]).Should(ContainSubstring("path: clusterb"))
})
It("jwtSVIDExec entry generates the Secret and stages the exec plugin", func() {
objs, err := ValueStringRender(chart, `
spire-server:
jwtSVIDExecConfig:
spiffeID: spiffe://example.org/external-spire-server
kubeConfigs:
clusterd:
jwtSVIDExec:
server: https://clusterd-api.example.com:6443
certificateAuthorityData: TESTCADATAB64==
`)
Expect(err).Should(Succeed())
Expect(objs[secretTmpl]).Should(ContainSubstring("kind: Secret"))
Expect(objs[serverTmpl]).Should(ContainSubstring("init-jwt-svid-exec"))
})
})
Describe("spire-server.externalServerSubject", func() {
It("binds the external server's downstream RBAC to a ServiceAccount subject", func() {