TL;DR
| |
Delete an annotated pod and check that the injector gets the call:
| |
The symptom
- The pod carries
vault.hashicorp.com/agent-inject: "true"and the rest of the annotations, but starts with the application container only: novault-agent-init, novault-agent. - Creating the pod takes about 30 seconds longer than usual.
kubectl get eventssays nothing about the webhook. The injector logs do not have a singleRequest receivedline.- On other clusters (public zonal, kind, k3s) the same chart and the same annotations work.
Why
On a private GKE cluster the control plane lives in a Google-owned VPC and reaches your nodes through the firewall rule GKE creates by itself (gke-<cluster>-<hash>-master). That rule only allows TCP 443 and 10250 from the control plane range to the nodes. Everything else is closed.
An admission webhook does not go through the Service like a normal client would: the API server resolves the Service to its endpoints and connects straight to the pod IP on the targetPort. The injector’s Service (vault-agent-injector-svc) exposes 443, but the pod listens on 8080 (injector.port in the chart), so the actual connection goes to 8080 and the firewall drops it.
What happens next is explained by two chart defaults:
| Value | Default | Effect |
|---|---|---|
injector.webhook.timeoutSeconds | 30 | The API server waits 30 s for the webhook’s answer |
injector.webhook.failurePolicy | Ignore | If the webhook does not answer, the pod is created unmutated, with no error |
Hence the 30-second delay and the pod with no sidecar and no message. With failurePolicy: Fail you would have seen failed calling webhook "vault.hashicorp.com": … context deadline exceeded in the ReplicaSet events, which at least points at the right place.
Not just Vault
Any admission webhook, CRD conversion webhook or aggregated API listening on a port other than 443 or 10250 fails the same way on private GKE: the OpenTelemetry operator (9443), Kyverno (9443), Gatekeeper (8443)… cert-manager listens on 10250 precisely to avoid needing this rule. The GKE documentation lists it as a requirement for “admission webhooks, aggregated API servers, webhook conversion”: one rule per port, from masterIpv4CidrBlock to the node tags.
If the cluster lives in Terraform, keep the rule next to the cluster so it survives a rebuild:
| |
Traps
- The wrong tag: nodes carry
gke-<cluster>-<hash>-node; copy it from the existinggke-<cluster>-*rules instead of typing it. With several node pools, check that all of them have the tag. - The control plane range changes if you recreate the cluster; a hand-written rule with the old CIDR silently stops working.
failurePolicy: Failmakes the failure visible, but while the webhook is down no pod gets created in the namespaces matching thenamespaceSelector; the chart excludes the injector itself, but not Vault if they share namespace and labels. Use it withreplicas: 2.- The injector does receive the request and there is still no sidecar: then it is no longer the network. Read the
vault-agent-initlogs:permission deniedis the Kubernetes auth role (bound_service_account_names,bound_service_account_namespaces) or the policy;connection refusedisvault.hashicorp.com/serviceor the Vault address. - One port per rule does not scale: as webhooks pile up, one rule with
--rules=tcp:8080,8443,9443from the control plane is easier to maintain than five.
References: vault-k8s #46, vault-k8s #32 and firewall rules on private clusters in the GKE documentation.
See also
- cert-manager: the HTTP-01 challenge returns 308 — another component that “does nothing” until you look in the right place.
- GitHub Actions → Google Cloud without keys — to deploy to that GKE without JSON keys.