feat: use go-spiffe SDK directly instead of spiffe-helper sidecar - #522
Conversation
Remove spiffe-helper sidecar from operator pod and use go-spiffe SDK directly to fetch JWT-SVIDs from the SPIRE Workload API. Changes: - operator/cmd/main.go: Remove --jwt-svid-path flag, pass SpiffeSocket instead of JWTSVIDPath to controller - operator/internal/controller/clientregistration_controller.go: Replace JWTSVIDPath field with SpiffeSocket, add fetchJWTSVID() helper that uses workloadapi.Client.FetchJWTSVID() to get JWT-SVID - charts/operator/templates/manager/manager.yaml: Remove spiffe-helper sidecar container, jwt-svid volume, and --jwt-svid-path CLI arg - charts/operator/templates/manager/configmap-spiffe-helper.yaml: Delete (no longer needed) This matches the architecture already used by authbridge-proxy and eliminates the need for maintaining a separate spiffe-helper sidecar. Closes: #478 Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Address errcheck linter: defer client.Close() now properly checks and logs any close errors. Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Add unit tests for fetchJWTSVID error handling: - Test invalid socket path - Test empty socket path - Document limitations of unit tests vs integration/E2E tests Add comprehensive testing guide documenting: - Unit test scope and limitations - Integration test requirements (future work) - E2E test procedures with SPIRE - Manual verification steps for token exchange - Common issues and troubleshooting Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
JWT-SVID audience must match the token endpoint URL for Keycloak
JWT-SPIFFE authentication, not just the realm name. Keycloak validates
the audience claim and rejects tokens with incorrect audience.
Changes fetchJWTSVID() call to construct the full token endpoint URL:
${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token
Fixes "Invalid token audience" error during operator authentication.
Assisted-By: Claude Code
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Update: Fixed JWT-SVID Audience IssueFound and fixed a critical bug during E2E testing: Problem: JWT-SVID was being fetched with Fix: Changed tokenEndpoint := strings.TrimSuffix(ab.KeycloakURL, "/") + "/realms/" + ab.KeycloakRealm + "/protocol/openid-connect/token"
jwtSVID, err := r.fetchJWTSVID(ctx, tokenEndpoint)Verification in progress:
Committed in 952a53d |
JWT-SVID audience must match Keycloak's realm issuer URL exactly per RFC 7523. The issuer is typically a public URL (e.g., keycloak.localtest.me) while the authbridge-config contains the in-cluster service address. Changes: - Add getKeycloakIssuer() to query /.well-known/openid-configuration - Use issuer URL as JWT-SVID audience instead of service URL - Add proper error handling and logging for issuer lookup Fixes "Invalid token audience" error during operator authentication. Ref: docs/users-guides/authentication.md lines 72-76 Assisted-By: Claude Code Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Critical Fix: JWT-SVID Audience Must Match Keycloak Issuer URLFound root cause of "Invalid token audience" error: Problem: JWT-SVID audience was using in-cluster service URL, but Keycloak validates against its public issuer URL. From authentication.md:
Solution: Query Keycloak's OIDC discovery endpoint to get authoritative issuer URL: func (r *ClientRegistrationReconciler) getKeycloakIssuer(ctx context.Context, keycloakURL, realm string) (string, error) {
discoveryURL := keycloakURL + "/realms/" + realm + "/.well-known/openid-configuration"
// Query and extract issuer field
return config.Issuer, nil // e.g., "http://keycloak.localtest.me:8080/realms/rossoctl"
}Testing with correct issuer URL now... Committed in afce861 |
Fixes errcheck linter error: Error return value of `resp.Body.Close` is not checked Assisted-By: Claude Code Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
Clean refactor that eliminates the spiffe-helper sidecar in favor of the go-spiffe SDK — good alignment with the authbridge-proxy pattern. A few items below.
Reviewed by clawgenti using the github-pr-review skill
| - name: jwt-svid | ||
| mountPath: /opt | ||
| readOnly: true | ||
| {{- end }} |
There was a problem hiding this comment.
nit: This {{- if and .Values.spiffe ... }}{{- end }} block is empty after removing the jwt-svid volume mount from inside it — it can be deleted.
| // Per RFC 7523 and Keycloak SPIFFE authentication: the JWT audience must match | ||
| // Keycloak's realm issuer URL exactly. Query the OIDC discovery endpoint to get | ||
| // the authoritative issuer value, since it may differ from the in-cluster service URL. | ||
| realmIssuer, err := r.getKeycloakIssuer(ctx, ab.KeycloakURL, ab.KeycloakRealm) |
There was a problem hiding this comment.
suggestion: getKeycloakIssuer is called on every reconcile when UseSpiffeAuth=true, creating a new http.Client and issuing an OIDC discovery request each time. The issuer URL is stable — consider caching it on the reconciler (e.g. a keycloakIssuer string field populated once on first successful fetch) to avoid per-reconcile HTTP overhead.
| } | ||
|
|
||
| jwtSVID, err := os.ReadFile(cleanPath) | ||
| jwtSVID, err := r.fetchJWTSVID(ctx, realmIssuer) |
There was a problem hiding this comment.
suggestion: fetchJWTSVID opens a new gRPC connection to the SPIRE Workload API on every call. Consider reusing a long-lived workloadapi.Client (stored on the reconciler and lazily initialized) to reduce connection churn on frequent reconciles, similar to how authbridge-proxy manages its SPIFFE client.
cwiklik
left a comment
There was a problem hiding this comment.
Solid, well-motivated change: dropping the spiffe-helper sidecar for the go-spiffe SDK (matches authbridge-proxy), and deriving the JWT-SVID audience from Keycloak's OIDC-discovery issuer is the correct, robust approach (Keycloak validates aud by string-equality against its own issuer). The token is never logged, all failure paths emit events + requeue, and Close() errors are checked.
One must-fix — a Helm gating regression (inline). The Workload API socket volume is declared for verifiedFetch.enabled OR operatorAuth.enabled (manager.yaml ~213), but the volumeMount into the manager container is gated on verifiedFetch.enabled only (~183), and the operatorAuth.enabled mount block (~188) is now empty. So enabling spiffe.operatorAuth.enabled without verifiedFetch.enabled creates the socket volume but never mounts it → the SDK's fetchJWTSVID dials a missing socket and client registration fails permanently. Pre-PR this worked because the spiffe-helper sidecar (gated on operatorAuth.enabled) carried the mount. CI/E2E is green because it enables both toggles, masking the gap. Fix: mount spiffe-workload-api under operatorAuth.enabled too (or use the same or condition on the mount, guarding a double-mount when both are on).
Nits: spiffe.operatorAuth.jwtSVIDPath (values.yaml:209) is now orphaned and its comment still says "written by spiffe-helper sidecar" — remove it; and two empty {{- if …operatorAuth.enabled }}{{- end }} template blocks remain in manager.yaml.
Suggestion (inline, non-blocking): getKeycloakIssuer does an OIDC-discovery HTTP round-trip and fetchJWTSVID opens a fresh Workload API client on every reconcile; the issuer is stable, so caching (and optionally reusing the client) would cut per-registration overhead.
Author: Alan-Cha (MEMBER — maintainer) · Areas: Go, Helm/K8s · .claude/.vscode: none · Commits: 7, signed-off · CI: passing.
Assisted-By: Claude Code
| // Per RFC 7523 and Keycloak SPIFFE authentication: the JWT audience must match | ||
| // Keycloak's realm issuer URL exactly. Query the OIDC discovery endpoint to get | ||
| // the authoritative issuer value, since it may differ from the in-cluster service URL. | ||
| realmIssuer, err := r.getKeycloakIssuer(ctx, ab.KeycloakURL, ab.KeycloakRealm) |
There was a problem hiding this comment.
suggestion (non-blocking) — getKeycloakIssuer runs an OIDC-discovery HTTP round-trip on every reconcileOne, and fetchJWTSVID opens+closes a fresh workloadapi.Client per call. Both are correct, but the issuer is stable — caching it (and optionally reusing the Workload API client) would remove a per-registration HTTP hop and connection churn.
Add caching to reduce HTTP and gRPC overhead on frequent reconciles: - getKeycloakIssuer(): Cache issuer URL by (keycloakURL, realm) using sync.Map. Issuer is stable per realm, no need to query OIDC discovery endpoint repeatedly. - fetchJWTSVID(): Reuse long-lived workloadapi.Client instead of creating new gRPC connection on every call. Lazily initialize with mutex protection. Addresses suggestions in PR #522 review from cwiklik. Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
Clean removal of the spiffe-helper sidecar in favour of direct go-spiffe SDK integration — the approach is sound and matches the authbridge-proxy pattern. Two related issues in fetchJWTSVID are worth addressing before merge.
Reviewed by clawgenti using the github-pr-review skill
| // Reuses a long-lived workloadapi.Client to avoid gRPC connection churn on frequent reconciles. | ||
| func (r *ClientRegistrationReconciler) fetchJWTSVID(ctx context.Context, audience string) (string, error) { | ||
| // Lazily initialize the workloadapi.Client | ||
| r.workloadAPIClientMu.Lock() |
There was a problem hiding this comment.
Mutex held during slow gRPC dial. workloadapi.New() makes a gRPC connection attempt which can block for the full context deadline. All concurrent reconciles will queue on this mutex during that window. Consider using sync.Once or releasing the lock, dialling outside the critical section, then re-acquiring to store — a standard double-checked lock:
r.workloadAPIClientMu.Lock()
if r.workloadAPIClient == nil {
r.workloadAPIClientMu.Unlock()
client, err := workloadapi.New(ctx, workloadapi.WithAddr(r.SpiffeSocket))
if err != nil {
return "", fmt.Errorf("failed to create SPIFFE Workload API client: %w", err)
}
r.workloadAPIClientMu.Lock()
if r.workloadAPIClient == nil {
r.workloadAPIClient = client
} else {
_ = client.Close()
}
}
client := r.workloadAPIClient
r.workloadAPIClientMu.Unlock()| Audience: audience, | ||
| }) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to fetch JWT-SVID: %w", err) |
There was a problem hiding this comment.
Stale client not retried on disconnect. If FetchJWTSVID returns an error due to a broken gRPC connection (SPIRE agent restarted, socket recycled), the cached workloadAPIClient is never replaced — every subsequent reconcile will fail with the same dead client until the operator pod restarts. Consider clearing r.workloadAPIClient = nil (under the mutex) when a fetch error is detected, so the next reconcile re-dials:
if err != nil {
r.workloadAPIClientMu.Lock()
if r.workloadAPIClient == client {
_ = r.workloadAPIClient.Close()
r.workloadAPIClient = nil
}
r.workloadAPIClientMu.Unlock()
return "", fmt.Errorf("failed to fetch JWT-SVID: %w", err)
}Address review feedback from clawgenti: 1. Mutex held during slow gRPC dial: Use double-checked locking to release mutex before calling workloadapi.New(). This prevents blocking all concurrent reconciles during the connection attempt. 2. Stale client not retried on disconnect: Clear cached client when FetchJWTSVID() fails, so next reconcile re-dials instead of reusing a dead connection. Both fixes follow the suggested patterns from PR #522 review. Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
clawgenti
left a comment
There was a problem hiding this comment.
The latest commit correctly addresses both items from the previous review: double-checked locking prevents holding the mutex during the slow gRPC dial, and the stale-client clear-on-error means a dead connection is evicted rather than reused. The Helm spiffe-workload-api volumeMount is now gated on verifiedFetch.enabled OR operatorAuth.enabled (manager.yaml:183), which closes the socket-created-but-not-mounted regression cwiklik flagged. One minor nit below.
Reviewed by clawgenti using the github-pr-review skill
| return "", fmt.Errorf("failed to create OIDC discovery request: %w", err) | ||
| } | ||
|
|
||
| client := &http.Client{Timeout: 10 * time.Second} |
There was a problem hiding this comment.
nit: getKeycloakIssuer allocates a fresh http.Client on every invocation. In practice this runs at most once per (keycloakURL, realm) pair thanks to the sync.Map cache, so it is not a hot path — but a struct-level httpClient *http.Client initialized in SetupWithManager (or a package-level var) would make the intent clearer and shave a tiny allocation. Non-blocking.
mrsabath
left a comment
There was a problem hiding this comment.
Clean, well-motivated refactor: dropping the spiffe-helper sidecar for the go-spiffe SDK matches the authbridge-proxy pattern, and deriving the JWT-SVID audience from Keycloak's OIDC-discovery issuer is the correct approach (Keycloak validates aud by string-equality against its own advertised issuer). The token is never logged, all failure paths emit events and requeue, and Close() errors are checked.
The Helm gating regression and orphaned jwtSVIDPath value flagged on the earlier commit are both already fixed on HEAD — the socket volumeMount and volume now share the same or condition, and the value is removed. Thanks for that.
One must-fix remains around the cached Workload API client (inline). clawgenti already surfaced the same client-lifecycle points; flagging them here too since the stale-client case is a permanent failure mode worth blocking on.
Areas reviewed: Go, Helm/K8s, values.yaml, tests. Commits: 8, all signed-off. CI: all green.
| svid, err := client.FetchJWTSVID(ctx, jwtsvid.Params{ | ||
| Audience: audience, | ||
| }) | ||
| if err != nil { |
There was a problem hiding this comment.
must-fix — Stale client never recovered on fetch error. If FetchJWTSVID fails because the gRPC connection is broken (SPIRE agent restarted, socket recycled), the cached workloadAPIClient is never replaced, so every subsequent reconcile fails with the same dead client until the operator pod restarts — a permanent failure mode. Clear the cached client (under the mutex, guarding == client) before returning:
if err != nil {
r.workloadAPIClientMu.Lock()
if r.workloadAPIClient == client {
_ = r.workloadAPIClient.Close()
r.workloadAPIClient = nil
}
r.workloadAPIClientMu.Unlock()
return "", fmt.Errorf("failed to fetch JWT-SVID: %w", err)
}| // Reuses a long-lived workloadapi.Client to avoid gRPC connection churn on frequent reconciles. | ||
| func (r *ClientRegistrationReconciler) fetchJWTSVID(ctx context.Context, audience string) (string, error) { | ||
| // Lazily initialize the workloadapi.Client | ||
| r.workloadAPIClientMu.Lock() |
There was a problem hiding this comment.
suggestion — workloadapi.New() performs a gRPC dial while workloadAPIClientMu is held, so all concurrent reconciles serialize behind the full dial window (up to the context deadline on a slow/unavailable agent). Consider a double-checked lock: dial outside the critical section, then re-acquire to store, Close()-ing the loser on a race.
| // Lazily initialize the workloadapi.Client | ||
| r.workloadAPIClientMu.Lock() | ||
| if r.workloadAPIClient == nil { | ||
| client, err := workloadapi.New(ctx, workloadapi.WithAddr(r.SpiffeSocket)) |
There was a problem hiding this comment.
suggestion — the client is created with the reconcile-scoped ctx. When that reconcile's context is cancelled, a long-lived client tied to it can be torn down out from under later reconciles. Consider dialing with a manager-scoped / background context so the cached client outlives any single reconcile.
| readOnly: true | ||
| {{- end }} | ||
| {{- if .Values.verifiedFetch.enabled }} | ||
| {{- if or .Values.verifiedFetch.enabled (and .Values.spiffe .Values.spiffe.enabled .Values.spiffe.operatorAuth .Values.spiffe.operatorAuth.enabled) }} |
There was a problem hiding this comment.
suggestion (related to the arg block above) — the volume/mount are now correctly gated on verifiedFetch.enabled OR operatorAuth.enabled, but the --verified-fetch-spiffe-socket arg (line ~49) is still rendered only when verifiedFetch.enabled. The controller wires SpiffeSocket from that flag, so with spiffe.operatorAuth.enabled alone the flag is absent and it falls back to the Go default (unix:///spiffe-workload-api/spire-agent.sock). It works today only because that default matches verifiedFetch.spiffeEndpointSocket; a user who customizes that socket path would silently diverge. Consider rendering the socket arg under the same or condition.
1. Use background context for workloadapi.New() to prevent client teardown when reconcile context is cancelled. The cached client should outlive any single reconcile. 2. Reuse http.Client for OIDC discovery requests (initialized in SetupWithManager) instead of allocating fresh on every call. Saves a tiny allocation and makes intent clearer. 3. Render --verified-fetch-spiffe-socket flag when operatorAuth is enabled, not just verifiedFetch. Uses same OR condition as the volume mount. Prevents silent divergence if user customizes spiffeEndpointSocket. Addresses suggestions from PR #522 review. Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Addressed review feedbackCommit 1703081 (response to first round):
Commit cde78f3 (response to latest comments):
All suggestions implemented. Ready for re-review. |
mrsabath
left a comment
There was a problem hiding this comment.
Summary
Clean, well-motivated change that removes the spiffe-helper sidecar and fetches JWT-SVIDs directly via the go-spiffe SDK, matching the authbridge-proxy architecture. I re-checked each prior-round concern against the current PR head (cde78f3):
- socket mount (cwiklik must-fix): resolved. The
spiffe-workload-apivolumeMount now usesor verifiedFetch.enabled (and ...operatorAuth.enabled), consistent with the volume declaration. - mutex held during slow gRPC dial (clawgenti): resolved.
fetchJWTSVIDdials outside the critical section with correct double-checked locking, and dials withcontext.Background()so the cached client outlives any single reconcile; the losing goroutine closes its client. - stale client not retried (clawgenti): resolved. On fetch error the cached client is closed and nilled under an identity check (
r.workloadAPIClient == client), so the next reconcile re-dials. - OIDC discovery every reconcile: resolved.
getKeycloakIssuercaches by(keycloakURL, realm)in async.Map.
The JWT-SVID is properly treated as a bearer secret (never logged). No spiffe-helper references linger in the tree. My only substantive point is the implicit socket-path coupling between operator-auth and verified-fetch (inline comment on main.go); it works today because the Go default matches the CSI mountPath, but the coupling is fragile. The other two comments are non-blocking.
Areas reviewed: Go (controller, main wiring, go-spiffe SDK), Helm/K8s (manager.yaml, values.yaml), Tests, Security (bearer-token handling), commit/PR conventions.
Commits: 9, all signed-off. CI: all green (Build, Lint, Unit, Integration, CodeQL, Trivy, Helm lint); E2E pending maintainer trigger.
Verdict: APPROVE — the prior CHANGES_REQUESTED items are resolved; remaining comments are a hardening suggestion and a nit.
Note: this approval does not dismiss @cwiklik's standing CHANGES_REQUESTED — that would need their re-review to clear.
| KeycloakAdminTokenCache: &keycloak.CachedAdminTokenProvider{}, | ||
| UseSpiffeAuth: useSpiffeAuth, | ||
| JWTSVIDPath: jwtSVIDPath, | ||
| SpiffeSocket: verifiedFetchSpiffeSocket, |
There was a problem hiding this comment.
suggestion: SpiffeSocket for operator JWT-SVID auth is wired to verifiedFetchSpiffeSocket, and the --verified-fetch-spiffe-socket arg is emitted only under verifiedFetch.enabled (manager.yaml:49-51), while JWT-SVID auth is gated on the separate operatorAuth.enabled. When operatorAuth.enabled=true + verifiedFetch.enabled=false, the flag isn't passed and the code relies on the Go default unix:///spiffe-workload-api/spire-agent.sock matching the CSI mountPath. It does today, so this works, but the coupling is implicit and fragile: change verifiedFetch.spiffeEndpointSocket or the default and the two features silently diverge. Consider a dedicated --operator-spiffe-socket (or emit the socket arg under the combined condition) and rename the reused variable so the operator-auth path doesn't depend on a verified-fetch-named default.
| // fetchJWTSVID fetches a JWT-SVID from the SPIRE Workload API for the given audience. | ||
| // Returns the JWT token as a string or an error if fetching fails. | ||
| // Reuses a long-lived workloadapi.Client to avoid gRPC connection churn on frequent reconciles. | ||
| func (r *ClientRegistrationReconciler) fetchJWTSVID(ctx context.Context, audience string) (string, error) { |
There was a problem hiding this comment.
suggestion (non-blocking): the cached workloadAPIClient is only closed on fetch error, never on operator shutdown. A single leaked gRPC conn for process lifetime is minor (freed on exit), but if the manager supports graceful stop, closing it in a cleanup hook would be tidy. The dial-with-context.Background() and double-checked locking here look correct.
| "time" | ||
| ) | ||
|
|
||
| func TestFetchJWTSVID_InvalidSocketPath(t *testing.T) { |
There was a problem hiding this comment.
nit: the comment says "client creation failure," but workloadapi.New uses non-blocking grpc.DialContext (no WithBlock), so New succeeds and the error actually surfaces from FetchJWTSVID blocking to the 2s ctx timeout. The test passes, but for a different reason than documented. Consider correcting the comment, or asserting the error is a deadline/connection error to make the intent precise.
mrsabath
left a comment
There was a problem hiding this comment.
Re-reviewed against cde78f3. All items from my previous review are addressed cleanly:
- must-fix (stale client) — resolved:
fetchJWTSVIDnow clears the cached client under the mutex (guarded on== client) on fetch error, so the next reconcile re-dials after a SPIRE agent restart. - double-checked locking — resolved: the gRPC dial happens outside the mutex, with a re-acquire to store and
Close()on the losing race. - reconcile-scoped ctx — resolved: dials with
context.Background()so the cached client outlives a single reconcile. - socket arg gating — resolved:
--verified-fetch-spiffe-socketis now rendered under the sameverifiedFetch.enabled OR operatorAuth.enabledcondition as the volume/mount.
Bonus: the OIDC-discovery http.Client is now hoisted to a reusable field initialized in SetupWithManager. Lock discipline checks out (no double-unlock, no leaked lock on the dial-error path), and r.httpClient is always initialized before any reconcile.
No new issues. Areas re-reviewed: Go, Helm. CI green except E2E still pending (passed on the prior equivalent commit).
clawgenti
left a comment
There was a problem hiding this comment.
Good progress — all the must-fix items from previous rounds (socket mount gating, mutex-during-dial, stale-client recovery, OIDC discovery caching) are cleanly resolved on HEAD. One latent bug remains around SpiffeSocket argument wiring, plus two nits.
Reviewed by clawgenti using the github-pr-review skill
| KeycloakAdminTokenCache: &keycloak.CachedAdminTokenProvider{}, | ||
| UseSpiffeAuth: useSpiffeAuth, | ||
| JWTSVIDPath: jwtSVIDPath, | ||
| SpiffeSocket: verifiedFetchSpiffeSocket, |
There was a problem hiding this comment.
bug: SpiffeSocket is always wired from verifiedFetchSpiffeSocket, but --verified-fetch-spiffe-socket is only emitted in manager.yaml when verifiedFetch.enabled=true (line ~51). When spiffe.operatorAuth.enabled=true and verifiedFetch.enabled=false, the socket arg is never passed and SpiffeSocket is an empty string — fetchJWTSVID will fail on every reconcile with "SpiffeSocket is required". The volume and mount were correctly extended to cover operatorAuth.enabled, but the CLI arg block was not. Fix: emit --verified-fetch-spiffe-socket also under the operatorAuth.enabled branch (or share the same or condition as the volume).
| "time" | ||
| ) | ||
|
|
||
| func TestFetchJWTSVID_InvalidSocketPath(t *testing.T) { |
There was a problem hiding this comment.
nit: the comment on line 10 says "expected error when connecting to nonexistent socket" and implies workloadapi.New is the failure point, but workloadapi.New uses a non-blocking grpc.DialContext (no WithBlock) so New succeeds immediately. The error is actually returned by the subsequent FetchJWTSVID call once the 2-second context deadline expires. The test is correct; the comment (and the check on line 23–26) should reference the fetch failing, not client creation.
| // fetchJWTSVID fetches a JWT-SVID from the SPIRE Workload API for the given audience. | ||
| // Returns the JWT token as a string or an error if fetching fails. | ||
| // Reuses a long-lived workloadapi.Client to avoid gRPC connection churn on frequent reconciles. | ||
| func (r *ClientRegistrationReconciler) fetchJWTSVID(ctx context.Context, audience string) (string, error) { |
There was a problem hiding this comment.
suggestion (non-blocking): the cached workloadAPIClient is closed on fetch error but never on operator shutdown. A single leaked gRPC connection for the process lifetime is harmless (freed on exit), but adding a cleanup hook (e.g., mgr.Add(manager.RunnableFunc(...)) in SetupWithManager) would be tidy and matches the pattern expected by graceful-stop tests.
Summary
Remove spiffe-helper sidecar from operator pod and use go-spiffe SDK directly to fetch JWT-SVIDs from the SPIRE Workload API.
Changes
--jwt-svid-pathflag, pass SpiffeSocket to controller--jwt-svid-pathCLI argMotivation
This matches the architecture already used by authbridge-proxy and eliminates the need for maintaining a separate spiffe-helper sidecar. The go-spiffe SDK provides direct Workload API access, making the file-based JWT-SVID exchange unnecessary.
Testing
Closes
Replaces #478 with a clean implementation.
Assisted-By: Claude Code