Skip to content

Commit dfe5b6f

Browse files
authored
Merge pull request #476 from kagenti/feat/spiffe-auth-full
Fix(charts+controller): SPIFFE auth follow-up — jwtAudience cleanup, credential secret skip, tag fix
2 parents 7887e96 + 0d25ce8 commit dfe5b6f

5 files changed

Lines changed: 61 additions & 29 deletions

File tree

charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,20 @@ data:
1717
cert_dir = ""
1818
svid_file_name = ""
1919
svid_bundle_file_name = ""
20+
{{- /*
21+
jwt_audience must equal the Keycloak realm's issuer URL — the string Keycloak
22+
advertises in its /.well-known/openid-configuration. It is always derived from
23+
keycloak.publicUrl (the external URL Keycloak was configured with). The internal
24+
k8s service address must NOT be used because Keycloak's FederatedJWTClientValidator
25+
checks the aud claim via string equality against its own issuer, which is always
26+
the external URL.
27+
*/ -}}
28+
{{- $audience := printf "%s/realms/%s" .Values.keycloak.publicUrl .Values.keycloak.realm -}}
29+
{{- if not (hasPrefix "http" $audience) -}}
30+
{{- fail "keycloak.publicUrl must be set to the external Keycloak URL (e.g. http://keycloak.example.com) when spiffe.operatorAuth.enabled=true" -}}
31+
{{- end -}}
2032
jwt_svids = [{
21-
jwt_audience = "{{ required "spiffe.operatorAuth.jwtAudience must be set when spiffe.operatorAuth.enabled=true (e.g. http://keycloak.example.com/realms/kagenti)" .Values.spiffe.operatorAuth.jwtAudience }}"
33+
jwt_audience = "{{ $audience }}"
2234
jwt_svid_file_name = "/opt/jwt_svid.token"
2335
}]
2436
{{- end }}

charts/kagenti-operator/templates/manager/manager.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ spec:
193193
{{- end }}
194194
{{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }}
195195
- name: spiffe-helper
196-
image: {{ .Values.spiffe.operatorAuth.spiffeHelper.image.repository }}:{{ .Values.spiffe.operatorAuth.spiffeHelper.image.tag }}
197-
imagePullPolicy: {{ .Values.spiffe.operatorAuth.spiffeHelper.image.pullPolicy | default "IfNotPresent" }}
196+
image: ghcr.io/kagenti/kagenti-extensions/spiffe-helper:latest
197+
imagePullPolicy: IfNotPresent
198198
args:
199199
- "-config"
200200
- "/etc/spiffe-helper/config.hcl"

charts/kagenti-operator/values.yaml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,23 +190,23 @@ sigstore:
190190
configMapKey: "trusted-root.json"
191191

192192
# [SPIFFE OPERATOR AUTH]: Operator authentication to Keycloak using JWT-SVID
193-
# When enabled, the operator uses its SPIFFE identity (JWT-SVID) to authenticate
194-
# to Keycloak instead of admin credentials. Requires Keycloak SPIFFE IdP configured.
193+
# spiffe.enabled — master switch indicating that SPIRE is present in the cluster
194+
# and SPIFFE workload identities are available. Set to true whenever --with-spire
195+
# is used. Features that consume SPIFFE identities (e.g. verified-fetch, operatorAuth)
196+
# each have their own sub-flag; this top-level switch guards them all.
197+
#
198+
# spiffe.operatorAuth — controls how the operator authenticates to the Keycloak
199+
# Admin API when registering agent OAuth clients. When disabled (default), the
200+
# operator uses admin credentials from keycloak-admin-secret. When enabled, the
201+
# operator uses its own SPIFFE JWT-SVID, eliminating the need for that secret.
202+
# This is independent of how agent/tool workloads authenticate (see authBridge
203+
# clientAuthType in the kagenti chart).
195204
spiffe:
196205
enabled: false
197206
operatorAuth:
198207
enabled: false
199-
# JWT audience must match Keycloak's realm issuer URL exactly.
200-
# If not set, defaults to: {{ .Values.keycloak.publicUrl }}/realms/{{ .Values.keycloak.realm }}
201-
# Check Keycloak's .well-known/openid-configuration for the correct issuer value.
202-
jwtAudience: ""
203208
# Path to JWT-SVID file written by spiffe-helper sidecar
204209
jwtSVIDPath: "/opt/jwt_svid.token"
205-
spiffeHelper:
206-
image:
207-
repository: ghcr.io/kagenti/kagenti-extensions/spiffe-helper
208-
tag: v0.6.0-alpha.4
209-
pullPolicy: IfNotPresent
210210

211211
# Feature gates — highest-priority layer in the injection precedence chain.
212212
# Set globalEnabled to false to disable ALL sidecar injection (kill switch).

kagenti-operator/internal/controller/clientregistration_controller.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -370,15 +370,28 @@ func (r *ClientRegistrationReconciler) reconcileOne(
370370
"clientId", clientID)
371371
}
372372

373+
// In federated-jwt mode, agents authenticate using their SPIFFE identity directly.
374+
// No credential secret is needed — AuthBridge reads JWT-SVIDs from the workload
375+
// API socket, not from a mounted secret. Skipping secret creation avoids leaving
376+
// unused Keycloak client secrets in agent namespaces.
373377
secretName := keycloakClientCredentialsSecretName(ns, workloadName)
374-
if err := r.ensureClientCredentialsSecret(ctx, owner, secretName, clientID, clientSecret); err != nil {
375-
logger.Error(err, "ensure client credentials secret")
376-
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
377-
}
378-
379-
if err := patchTemplate(ctx); err != nil {
380-
logger.Error(err, "patch workload pod template")
381-
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
378+
if authType != "federated-jwt" {
379+
if err := r.ensureClientCredentialsSecret(ctx, owner, secretName, clientID, clientSecret); err != nil {
380+
logger.Error(err, "ensure client credentials secret")
381+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
382+
}
383+
// Annotate the pod template with the secret name so the webhook mounts it.
384+
if err := patchTemplate(ctx); err != nil {
385+
logger.Error(err, "patch workload pod template")
386+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
387+
}
388+
} else {
389+
// In federated-jwt mode, AuthBridge reads JWT-SVIDs directly from the SPIFFE
390+
// workload API socket — no credential secret is needed or created. Skip both the
391+
// secret and the pod template annotation so the webhook does not try to mount a
392+
// non-existent secret.
393+
logger.V(1).Info("skipping credential secret and template patch (federated-jwt — AuthBridge uses SPIFFE identity directly)",
394+
"workload", workloadName, "namespace", ns)
382395
}
383396

384397
logger.Info("operator client registration applied",

kagenti-operator/internal/webhook/v1alpha1/authbridge_webhook.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,23 @@ func (w *AuthBridgeWebhook) Handle(ctx context.Context, req admission.Request) a
9696
// kubelet will wait for the Secret to appear and mount it — no pod restart required.
9797
// Without this, the first pod comes up with an empty /shared/ and envoy returns 503
9898
// "identity not yet configured (credentials pending)" until the user deletes the pod.
99+
//
100+
// Skip in federated-jwt mode: AuthBridge reads JWT-SVIDs from the SPIFFE workload API
101+
// socket directly and no credential Secret is created or needed. Injecting the volume
102+
// mount for a non-existent Secret would leave pods stuck in Init.
99103
if pod.Annotations[injector.AnnotationKeycloakClientSecretName] == "" &&
100104
clientreg.WorkloadWantsOperatorClientReg(pod.Labels, w.Mutator.GetFeatureGates().InjectTools) {
101-
if pod.Annotations == nil {
102-
pod.Annotations = map[string]string{}
105+
nsConfig, _ := injector.ReadNamespaceConfig(ctx, w.Mutator.APIReader, req.Namespace)
106+
if nsConfig == nil || nsConfig.ClientAuthType != injector.ClientAuthTypeFederatedJWT {
107+
if pod.Annotations == nil {
108+
pod.Annotations = map[string]string{}
109+
}
110+
pod.Annotations[injector.AnnotationKeycloakClientSecretName] =
111+
clientreg.KeycloakClientCredentialsSecretName(req.Namespace, resourceName)
112+
authbridgelog.Info("pre-populated Keycloak client credentials annotation",
113+
"namespace", req.Namespace, "name", resourceName,
114+
"secret", pod.Annotations[injector.AnnotationKeycloakClientSecretName])
103115
}
104-
pod.Annotations[injector.AnnotationKeycloakClientSecretName] =
105-
clientreg.KeycloakClientCredentialsSecretName(req.Namespace, resourceName)
106-
authbridgelog.Info("pre-populated Keycloak client credentials annotation",
107-
"namespace", req.Namespace, "name", resourceName,
108-
"secret", pod.Annotations[injector.AnnotationKeycloakClientSecretName])
109116
}
110117

111118
// Check if already injected (idempotency / reinvocation)

0 commit comments

Comments
 (0)