Feat(operator): implement SPIFFE JWT-SVID authentication for client registration#473
Merged
Conversation
…egistration 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 <alan.cha@ibm.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
…ation 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 <alan.cha@ibm.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
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 <alan.cha@ibm.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
esnible
approved these changes
Jul 7, 2026
esnible
left a comment
Member
There was a problem hiding this comment.
Clean, well-scoped implementation of JWT-SVID operator auth. The dual-auth split is correct: both paths converge on RegisterOrFetchClientWithToken with identical params, the SPIFFE path is fully feature-gated (spiffe.enabled + spiffe.operatorAuth.enabled, both default false), and legacy admin-credential auth is untouched as the default.
Verified against the branch:
- All reused helpers (
trimBaseURL,truncate,httpc,adminTokenResponse) and imports already exist inadmin.go. corev1is imported andowneris in scope for allRecorder.Eventcalls.- The
spiffe-workload-apiCSI volume guard is correctly widened toor verifiedFetch.enabled (spiffe operatorAuth), so the sidecar's mount is satisfied. - Path-traversal protection (
filepath.Clean+/opt/,/var/run/secrets/prefix whitelist) is sound against..sequences. - Error classification is appropriate: permanent failure on bad config path, transient requeue on read/auth failure. JWT-SVID is never logged.
All 16 CI checks green (including E2E), all commits signed-off. Two non-blocking suggestions inline.
Assisted-By: Claude Code
- 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 <alan.cha@ibm.com>
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #1421 — eliminates admin credentials from operator client registration
by using the operator's SPIFFE identity (JWT-SVID) to authenticate with the
Keycloak Admin API.
Problem
The operator currently uses admin credentials to register OAuth clients. This means:
Solution
The operator authenticates with its SPIFFE JWT-SVID via Keycloak's
federated-jwtclient authenticator, using only the scoped
manage-clientsrole.Architecture
Changes
Go (
internal/keycloak/admin.go)New
JWTSVIDGrantToken()method authenticates the operator using JWT-SVID.Go (
internal/controller/clientregistration_controller.go)Dual authentication path:
UseSpiffeAuth=true: reads JWT-SVID from file, callsJWTSVIDGrantToken()UseSpiffeAuth=false: admin credentials (legacy, default)Security: path traversal protection (
filepath.Clean+ whitelist), JWT-SVIDnever logged, all failures surface as Kubernetes Events.
Go (
cmd/main.go)Registers
--use-spiffe-auth,--jwt-svid-path,--operator-client-idflags.Helm (
charts/kagenti-operator/)configmap-spiffe-helper.yaml(new): configures spiffe-helper to writeJWT-SVID to
/opt/jwt_svid.token. Setspiffe.operatorAuth.jwtAudienceexplicitly — the fallback uses
keycloak.publicUrlwhich defaults to empty.manager.yaml: conditionally adds spiffe-helper sidecar (UID 65532, matchingthe manager so the 600-mode file is readable), jwt-svid emptyDir volume, SPIFFE
flags — all gated on
spiffe.operatorAuth.enabled.values.yaml: newspiffe:block, bothenabledandoperatorAuth.enableddefault to
false.Security improvements
manage-*rolesmanage-clientsonlyE2E testing
Complete — multiple fresh-cluster runs, all 9 steps passed.
See https://gist.github.com/Alan-Cha/527be60c79b40aedd54fe7be586c3452
Related
Checklist
UseSpiffeAuth=falseuses admin credentialsAssisted-By: Claude Code (Anthropic AI) noreply@anthropic.com