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:
co-authored by
Claude Opus 4.8
parent
a481bab3f0
commit
648e0e45e5
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user