Skip to content

Commit 0532b06

Browse files
committed
feat(operator): fetch JWT-SVID directly via go-spiffe SDK, remove spiffe-helper sidecar
The spiffe-helper sidecar approach used an abandoned standalone image from the kagenti-extensions pipeline (removed April 2026 when spiffe-helper was bundled into authbridge). The authbridge-proxy itself already uses the go-spiffe WorkloadAPI client in-process — this change makes the operator consistent with that architecture. Changes: - clientregistration_controller.go: replace JWTSVIDPath + os.ReadFile with workloadapi.New() + FetchJWTSVID(). Uses the same socket as verifiedFetch. Removes path traversal validation (no longer needed without file I/O). - cmd/main.go: remove --jwt-svid-path flag; reuse --verified-fetch-spiffe-socket for JWT-SVID fetching via SpiffeSocket field on the reconciler. - manager.yaml: remove spiffe-helper container, jwt-svid emptyDir volume, spiffe-helper-config volume mount. spiffe-workload-api CSI volume remains (shared with verifiedFetch). - configmap-spiffe-helper.yaml: deleted (no longer needed). - values.yaml: remove jwtSVIDPath value. Signed-off-by: Alan Cha <alan.cha@ibm.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
1 parent 2e6ad42 commit 0532b06

5 files changed

Lines changed: 36 additions & 117 deletions

File tree

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

Lines changed: 0 additions & 36 deletions
This file was deleted.

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

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ spec:
118118
{{- end }}
119119
{{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }}
120120
- "--use-spiffe-auth=true"
121-
- "--jwt-svid-path={{ .Values.spiffe.operatorAuth.jwtSVIDPath | default "/opt/jwt_svid.token" }}"
122121
- "--operator-client-id=spiffe://{{ .Values.signatureVerification.spireTrustDomain | default "localtest.me" }}/ns/{{ .Release.Namespace }}/sa/{{ .Values.controllerManager.serviceAccountName }}"
123122
{{- end }}
124123
command:
@@ -186,42 +185,6 @@ spec:
186185
mountPath: /spiffe-workload-api
187186
readOnly: true
188187
{{- end }}
189-
{{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }}
190-
- name: jwt-svid
191-
mountPath: /opt
192-
readOnly: true
193-
{{- end }}
194-
{{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }}
195-
- name: spiffe-helper
196-
image: ghcr.io/kagenti/kagenti-extensions/spiffe-helper:latest
197-
imagePullPolicy: IfNotPresent
198-
args:
199-
- "-config"
200-
- "/etc/spiffe-helper/config.hcl"
201-
volumeMounts:
202-
- name: spiffe-workload-api
203-
mountPath: /spiffe-workload-api
204-
readOnly: true
205-
- name: spiffe-helper-config
206-
mountPath: /etc/spiffe-helper
207-
readOnly: true
208-
- name: jwt-svid
209-
mountPath: /opt
210-
securityContext:
211-
allowPrivilegeEscalation: false
212-
runAsNonRoot: true
213-
runAsUser: 65532
214-
capabilities:
215-
drop:
216-
- ALL
217-
resources:
218-
requests:
219-
cpu: 10m
220-
memory: 32Mi
221-
limits:
222-
cpu: 100m
223-
memory: 64Mi
224-
{{- end }}
225188
securityContext:
226189
{{- toYaml .Values.controllerManager.securityContext | nindent 8 }}
227190
serviceAccountName: {{ .Values.controllerManager.serviceAccountName }}
@@ -251,11 +214,3 @@ spec:
251214
driver: "csi.spiffe.io"
252215
readOnly: true
253216
{{- end }}
254-
{{- if and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled }}
255-
- name: spiffe-helper-config
256-
configMap:
257-
name: operator-spiffe-helper-config
258-
- name: jwt-svid
259-
emptyDir:
260-
medium: Memory
261-
{{- end }}

charts/kagenti-operator/values.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,6 @@ spiffe:
205205
enabled: false
206206
operatorAuth:
207207
enabled: false
208-
# Path to JWT-SVID file written by spiffe-helper sidecar
209-
jwtSVIDPath: "/opt/jwt_svid.token"
210208

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

kagenti-operator/cmd/main.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ func main() {
135135
var credentialWaitTimeout string
136136
var enableAuthbridgeConfig bool
137137
var useSpiffeAuth bool
138-
var jwtSVIDPath string
139138
var operatorClientID string
140139

141140
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
@@ -211,9 +210,8 @@ func main() {
211210
flag.BoolVar(&enableAuthbridgeConfig, "enable-authbridge-config", true,
212211
"Reconcile authbridge-config ConfigMap in namespaces labeled kagenti-enabled=true")
213212
flag.BoolVar(&useSpiffeAuth, "use-spiffe-auth", false,
214-
"Use JWT-SVID authentication for Keycloak client registration instead of admin credentials")
215-
flag.StringVar(&jwtSVIDPath, "jwt-svid-path", "/opt/jwt_svid.token",
216-
"Path to JWT-SVID file written by spiffe-helper sidecar (used when --use-spiffe-auth=true)")
213+
"Use JWT-SVID authentication for Keycloak client registration instead of admin credentials. "+
214+
"JWT-SVIDs are fetched directly from the SPIRE workload API (--verified-fetch-spiffe-socket).")
217215
flag.StringVar(&operatorClientID, "operator-client-id", "",
218216
"Operator SPIFFE ID (e.g. spiffe://<domain>/ns/<ns>/sa/<sa>), used when --use-spiffe-auth=true")
219217

@@ -711,7 +709,7 @@ func main() {
711709
SpireTrustDomain: spireTrustDomain,
712710
KeycloakAdminTokenCache: &keycloak.CachedAdminTokenProvider{},
713711
UseSpiffeAuth: useSpiffeAuth,
714-
JWTSVIDPath: jwtSVIDPath,
712+
SpiffeSocket: verifiedFetchSpiffeSocket,
715713
OperatorClientID: operatorClientID,
716714
Recorder: mgr.GetEventRecorderFor("clientregistration"), //nolint:staticcheck
717715
}).SetupWithManager(mgr); err != nil {

kagenti-operator/internal/controller/clientregistration_controller.go

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ package controller
1010
import (
1111
"context"
1212
"fmt"
13-
"os"
14-
"path/filepath"
1513
"sort"
1614
"strings"
1715
"time"
1816

17+
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
18+
"github.com/spiffe/go-spiffe/v2/workloadapi"
19+
1920
appsv1 "k8s.io/api/apps/v1"
2021
corev1 "k8s.io/api/core/v1"
2122
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -80,15 +81,16 @@ type ClientRegistrationReconciler struct {
8081
KeycloakAdminTokenCache *keycloak.CachedAdminTokenProvider
8182

8283
// UseSpiffeAuth enables JWT-SVID authentication instead of admin credentials.
83-
// When true, the operator authenticates to Keycloak with its JWT-SVID and uses
84-
// the Admin API with manage-clients role. When false, uses admin credentials.
84+
// When true, the operator fetches a JWT-SVID from SPIRE via the go-spiffe SDK
85+
// and uses it with the Keycloak Admin API (manage-clients role only).
86+
// When false, uses admin credentials.
8587
UseSpiffeAuth bool
8688

87-
// JWTSVIDPath is the file path to read the operator's JWT-SVID from.
88-
// Only used when UseSpiffeAuth is true. Default: /opt/jwt_svid.token
89-
JWTSVIDPath string
89+
// SpiffeSocket is the SPIRE workload API socket address used to fetch JWT-SVIDs.
90+
// Only used when UseSpiffeAuth is true. Defaults to the verified-fetch socket.
91+
SpiffeSocket string
9092

91-
// OperatorClientID is the operator's SPIFFE ID (e.g., spiffe://localtest.me/ns/kagenti-operator-system/sa/...).
93+
// OperatorClientID is the operator's SPIFFE ID (e.g., spiffe://localtest.me/ns/kagenti-system/sa/controller-manager).
9294
// Only used when UseSpiffeAuth is true.
9395
OperatorClientID string
9496

@@ -269,8 +271,6 @@ func (r *ClientRegistrationReconciler) reconcileOne(
269271

270272
// Authenticate to Keycloak: use JWT-SVID if enabled, otherwise admin credentials
271273
if r.UseSpiffeAuth {
272-
// SPIFFE authentication path
273-
// Validate OperatorClientID before file I/O to fail fast on misconfiguration
274274
if r.OperatorClientID == "" {
275275
err := fmt.Errorf("OperatorClientID not configured")
276276
logger.Error(err, "SPIFFE auth requires OperatorClientID")
@@ -281,36 +281,40 @@ func (r *ClientRegistrationReconciler) reconcileOne(
281281
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
282282
}
283283

284-
jwtSVIDPath := r.JWTSVIDPath
285-
if jwtSVIDPath == "" {
286-
jwtSVIDPath = "/opt/jwt_svid.token"
284+
socket := r.SpiffeSocket
285+
if socket == "" {
286+
socket = "unix:///spiffe-workload-api/spire-agent.sock"
287287
}
288288

289-
// Path traversal protection: only allow reading from designated directories
290-
cleanPath := filepath.Clean(jwtSVIDPath)
291-
if !strings.HasPrefix(cleanPath, "/opt/") && !strings.HasPrefix(cleanPath, "/var/run/secrets/") {
292-
err := fmt.Errorf("JWT-SVID path %q outside allowed directories (/opt/, /var/run/secrets/)", jwtSVIDPath)
293-
logger.Error(err, "invalid JWT-SVID path")
289+
// Fetch a JWT-SVID directly from the SPIRE workload API using the go-spiffe SDK.
290+
// This is the same socket used by verifiedFetch — no sidecar or file needed.
291+
// The audience must be the Keycloak realm's issuer URL (external URL, not in-cluster).
292+
// The JWT-SVID audience must be the Keycloak realm's issuer URL (external/public URL).
293+
// Keycloak's FederatedJWTClientValidator checks aud == its own issuer string.
294+
audience := fmt.Sprintf("%s/realms/%s", strings.TrimRight(ab.KeycloakURL, "/"), ab.KeycloakRealm)
295+
spiffeClient, err := workloadapi.New(ctx, workloadapi.WithAddr(socket))
296+
if err != nil {
297+
logger.Error(err, "failed to create SPIFFE workload API client")
294298
if r.Recorder != nil {
295-
r.Recorder.Eventf(owner, corev1.EventTypeWarning, "InvalidJWTSVIDPath",
296-
"JWT-SVID path %q rejected: must be under /opt/ or /var/run/secrets/", jwtSVIDPath)
299+
r.Recorder.Eventf(owner, corev1.EventTypeWarning, "SPIFFEClientFailed",
300+
"Failed to connect to SPIRE workload API at %s: %v", socket, err)
297301
}
298-
return ctrl.Result{}, err // fail permanently on config error
302+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
299303
}
304+
defer spiffeClient.Close()
300305

301-
jwtSVID, err := os.ReadFile(cleanPath)
306+
svid, err := spiffeClient.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience})
302307
if err != nil {
303-
logger.Error(err, "read JWT-SVID failed", "path", cleanPath)
308+
logger.Error(err, "failed to fetch JWT-SVID from SPIRE")
304309
if r.Recorder != nil {
305-
r.Recorder.Eventf(owner, corev1.EventTypeWarning, "JWTSVIDReadFailed",
306-
"Failed to read JWT-SVID from %s: %v. Check spiffe-helper sidecar configuration.", cleanPath, err)
310+
r.Recorder.Event(owner, corev1.EventTypeWarning, "JWTSVIDFetchFailed",
311+
"Failed to fetch JWT-SVID from SPIRE workload API. Check SPIRE is running and the operator has a registered identity.")
307312
}
308313
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
309314
}
310315

311-
// WARNING: JWT-SVID is a bearer token - must never appear in logs or error messages
312-
// to prevent token exposure. All code paths must handle jwtSVID as sensitive data.
313-
token, err = kc.JWTSVIDGrantToken(ctx, ab.KeycloakRealm, r.OperatorClientID, string(jwtSVID))
316+
// WARNING: JWT-SVID is a bearer token — must never appear in logs or error messages.
317+
token, err = kc.JWTSVIDGrantToken(ctx, ab.KeycloakRealm, r.OperatorClientID, svid.Marshal())
314318
if err != nil {
315319
logger.Error(err, "Keycloak JWT-SVID authentication failed")
316320
if r.Recorder != nil {
@@ -319,7 +323,7 @@ func (r *ClientRegistrationReconciler) reconcileOne(
319323
}
320324
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
321325
}
322-
logger.V(1).Info("authenticated with JWT-SVID", "clientId", r.OperatorClientID)
326+
logger.V(1).Info("authenticated with JWT-SVID via SPIRE workload API", "clientId", r.OperatorClientID)
323327
} else {
324328
// Admin credentials path (legacy)
325329
adminUser, adminPass, err := r.resolveKeycloakAdminCredentials(ctx)

0 commit comments

Comments
 (0)