You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ClientRegistration controller and AuthBridge webhook disagree on serviceAccountName: bare Deployments never get credentials Secret (silent, permanent) #513
With SPIRE enabled, any workload deployed as a plain Deploymentwithout an
explicit dedicated serviceAccountName in its pod template never receives its
per-agent Keycloak client-credentials Secret. The pod stays Pending on FailedMountpermanently. Nothing surfaces an error: no Event, no status
condition, only an Info-level log line and a 30s requeue loop that never
converges.
This is deterministic and reproducible — not the intermittent Keycloak client
registration that e2e-kind.yaml works around with a wait loop and continue-on-error, and not related to the k8s.keycloak.org CRD (this path uses
Keycloak's admin REST API directly).
Root cause: two halves of the feature read different objects
Component
Action
Value it sees
internal/webhook/injector/pod_mutator.go:191-198
Creates a dedicated SA and sets podSpec.ServiceAccountName = crName on the admitted Pod; pre-populates the …/keycloak-client-credentials-secret-name annotation
Reads template.Spec.ServiceAccountName from the Deployment pod template, which the webhook never patches
"" → "default" ❌
resolveKeycloakClientID() (:449-464) then returns an error, and reconcileOne
logs and requeues:
"msg":"cannot resolve Keycloak client id yet"
"reason":"SPIRE enabled: set spec.template.spec.serviceAccountName to a dedicated
ServiceAccount (not default) on the workload for a stable SPIFFE client ID"
So the webhook has already promised the pod a Secret (by name, via the annotation)
that the controller has just declined to create. internal/clientreg/names.go's
package doc states its purpose is keeping these two sides in lockstep — they agree
on the Secret name and disagree on the input.
The webhook's own fixup is the tell: it knows the SA must not be default, creates
one, and assigns it — but only on the Pod, which the controller never reads.
Reproduction
Verified on a Kind cluster, both directions.
Namespace with SPIRE_ENABLED=true in authbridge-config.
Apply a plain Deployment with noserviceAccountName, plus an AgentRuntime with targetRef pointing at it.
Observe: controller logs cannot resolve Keycloak client id yet with the reason
above and requeues every 30s. Zero credentials Secrets in the namespace. Pod Pending, FailedMount on the annotation-named Secret. State is stable
indefinitely.
Secret appears within 20s — with the exact name hash the webhook had already
pre-populated — and the pod reaches Ready ~10s later.
Contrast: 74-deploy-weather-agent.sh creates a dedicated SA explicitly, with the
comment "required by webhook for correct SPIFFE ID derivation". That is why weather-service works while bare Deployments silently do not.
Impact
Any consumer deploying an agent as a plain Deployment hits this with no usable
signal. The visible symptom (FailedMount on a missing Secret) points at
Keycloak or the webhook, not at a serviceAccountName default.
Any one of these closes the silent-permanent-failure class; the third is worth
doing regardless:
Controller resolves the effective SA the way the webhook does — fall back to
the workload-name SA when the template says ""/default and SPIRE is on,
rather than refusing.
Webhook patches the Deployment template, not just the pod, so the two sides
read the same value.
Surface it. A permanent refusal should not be an Info line inside a 30s
requeue. Emit an Event on the workload and/or set a status condition, so the
symptom names its own cause.
Minor, related
Per-agent ServiceAccounts created by ensureServiceAccount
(pod_mutator.go:835) carry no ownerReference, so nothing garbage-collects them
when the workload goes away. Eight had accumulated in two namespaces over one
development cycle. (The credentials Secret is correctly owner-referenced to the
Deployment and does self-GC.)
Summary
With SPIRE enabled, any workload deployed as a plain
Deploymentwithout anexplicit dedicated
serviceAccountNamein its pod template never receives itsper-agent Keycloak client-credentials Secret. The pod stays
PendingonFailedMountpermanently. Nothing surfaces an error: no Event, no statuscondition, only an
Info-level log line and a 30s requeue loop that neverconverges.
This is deterministic and reproducible — not the intermittent Keycloak client
registration that
e2e-kind.yamlworks around with a wait loop andcontinue-on-error, and not related to thek8s.keycloak.orgCRD (this path usesKeycloak's admin REST API directly).
Root cause: two halves of the feature read different objects
internal/webhook/injector/pod_mutator.go:191-198podSpec.ServiceAccountName = crNameon the admitted Pod; pre-populates the…/keycloak-client-credentials-secret-nameannotationinternal/controller/clientregistration_controller.go:247template.Spec.ServiceAccountNamefrom the Deployment pod template, which the webhook never patches""→"default"❌resolveKeycloakClientID()(:449-464) then returns an error, andreconcileOnelogs and requeues:
So the webhook has already promised the pod a Secret (by name, via the annotation)
that the controller has just declined to create.
internal/clientreg/names.go'spackage doc states its purpose is keeping these two sides in lockstep — they agree
on the Secret name and disagree on the input.
The webhook's own fixup is the tell: it knows the SA must not be
default, createsone, and assigns it — but only on the Pod, which the controller never reads.
Reproduction
Verified on a Kind cluster, both directions.
SPIRE_ENABLED=trueinauthbridge-config.Deploymentwith noserviceAccountName, plus anAgentRuntimewithtargetRefpointing at it.cannot resolve Keycloak client id yetwith the reasonabove and requeues every 30s. Zero credentials Secrets in the namespace. Pod
Pending,FailedMounton the annotation-named Secret. State is stableindefinitely.
kubectl patch deploy <name> --type=merge -p '{"spec":{"template":{"spec":{"serviceAccountName":"<name>"}}}}'pre-populated — and the pod reaches Ready ~10s later.
Contrast:
74-deploy-weather-agent.shcreates a dedicated SA explicitly, with thecomment "required by webhook for correct SPIFFE ID derivation". That is why
weather-serviceworks while bare Deployments silently do not.Impact
signal. The visible symptom (
FailedMounton a missing Secret) points atKeycloak or the webhook, not at a
serviceAccountNamedefault.transparent_inboundskipped onevery shared Kind run since they merged, and the failure looked like flaky client
registration for weeks. Fixed on the test side by adding the SA
(test(e2e): cover inbound mTLS and the ambient/HBONE path in transparent inbound rossoctl#2404), which is only the half a caller controls.
Suggested fixes
Any one of these closes the silent-permanent-failure class; the third is worth
doing regardless:
the workload-name SA when the template says
""/defaultand SPIRE is on,rather than refusing.
read the same value.
Infoline inside a 30srequeue. Emit an Event on the workload and/or set a status condition, so the
symptom names its own cause.
Minor, related
Per-agent ServiceAccounts created by
ensureServiceAccount(
pod_mutator.go:835) carry noownerReference, so nothing garbage-collects themwhen the workload goes away. Eight had accumulated in two namespaces over one
development cycle. (The credentials Secret is correctly owner-referenced to the
Deployment and does self-GC.)
Assisted-By: Claude Code