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.pullPolicy` | The image pull policy | `IfNotPresent` |
|
||||||
| `tests.bash.image.tag` | Overrides the image tag whose default is the chart appVersion | `latest@sha256:90041f375e30f41aa7e0390075d8a69dc61900771d52fa98d63ee5d03d866a58` |
|
| `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 | `{}` |
|
| `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` |
|
| `spireIdentityExchange.enabled` | Enable the server side of the SPIRE Identity Exchange system | `false` |
|
||||||
| `spike.enabled` | Enable the server side of SPIKE | `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: {{ . }}
|
subPath: {{ . }}
|
||||||
readOnly: true
|
readOnly: true
|
||||||
{{- end }}
|
{{- 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
|
- name: spire-controller-manager-tmp
|
||||||
mountPath: /tmp
|
mountPath: /tmp
|
||||||
subPath: {{ printf "spire-controller-manager%s" .suffix }}
|
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
|
{{ include "spire-server.fullname" . }}-kubeconfigs
|
||||||
{{- end }}
|
{{- 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" }}
|
{{- define "spire-server.serviceAccountAllowedList" }}
|
||||||
{{- $releaseNamespace := include "spire-server.agent-namespace" . }}
|
{{- $releaseNamespace := include "spire-server.agent-namespace" . }}
|
||||||
{{- if ne (len .Values.nodeAttestor.k8sPSAT.serviceAccountAllowList) 0 }}
|
{{- if ne (len .Values.nodeAttestor.k8sPSAT.serviceAccountAllowList) 0 }}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
{{- if hasKey $value "kubeConfig" }}{{ $present = append $present "kubeConfig" }}{{- end }}
|
{{- if hasKey $value "kubeConfig" }}{{ $present = append $present "kubeConfig" }}{{- end }}
|
||||||
{{- if hasKey $value "kubeConfigBase64" }}{{ $present = append $present "kubeConfigBase64" }}{{- end }}
|
{{- if hasKey $value "kubeConfigBase64" }}{{ $present = append $present "kubeConfigBase64" }}{{- end }}
|
||||||
{{- if hasKey $value "externalSecret" }}{{ $present = append $present "externalSecret" }}{{- end }}
|
{{- if hasKey $value "externalSecret" }}{{ $present = append $present "externalSecret" }}{{- end }}
|
||||||
|
{{- if hasKey $value "jwtSVIDExec" }}{{ $present = append $present "jwtSVIDExec" }}{{- end }}
|
||||||
{{- if ne (len $present) 1 }}
|
{{- 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 }}
|
{{- end }}
|
||||||
{{- if hasKey $value "externalSecret" }}
|
{{- if hasKey $value "externalSecret" }}
|
||||||
{{- if not $value.externalSecret.name }}
|
{{- if not $value.externalSecret.name }}
|
||||||
@@ -30,6 +31,8 @@ data:
|
|||||||
{{- range $name, $value := $inline }}
|
{{- range $name, $value := $inline }}
|
||||||
{{- if hasKey $value "kubeConfig" }}
|
{{- if hasKey $value "kubeConfig" }}
|
||||||
{{ $name }}: {{ $value.kubeConfig | b64enc }}
|
{{ $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 }}
|
{{- else }}
|
||||||
{{ $name }}: {{ $value.kubeConfigBase64 | nospace }}
|
{{ $name }}: {{ $value.kubeConfigBase64 | nospace }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -65,7 +65,11 @@
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- $pluginsToLoad := include "spire-lib.extract_custom_plugin_images" . | fromYamlArray }}
|
{{- $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 }}
|
{{- if not .Values.externalServer }}
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
{{- if eq .Values.kind "statefulset" }}
|
{{- if eq .Values.kind "statefulset" }}
|
||||||
@@ -176,6 +180,23 @@ spec:
|
|||||||
mountPath: /plugins
|
mountPath: /plugins
|
||||||
imagePullPolicy: {{ .Values.credentialComposer.spireIdentityExchange.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.credentialComposer.spireIdentityExchange.image.pullPolicy }}
|
||||||
{{- end }}
|
{{- 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 }}
|
{{- range $idx, $plugin := $pluginsToLoad }}
|
||||||
- name: {{ printf "init-plugin-%d" $idx }}
|
- name: {{ printf "init-plugin-%d" $idx }}
|
||||||
securityContext:
|
securityContext:
|
||||||
|
|||||||
@@ -1622,9 +1622,11 @@ tests:
|
|||||||
tag: latest@sha256:90041f375e30f41aa7e0390075d8a69dc61900771d52fa98d63ee5d03d866a58
|
tag: latest@sha256:90041f375e30f41aa7e0390075d8a69dc61900771d52fa98d63ee5d03d866a58
|
||||||
|
|
||||||
## @param kubeConfigs [object] Manage additional kubeconfig files to talk to external Kubernetes clusters
|
## @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
|
## Each entry sets exactly one of kubeConfig, kubeConfigBase64, externalSecret, or jwtSVIDExec. Use externalSecret
|
||||||
## reference a kubeconfig from an externally-managed Secret instead of embedding it in values;
|
## to reference a kubeconfig from an externally-managed Secret instead of embedding it in values; entries may
|
||||||
## entries may reference different Secrets and mix with inline ones.
|
## 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: {}
|
kubeConfigs: {}
|
||||||
# clustera:
|
# clustera:
|
||||||
# kubeConfig: |
|
# kubeConfig: |
|
||||||
@@ -1636,6 +1638,29 @@ kubeConfigs: {}
|
|||||||
# externalSecret:
|
# externalSecret:
|
||||||
# name: my-kubeconfigs-secret # name of the externally-managed Secret to read from
|
# name: my-kubeconfigs-secret # name of the externally-managed Secret to read from
|
||||||
# key: clusterc # optional, defaults to the entry name
|
# 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:
|
spireIdentityExchange:
|
||||||
## @param spireIdentityExchange.enabled Enable the server side of the SPIRE Identity Exchange system
|
## @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("name: my-ext-secret"))
|
||||||
Expect(objs[serverTmpl]).Should(ContainSubstring("path: clusterb"))
|
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() {
|
Describe("spire-server.externalServerSubject", func() {
|
||||||
It("binds the external server's downstream RBAC to a ServiceAccount subject", func() {
|
It("binds the external server's downstream RBAC to a ServiceAccount subject", func() {
|
||||||
|
|||||||
Reference in New Issue
Block a user