Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -364,28 +364,22 @@ func (r *ClientRegistrationReconciler) reconcileOne(
"clientId", clientID)
}

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

logger.Info("operator client registration applied",
Expand Down Expand Up @@ -571,6 +565,10 @@ func (r *ClientRegistrationReconciler) ensureClientCredentialsSecret(ctx context
if sec.StringData == nil {
sec.StringData = map[string]string{}
}
// In federated-jwt mode clientSecret is empty — agents authenticate via JWT-SVID.
// We still write client-secret.txt (as an empty string) so the webhook's subPath
// mount produces a file rather than a directory (Kubernetes creates a directory
// when a subPath key is absent from the Secret).
sec.StringData["client-secret.txt"] = clientSecret
sec.StringData["client-id.txt"] = clientID
return controllerutil.SetControllerReference(owner, sec, r.Scheme)
Expand Down
24 changes: 11 additions & 13 deletions kagenti-operator/internal/webhook/v1alpha1/authbridge_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,20 @@ func (w *AuthBridgeWebhook) Handle(ctx context.Context, req admission.Request) a
// Without this, the first pod comes up with an empty /shared/ and envoy returns 503
// "identity not yet configured (credentials pending)" until the user deletes the pod.
//
// Skip in federated-jwt mode: AuthBridge reads JWT-SVIDs from the SPIFFE workload API
// socket directly and no credential Secret is created or needed. Injecting the volume
// mount for a non-existent Secret would leave pods stuck in Init.
// In federated-jwt mode the Secret still exists (written by the operator with only

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this comment says the Secret is "written by the operator with only client-id.txt, no client-secret.txt", but the controller change in this same PR writes client-secret.txt as an empty string rather than omitting it (that's the whole point of the subPath fix). Suggest rewording to "written with client-id.txt and an empty client-secret.txt" so it matches the actual behavior and the controller-side comment.

// client-id.txt, no client-secret.txt) so the volume mount is still needed. The
// inbound jwt-validation plugin reads client-id.txt to determine the expected audience;
// without the mount the plugin polls forever and rejects all inbound traffic with 503.
if pod.Annotations[injector.AnnotationKeycloakClientSecretName] == "" &&
clientreg.WorkloadWantsOperatorClientReg(pod.Labels, w.Mutator.GetFeatureGates().InjectTools) {
nsConfig, _ := injector.ReadNamespaceConfig(ctx, w.Mutator.APIReader, req.Namespace)
if nsConfig == nil || nsConfig.ClientAuthType != injector.ClientAuthTypeFederatedJWT {
if pod.Annotations == nil {
pod.Annotations = map[string]string{}
}
pod.Annotations[injector.AnnotationKeycloakClientSecretName] =
clientreg.KeycloakClientCredentialsSecretName(req.Namespace, resourceName)
authbridgelog.Info("pre-populated Keycloak client credentials annotation",
"namespace", req.Namespace, "name", resourceName,
"secret", pod.Annotations[injector.AnnotationKeycloakClientSecretName])
if pod.Annotations == nil {
pod.Annotations = map[string]string{}
}
pod.Annotations[injector.AnnotationKeycloakClientSecretName] =
clientreg.KeycloakClientCredentialsSecretName(req.Namespace, resourceName)
authbridgelog.Info("pre-populated Keycloak client credentials annotation",
"namespace", req.Namespace, "name", resourceName,
"secret", pod.Annotations[injector.AnnotationKeycloakClientSecretName])
}

// Check if already injected (idempotency / reinvocation)
Expand Down
Loading