From 0024e996e37f3aa44708d10e095813a38f38d8ca Mon Sep 17 00:00:00 2001 From: Alan Cha Date: Thu, 2 Jul 2026 15:30:43 -0400 Subject: [PATCH 1/5] feat(operator): implement SPIFFE JWT-SVID authentication for client registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates admin credentials from operator client registration by using the operator's SPIFFE identity (JWT-SVID) to authenticate with the Keycloak Admin API. Changes: - internal/keycloak/admin.go: new JWTSVIDGrantToken() method that authenticates using client_assertion_type jwt-spiffe and returns an access token with manage-clients role - internal/controller/clientregistration_controller.go: dual auth path — UseSpiffeAuth=true reads JWT-SVID from file and calls JWTSVIDGrantToken; UseSpiffeAuth=false falls back to admin credentials. Includes path traversal protection, token exposure prevention, and Kubernetes Event recording for all failure modes. - cmd/main.go: registers --use-spiffe-auth, --jwt-svid-path, --operator-client-id flags and wires them into the reconciler Feature defaults to disabled (UseSpiffeAuth=false) for backward compatibility. Signed-off-by: Alan Cha Assisted-By: Claude (Anthropic AI) Signed-off-by: Alan Cha --- kagenti-operator/cmd/main.go | 18 +++ .../clientregistration_controller.go | 117 +++++++++++++++--- kagenti-operator/internal/keycloak/admin.go | 42 +++++++ 3 files changed, 157 insertions(+), 20 deletions(-) diff --git a/kagenti-operator/cmd/main.go b/kagenti-operator/cmd/main.go index 7f4a3877..f9f663af 100644 --- a/kagenti-operator/cmd/main.go +++ b/kagenti-operator/cmd/main.go @@ -134,6 +134,9 @@ func main() { var spiffeIdpAlias string var credentialWaitTimeout string var enableAuthbridgeConfig bool + var useSpiffeAuth bool + var jwtSVIDPath string + var operatorClientID string flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") @@ -207,6 +210,12 @@ func main() { "How long AuthBridge waits for Keycloak credentials to become available") flag.BoolVar(&enableAuthbridgeConfig, "enable-authbridge-config", true, "Reconcile authbridge-config ConfigMap in namespaces labeled kagenti-enabled=true") + flag.BoolVar(&useSpiffeAuth, "use-spiffe-auth", false, + "Use JWT-SVID authentication for Keycloak client registration instead of admin credentials") + flag.StringVar(&jwtSVIDPath, "jwt-svid-path", "/opt/jwt_svid.token", + "Path to JWT-SVID file written by spiffe-helper sidecar (used when --use-spiffe-auth=true)") + flag.StringVar(&operatorClientID, "operator-client-id", "", + "Operator SPIFFE ID (e.g. spiffe:///ns//sa/), used when --use-spiffe-auth=true") var enableSigstoreVerification bool var sigstoreAuditMode bool @@ -688,6 +697,11 @@ func main() { setupLog.Info("Client registration controller enabled", "keycloakAdminSecretNamespace", keycloakAdminSecretNamespace, "operatorNamespace", operatorNS) + if useSpiffeAuth { + setupLog.Info("SPIFFE ID authentication enabled: using JWT-SVID for client registration", + "spireSocket", verifiedFetchSpiffeSocket, + "operatorSPIFFEID", operatorClientID) + } if err = (&controller.ClientRegistrationReconciler{ Client: mgr.GetClient(), APIReader: mgr.GetAPIReader(), @@ -696,6 +710,10 @@ func main() { KeycloakAdminSecretNamespace: keycloakAdminSecretNamespace, SpireTrustDomain: spireTrustDomain, KeycloakAdminTokenCache: &keycloak.CachedAdminTokenProvider{}, + UseSpiffeAuth: useSpiffeAuth, + JWTSVIDPath: jwtSVIDPath, + OperatorClientID: operatorClientID, + Recorder: mgr.GetEventRecorderFor("clientregistration"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClientRegistration") os.Exit(1) diff --git a/kagenti-operator/internal/controller/clientregistration_controller.go b/kagenti-operator/internal/controller/clientregistration_controller.go index e19cb809..9efcb04a 100644 --- a/kagenti-operator/internal/controller/clientregistration_controller.go +++ b/kagenti-operator/internal/controller/clientregistration_controller.go @@ -10,6 +10,8 @@ package controller import ( "context" "fmt" + "os" + "path/filepath" "sort" "strings" "time" @@ -21,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -73,7 +76,24 @@ type ClientRegistrationReconciler struct { SpireTrustDomain string // KeycloakAdminTokenCache caches admin password-grant tokens by Keycloak URL and credentials to // avoid a token request on every reconcile. If nil, PasswordGrantToken is used without caching. + // Only used when UseSpiffeAuth is false. KeycloakAdminTokenCache *keycloak.CachedAdminTokenProvider + + // UseSpiffeAuth enables JWT-SVID authentication instead of admin credentials. + // When true, the operator authenticates to Keycloak with its JWT-SVID and uses + // the Admin API with manage-clients role. When false, uses admin credentials. + UseSpiffeAuth bool + + // JWTSVIDPath is the file path to read the operator's JWT-SVID from. + // Only used when UseSpiffeAuth is true. Default: /opt/jwt_svid.token + JWTSVIDPath string + + // OperatorClientID is the operator's SPIFFE ID (e.g., spiffe://localtest.me/ns/kagenti-operator-system/sa/...). + // Only used when UseSpiffeAuth is true. + OperatorClientID string + + // Recorder emits Kubernetes Events to surface configuration issues visible in kubectl describe. + Recorder record.EventRecorder } func (r *ClientRegistrationReconciler) uncachedReader() client.Reader { @@ -229,19 +249,6 @@ func (r *ClientRegistrationReconciler) reconcileOne( return ctrl.Result{RequeueAfter: 30 * time.Second}, nil } - adminUser, adminPass, err := r.resolveKeycloakAdminCredentials(ctx) - if err != nil { - if apierrors.IsNotFound(err) { - logger.Info("waiting for Keycloak admin secret") - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil - } - return ctrl.Result{}, err - } - if adminUser == "" || adminPass == "" { - logger.Info("Keycloak admin secret missing credentials") - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil - } - spireEnabled := r.SpireTrustDomain != "" clientName := ns + "/" + workloadName clientID, err := resolveKeycloakClientID(ns, workloadName, template.Spec.ServiceAccountName, spireEnabled, r.SpireTrustDomain) @@ -259,14 +266,84 @@ func (r *ClientRegistrationReconciler) reconcileOne( kc := keycloak.Admin{BaseURL: ab.KeycloakURL, HTTPClient: keycloak.DefaultHTTPClient()} var token string - if r.KeycloakAdminTokenCache != nil { - token, err = r.KeycloakAdminTokenCache.Token(ctx, &kc, adminUser, adminPass) + + // Authenticate to Keycloak: use JWT-SVID if enabled, otherwise admin credentials + if r.UseSpiffeAuth { + // SPIFFE authentication path + // Validate OperatorClientID before file I/O to fail fast on misconfiguration + if r.OperatorClientID == "" { + err := fmt.Errorf("OperatorClientID not configured") + logger.Error(err, "SPIFFE auth requires OperatorClientID") + if r.Recorder != nil { + r.Recorder.Event(owner, corev1.EventTypeWarning, "OperatorClientIDMissing", + "UseSpiffeAuth=true but OperatorClientID is empty. Check operator configuration.") + } + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + jwtSVIDPath := r.JWTSVIDPath + if jwtSVIDPath == "" { + jwtSVIDPath = "/opt/jwt_svid.token" + } + + // Path traversal protection: only allow reading from designated directories + cleanPath := filepath.Clean(jwtSVIDPath) + if !strings.HasPrefix(cleanPath, "/opt/") && !strings.HasPrefix(cleanPath, "/var/run/secrets/") { + err := fmt.Errorf("JWT-SVID path %q outside allowed directories (/opt/, /var/run/secrets/)", jwtSVIDPath) + logger.Error(err, "invalid JWT-SVID path") + if r.Recorder != nil { + r.Recorder.Eventf(owner, corev1.EventTypeWarning, "InvalidJWTSVIDPath", + "JWT-SVID path %q rejected: must be under /opt/ or /var/run/secrets/", jwtSVIDPath) + } + return ctrl.Result{}, err // fail permanently on config error + } + + jwtSVID, err := os.ReadFile(cleanPath) + if err != nil { + logger.Error(err, "read JWT-SVID failed", "path", cleanPath) + if r.Recorder != nil { + r.Recorder.Eventf(owner, corev1.EventTypeWarning, "JWTSVIDReadFailed", + "Failed to read JWT-SVID from %s: %v. Check spiffe-helper sidecar configuration.", cleanPath, err) + } + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + // WARNING: JWT-SVID is a bearer token - must never appear in logs or error messages + // to prevent token exposure. All code paths must handle jwtSVID as sensitive data. + token, err = kc.JWTSVIDGrantToken(ctx, ab.KeycloakRealm, r.OperatorClientID, string(jwtSVID)) + if err != nil { + logger.Error(err, "Keycloak JWT-SVID authentication failed") + if r.Recorder != nil { + r.Recorder.Event(owner, corev1.EventTypeWarning, "KeycloakAuthFailed", + "Failed to authenticate to Keycloak with JWT-SVID. Check SPIFFE IdP configuration in Keycloak.") + } + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + logger.V(1).Info("authenticated with JWT-SVID", "clientId", r.OperatorClientID) } else { - token, err = kc.PasswordGrantToken(ctx, adminUser, adminPass) - } - if err != nil { - logger.Error(err, "Keycloak admin token failed") - return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + // Admin credentials path (legacy) + adminUser, adminPass, err := r.resolveKeycloakAdminCredentials(ctx) + if err != nil { + if apierrors.IsNotFound(err) { + logger.Info("waiting for Keycloak admin secret") + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + return ctrl.Result{}, err + } + if adminUser == "" || adminPass == "" { + logger.Info("Keycloak admin secret missing credentials") + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + if r.KeycloakAdminTokenCache != nil { + token, err = r.KeycloakAdminTokenCache.Token(ctx, &kc, adminUser, adminPass) + } else { + token, err = kc.PasswordGrantToken(ctx, adminUser, adminPass) + } + if err != nil { + logger.Error(err, "Keycloak admin token failed") + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + logger.V(1).Info("authenticated with admin credentials") } agentClientUUID, clientSecret, err := kc.RegisterOrFetchClientWithToken(ctx, token, keycloak.ClientRegistrationParams{ Realm: ab.KeycloakRealm, diff --git a/kagenti-operator/internal/keycloak/admin.go b/kagenti-operator/internal/keycloak/admin.go index ade60d1a..20f5b104 100644 --- a/kagenti-operator/internal/keycloak/admin.go +++ b/kagenti-operator/internal/keycloak/admin.go @@ -126,6 +126,48 @@ func (a *Admin) PasswordGrantToken(ctx context.Context, adminUser, adminPass str return token, err } +// JWTSVIDGrantToken authenticates using JWT-SVID and returns an access token. +// The clientID must be the operator's SPIFFE ID (e.g., spiffe://localtest.me/ns/kagenti-operator-system/sa/...). +// The operator client must be configured in Keycloak with: +// - clientAuthenticatorType: "federated-jwt" +// - attributes.jwt.credential.issuer: SPIFFE IdP alias +// - attributes.jwt.credential.sub: operator's SPIFFE ID +// - Service account with manage-clients role +func (a *Admin) JWTSVIDGrantToken(ctx context.Context, realm, clientID, jwtSVID string) (string, error) { + base := trimBaseURL(a.BaseURL) + form := url.Values{} + form.Set("grant_type", "client_credentials") + form.Set("client_id", clientID) + form.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe") + form.Set("client_assertion", jwtSVID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + base+"/realms/"+url.PathEscape(realm)+"/protocol/openid-connect/token", + strings.NewReader(form.Encode())) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := a.httpc().Do(req) + if err != nil { + return "", err + } + defer func() { _ = resp.Body.Close() }() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("keycloak JWT-SVID token: status %d: %s", resp.StatusCode, truncate(body, 512)) + } + var tr adminTokenResponse + if err := json.Unmarshal(body, &tr); err != nil { + return "", fmt.Errorf("keycloak JWT-SVID token decode: %w", err) + } + if tr.AccessToken == "" { + return "", fmt.Errorf("keycloak JWT-SVID token: empty access_token") + } + return tr.AccessToken, nil +} + // RegisterOrFetchClient ensures an OAuth client exists and returns its internal UUID and client secret value. func (a *Admin) RegisterOrFetchClient(ctx context.Context, adminUser, adminPass string, p ClientRegistrationParams) (internalID, secret string, err error) { token, _, err := a.adminToken(ctx, adminUser, adminPass) From 9b07565bd16069a549225a0da82faecc825fd733 Mon Sep 17 00:00:00 2001 From: Alan Cha Date: Thu, 2 Jul 2026 15:30:56 -0400 Subject: [PATCH 2/5] feat(charts): add spiffe-helper sidecar and SPIFFE auth helm configuration Adds the operator-side helm chart support for SPIFFE authentication: - configmap-spiffe-helper.yaml (new): ConfigMap that configures spiffe-helper to write the operator's JWT-SVID to /opt/jwt_svid.token. jwt_audience defaults to spiffe.operatorAuth.jwtAudience (must be set explicitly; the fallback uses keycloak.publicUrl which defaults to empty). - manager.yaml: conditionally adds spiffe-helper sidecar container (runs as UID 65532 matching the manager, so the 600-mode JWT-SVID file is readable), jwt-svid emptyDir volume, spiffe-workload-api CSI volume, and --use-spiffe-auth/--jwt-svid-path/--operator-client-id flags when spiffe.operatorAuth.enabled=true. - values.yaml: adds spiffe: block with spiffe.enabled and spiffe.operatorAuth.enabled both defaulting to false. Signed-off-by: Alan Cha Assisted-By: Claude (Anthropic AI) Signed-off-by: Alan Cha --- .../manager/configmap-spiffe-helper.yaml | 24 +++++++++ .../templates/manager/manager.yaml | 53 ++++++++++++++++++- charts/kagenti-operator/values.yaml | 14 +++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml diff --git a/charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml b/charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml new file mode 100644 index 00000000..7abe2f92 --- /dev/null +++ b/charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: operator-spiffe-helper-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "chart.labels" . | nindent 4 }} +data: + config.hcl: | + agent_address = "/spiffe-workload-api/spire-agent.sock" + # cmd/cmd_args fire on X.509 SVID renewals only, not JWT SVIDs. + # File permissions are handled by running spiffe-helper as the same UID + # as the manager container (65532) so it can read the 600-mode file. + renew_signal = "" + cert_dir = "" + svid_file_name = "" + svid_bundle_file_name = "" + jwt_svids = [{ + jwt_audience = "{{ .Values.spiffe.operatorAuth.jwtAudience | default (printf "%s/realms/%s" .Values.keycloak.publicUrl .Values.keycloak.realm) }}" + jwt_svid_file_name = "/opt/jwt_svid.token" + }] +{{- end }} diff --git a/charts/kagenti-operator/templates/manager/manager.yaml b/charts/kagenti-operator/templates/manager/manager.yaml index d15a92fd..be247d97 100644 --- a/charts/kagenti-operator/templates/manager/manager.yaml +++ b/charts/kagenti-operator/templates/manager/manager.yaml @@ -92,7 +92,7 @@ spec: - "--credential-wait-timeout={{ .Values.authbridgeConfig.credentialWaitTimeout }}" {{- end }} {{- end }} - {{- if .Values.sigstore.cardVerification.enabled }} +{{- if .Values.sigstore.cardVerification.enabled }} - "--enable-sigstore-verification=true" {{- if .Values.sigstore.cardVerification.auditMode }} - "--sigstore-audit-mode=true" @@ -116,6 +116,11 @@ spec: - "--sigstore-trusted-root-configmap-key={{ .Values.sigstore.cardVerification.trustedRoot.configMapKey }}" {{- end }} {{- end }} + {{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }} + - "--use-spiffe-auth=true" + - "--jwt-svid-path={{ .Values.spiffe.operatorAuth.jwtSVIDPath | default "/opt/jwt_svid.token" }}" + - "--operator-client-id=spiffe://{{ .Values.signatureVerification.spireTrustDomain | default "localtest.me" }}/ns/{{ .Release.Namespace }}/sa/{{ .Values.controllerManager.serviceAccountName }}" + {{- end }} command: - {{ .Values.controllerManager.container.cmd }} image: {{ .Values.controllerManager.container.image.repository }}:{{ .Values.controllerManager.container.image.tag }} @@ -181,6 +186,42 @@ spec: mountPath: /spiffe-workload-api readOnly: true {{- end }} + {{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }} + - name: jwt-svid + mountPath: /opt + readOnly: true + {{- end }} + {{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }} + - name: spiffe-helper + image: ghcr.io/kagenti/kagenti-extensions/spiffe-helper:latest + imagePullPolicy: IfNotPresent + args: + - "-config" + - "/etc/spiffe-helper/config.hcl" + volumeMounts: + - name: spiffe-workload-api + mountPath: /spiffe-workload-api + readOnly: true + - name: spiffe-helper-config + mountPath: /etc/spiffe-helper + readOnly: true + - name: jwt-svid + mountPath: /opt + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 65532 + capabilities: + drop: + - ALL + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + {{- end }} securityContext: {{- toYaml .Values.controllerManager.securityContext | nindent 8 }} serviceAccountName: {{ .Values.controllerManager.serviceAccountName }} @@ -204,9 +245,17 @@ spec: secret: secretName: kagenti-operator-metrics-server-cert {{- end }} - {{- if .Values.verifiedFetch.enabled }} + {{- if or .Values.verifiedFetch.enabled (and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled) }} - name: spiffe-workload-api csi: driver: "csi.spiffe.io" readOnly: true {{- end }} + {{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }} + - name: spiffe-helper-config + configMap: + name: operator-spiffe-helper-config + - name: jwt-svid + emptyDir: + medium: Memory + {{- end }} diff --git a/charts/kagenti-operator/values.yaml b/charts/kagenti-operator/values.yaml index 7491a5cd..f260a3d4 100644 --- a/charts/kagenti-operator/values.yaml +++ b/charts/kagenti-operator/values.yaml @@ -189,6 +189,20 @@ sigstore: configMapNamespace: "" configMapKey: "trusted-root.json" +# [SPIFFE OPERATOR AUTH]: Operator authentication to Keycloak using JWT-SVID +# When enabled, the operator uses its SPIFFE identity (JWT-SVID) to authenticate +# to Keycloak instead of admin credentials. Requires Keycloak SPIFFE IdP configured. +spiffe: + enabled: false + operatorAuth: + enabled: false + # JWT audience must match Keycloak's realm issuer URL exactly. + # If not set, defaults to: {{ .Values.keycloak.publicUrl }}/realms/{{ .Values.keycloak.realm }} + # Check Keycloak's .well-known/openid-configuration for the correct issuer value. + jwtAudience: "" + # Path to JWT-SVID file written by spiffe-helper sidecar + jwtSVIDPath: "/opt/jwt_svid.token" + # Feature gates — highest-priority layer in the injection precedence chain. # Set globalEnabled to false to disable ALL sidecar injection (kill switch). # Set individual gates to false to disable specific sidecars cluster-wide. From 20e1ed3cc74236d5e86ce9c3769f0a1609f4e630 Mon Sep 17 00:00:00 2001 From: Alan Cha Date: Thu, 2 Jul 2026 15:45:21 -0400 Subject: [PATCH 3/5] fix(operator): suppress deprecated GetEventRecorderFor lint warning The Recorder field is typed as record.EventRecorder (old events API). GetEventRecorderFor returns the old API type and is the correct call here. Suppress the SA1019 staticcheck warning until the field type is migrated to the new events.EventRecorder API. Signed-off-by: Alan Cha Assisted-By: Claude (Anthropic AI) Signed-off-by: Alan Cha --- kagenti-operator/cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kagenti-operator/cmd/main.go b/kagenti-operator/cmd/main.go index f9f663af..1be8a4eb 100644 --- a/kagenti-operator/cmd/main.go +++ b/kagenti-operator/cmd/main.go @@ -713,7 +713,7 @@ func main() { UseSpiffeAuth: useSpiffeAuth, JWTSVIDPath: jwtSVIDPath, OperatorClientID: operatorClientID, - Recorder: mgr.GetEventRecorderFor("clientregistration"), + Recorder: mgr.GetEventRecorderFor("clientregistration"), //nolint:staticcheck }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClientRegistration") os.Exit(1) From 6ae95f910878135e8ee88689a6d9392e41ae09cc Mon Sep 17 00:00:00 2001 From: Alan Cha Date: Mon, 6 Jul 2026 14:39:23 -0400 Subject: [PATCH 4/5] Fix spacing Signed-off-by: Alan Cha --- charts/kagenti-operator/templates/manager/manager.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/kagenti-operator/templates/manager/manager.yaml b/charts/kagenti-operator/templates/manager/manager.yaml index be247d97..d7fa042f 100644 --- a/charts/kagenti-operator/templates/manager/manager.yaml +++ b/charts/kagenti-operator/templates/manager/manager.yaml @@ -92,7 +92,7 @@ spec: - "--credential-wait-timeout={{ .Values.authbridgeConfig.credentialWaitTimeout }}" {{- end }} {{- end }} -{{- if .Values.sigstore.cardVerification.enabled }} + {{- if .Values.sigstore.cardVerification.enabled }} - "--enable-sigstore-verification=true" {{- if .Values.sigstore.cardVerification.auditMode }} - "--sigstore-audit-mode=true" From 541a618f9ce659ab8cf08168dc86a48569ce8c37 Mon Sep 17 00:00:00 2001 From: Alan Cha Date: Tue, 7 Jul 2026 10:35:58 -0400 Subject: [PATCH 5/5] fix(charts): pin spiffe-helper image and require jwtAudience - spiffe-helper was hardcoded to :latest. Added spiffe.operatorAuth.spiffeHelper.image.{repository,tag,pullPolicy} values defaulting to ghcr.io/kagenti/kagenti-extensions/spiffe-helper:v0.6.0-alpha.4, matching the latest kagenti-extensions release and following the same repository/tag pattern as every other image in this chart. - jwt_audience fallback (printf keycloak.publicUrl/realms/realm) produced /realms/kagenti when keycloak.publicUrl was unset (default: empty string), causing a silent runtime auth failure. Replaced with required so the misconfiguration surfaces at helm install instead. Signed-off-by: Alan Cha Assisted-By: Claude (Anthropic AI) Signed-off-by: Alan Cha --- .../templates/manager/configmap-spiffe-helper.yaml | 2 +- charts/kagenti-operator/templates/manager/manager.yaml | 4 ++-- charts/kagenti-operator/values.yaml | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml b/charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml index 7abe2f92..75cb7079 100644 --- a/charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml +++ b/charts/kagenti-operator/templates/manager/configmap-spiffe-helper.yaml @@ -18,7 +18,7 @@ data: svid_file_name = "" svid_bundle_file_name = "" jwt_svids = [{ - jwt_audience = "{{ .Values.spiffe.operatorAuth.jwtAudience | default (printf "%s/realms/%s" .Values.keycloak.publicUrl .Values.keycloak.realm) }}" + 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 }}" jwt_svid_file_name = "/opt/jwt_svid.token" }] {{- end }} diff --git a/charts/kagenti-operator/templates/manager/manager.yaml b/charts/kagenti-operator/templates/manager/manager.yaml index d7fa042f..5a9fc57b 100644 --- a/charts/kagenti-operator/templates/manager/manager.yaml +++ b/charts/kagenti-operator/templates/manager/manager.yaml @@ -193,8 +193,8 @@ spec: {{- end }} {{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }} - name: spiffe-helper - image: ghcr.io/kagenti/kagenti-extensions/spiffe-helper:latest - imagePullPolicy: IfNotPresent + image: {{ .Values.spiffe.operatorAuth.spiffeHelper.image.repository }}:{{ .Values.spiffe.operatorAuth.spiffeHelper.image.tag }} + imagePullPolicy: {{ .Values.spiffe.operatorAuth.spiffeHelper.image.pullPolicy | default "IfNotPresent" }} args: - "-config" - "/etc/spiffe-helper/config.hcl" diff --git a/charts/kagenti-operator/values.yaml b/charts/kagenti-operator/values.yaml index f260a3d4..acdde623 100644 --- a/charts/kagenti-operator/values.yaml +++ b/charts/kagenti-operator/values.yaml @@ -202,6 +202,11 @@ spiffe: jwtAudience: "" # Path to JWT-SVID file written by spiffe-helper sidecar jwtSVIDPath: "/opt/jwt_svid.token" + spiffeHelper: + image: + repository: ghcr.io/kagenti/kagenti-extensions/spiffe-helper + tag: v0.6.0-alpha.4 + pullPolicy: IfNotPresent # Feature gates — highest-priority layer in the injection precedence chain. # Set globalEnabled to false to disable ALL sidecar injection (kill switch).