Skip to content

Commit 30438f8

Browse files
authored
Merge pull request #483 from kagenti/fix/federated-jwt-client-id-txt
2 parents bcc883d + 60e1cc7 commit 30438f8

2 files changed

Lines changed: 30 additions & 34 deletions

File tree

kagenti-operator/internal/controller/clientregistration_controller.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -364,28 +364,22 @@ func (r *ClientRegistrationReconciler) reconcileOne(
364364
"clientId", clientID)
365365
}
366366

367-
// In federated-jwt mode, agents authenticate using their SPIFFE identity directly.
368-
// No credential secret is needed — AuthBridge reads JWT-SVIDs from the workload
369-
// API socket, not from a mounted secret. Skipping secret creation avoids leaving
370-
// unused Keycloak client secrets in agent namespaces.
367+
// In federated-jwt mode pass an empty clientSecret so ensureClientCredentialsSecret
368+
// writes only client-id.txt (not client-secret.txt). The inbound jwt-validation
369+
// plugin in AuthBridge reads client-id.txt to determine the expected audience; without
370+
// it, all inbound requests to the agent are rejected with 503 indefinitely.
371371
secretName := keycloakClientCredentialsSecretName(ns, workloadName)
372-
if authType != "federated-jwt" {
373-
if err := r.ensureClientCredentialsSecret(ctx, owner, secretName, clientID, clientSecret); err != nil {
374-
logger.Error(err, "ensure client credentials secret")
375-
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
376-
}
377-
// Annotate the pod template with the secret name so the webhook mounts it.
378-
if err := patchTemplate(ctx); err != nil {
379-
logger.Error(err, "patch workload pod template")
380-
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
381-
}
382-
} else {
383-
// In federated-jwt mode, AuthBridge reads JWT-SVIDs directly from the SPIFFE
384-
// workload API socket — no credential secret is needed or created. Skip both the
385-
// secret and the pod template annotation so the webhook does not try to mount a
386-
// non-existent secret.
387-
logger.V(1).Info("skipping credential secret and template patch (federated-jwt — AuthBridge uses SPIFFE identity directly)",
388-
"workload", workloadName, "namespace", ns)
372+
credSecret := clientSecret
373+
if authType == "federated-jwt" {
374+
credSecret = ""
375+
}
376+
if err := r.ensureClientCredentialsSecret(ctx, owner, secretName, clientID, credSecret); err != nil {
377+
logger.Error(err, "ensure client credentials secret")
378+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
379+
}
380+
if err := patchTemplate(ctx); err != nil {
381+
logger.Error(err, "patch workload pod template")
382+
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
389383
}
390384

391385
logger.Info("operator client registration applied",
@@ -571,6 +565,10 @@ func (r *ClientRegistrationReconciler) ensureClientCredentialsSecret(ctx context
571565
if sec.StringData == nil {
572566
sec.StringData = map[string]string{}
573567
}
568+
// In federated-jwt mode clientSecret is empty — agents authenticate via JWT-SVID.
569+
// We still write client-secret.txt (as an empty string) so the webhook's subPath
570+
// mount produces a file rather than a directory (Kubernetes creates a directory
571+
// when a subPath key is absent from the Secret).
574572
sec.StringData["client-secret.txt"] = clientSecret
575573
sec.StringData["client-id.txt"] = clientID
576574
return controllerutil.SetControllerReference(owner, sec, r.Scheme)

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,20 @@ func (w *AuthBridgeWebhook) Handle(ctx context.Context, req admission.Request) a
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.
9999
//
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.
100+
// In federated-jwt mode the Secret still exists (written by the operator with only
101+
// client-id.txt, no client-secret.txt) so the volume mount is still needed. The
102+
// inbound jwt-validation plugin reads client-id.txt to determine the expected audience;
103+
// without the mount the plugin polls forever and rejects all inbound traffic with 503.
103104
if pod.Annotations[injector.AnnotationKeycloakClientSecretName] == "" &&
104105
clientreg.WorkloadWantsOperatorClientReg(pod.Labels, w.Mutator.GetFeatureGates().InjectTools) {
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])
106+
if pod.Annotations == nil {
107+
pod.Annotations = map[string]string{}
115108
}
109+
pod.Annotations[injector.AnnotationKeycloakClientSecretName] =
110+
clientreg.KeycloakClientCredentialsSecretName(req.Namespace, resourceName)
111+
authbridgelog.Info("pre-populated Keycloak client credentials annotation",
112+
"namespace", req.Namespace, "name", resourceName,
113+
"secret", pod.Annotations[injector.AnnotationKeycloakClientSecretName])
116114
}
117115

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

0 commit comments

Comments
 (0)