BlueByte
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

By Haneul SeoUpdated September 26, 20268 min

Hi, it's BlueByte. A kubectl apply that worked yesterday now fails in under a second with Internal error occurred: failed calling webhook, and nothing in your manifest changed. Your object was never actually evaluated: an admission webhook sits in front of that write, the API server could not get an answer out of it, and the default failure policy turned that silence into a rejection. We'll walk through the shape of the message, why a webhook you may not have installed yourself can block your writes, how to tell a network block from a certificate mismatch, the fix for each cause, and how to scope the webhook so it cannot lock you out again.

What the API server is actually reporting

The failure comes back as a server-side error on any verb the webhook matches:

Error from server (InternalError): error when creating "ingress.yaml": Internal error occurred: failed calling webhook "validate.example.com": failed to call webhook: Post "https://webhook-service.webhook-system.svc:443/validate?timeout=10s": context deadline exceeded

Three fragments carry all the information. The quoted webhook name identifies one entry inside a ValidatingWebhookConfiguration or MutatingWebhookConfiguration. The URL is assembled from clientConfig.service — namespace, name and port, where the documented defaults are port 443 and path /. The tail after the final colon is the diagnosis, and it arrives in a small set of shapes:

  • context deadline exceeded — the call was dispatched and no reply came back in time.
  • connect: connection refused — something is routable at that address and nothing is listening on the port.
  • no endpoints available for service "webhook-service" — the Service exists but has no ready backends.
  • x509: certificate signed by unknown authority — the API server does not trust the certificate the webhook served.
  • x509: certificate is valid for ..., not webhook-service.webhook-system.svc — the certificate is trusted but carries the wrong name.

Read that tail first. Everything below branches on it.

Why a webhook that cannot answer rejects your write

Each webhook entry carries a failurePolicy, and the Kubernetes reference is explicit that the default is Fail: an error calling the webhook causes admission failure and the request is rejected. Ignore is the other value, and it lets the request proceed. The policy covers network errors, timeouts, non-2xx responses, malformed responses and serialization failures — every way of not getting a usable answer.

The clock is timeoutSeconds, documented with an allowed range of 1 to 30 seconds and a default of 10. When it expires, the call is ignored or the request is rejected according to that same failure policy.

One distinction saves a lot of wasted debugging: a webhook that runs and says no is a different event entirely. The reference notes that an explicit rejection, correctly transmitted, always denies the API request regardless of the failurePolicy setting. That message reads admission webhook "..." denied the request, not failed calling webhook.

Four things that stop the API server reaching the webhook

  • No ready backends. The webhook Deployment was scaled to zero, evicted, or is crash-looping. The Service resolves, the endpoint list is empty.
  • A blocked network path. On a managed cluster the control plane lives outside your VPC and reaches the nodes through a narrow hole. Google's GKE guidance states that by default the firewall does not allow TCP connections to nodes except on ports 443 (HTTPS) and 10250 (kubelet), and that an admission webhook trying to reach a Pod on a different port fails without a custom firewall rule.
  • Broken TLS trust. caBundle is the PEM-encoded, base64-wrapped CA the API server validates against, and the serving certificate must be valid for <svc_name>.<svc_namespace>.svc. A regenerated cert, an expired one, or a re-created Secret breaks the pair.
  • The webhook is up but slow. A handler that calls an external API or a database can exceed timeoutSeconds under load while looking perfectly healthy in its own logs.

There is a fifth case worth naming because it is self-inflicted: a webhook that validates the namespace it runs in cannot be restarted once it is down, because its own Pod creation is blocked by itself.

Check the three things the tail points at

Start by listing what is actually in the admission path, with its policy and timeout:

kubectl get validatingwebhookconfigurations -o custom-columns=NAME:.metadata.name,HOOKS:.webhooks[*].name,POLICY:.webhooks[*].failurePolicy,TIMEOUT:.webhooks[*].timeoutSeconds
NAME                HOOKS                  POLICY   TIMEOUT
example-admission   validate.example.com   Fail     10

Then ask whether anything is behind the Service named in the URL:

kubectl -n webhook-system get endpoints webhook-service
kubectl -n webhook-system get pods -l app=webhook-server
NAME              ENDPOINTS         AGE
webhook-service   10.20.1.23:8443   41d

An ENDPOINTS column reading <none> ends the investigation right there — fix the workload. If endpoints exist, probe the same URL from inside the cluster:

kubectl -n webhook-system run probe --rm -i --restart=Never --image=curlimages/curl:8.11.1 -- curl -sk -o /dev/null -w '%{http_code}\n' https://webhook-service.webhook-system.svc:443/

This one deserves care in how you read it. A Pod lives inside the cluster network; the API server, on a managed control plane, does not. A probe that returns a status code while the API server still times out is the signature of a firewall rule, not of a sick webhook. For the TLS branch, compare the two halves of the trust pair:

kubectl get validatingwebhookconfiguration example-admission -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d | openssl x509 -noout -subject -dates
kubectl -n webhook-system get secret webhook-server-cert -o go-template='{{index .data "tls.crt"}}' | base64 -d | openssl x509 -noout -ext subjectAltName -dates

The issuer of the serving certificate has to be the CA in caBundle, and its subjectAltName has to include webhook-service.webhook-system.svc. Nothing is broken if you have never run these before — they only read, and you can run them on a healthy cluster to see what a matching pair looks like.

Fix it by cause, cheapest move first

Empty endpoints: bring the workload back and let the next retry through.

kubectl -n webhook-system rollout restart deploy/webhook-server
kubectl -n webhook-system rollout status deploy/webhook-server --timeout=120s

Blocked port on a private cluster: open the control-plane range to the port the webhook Pod actually listens on. Google documents collecting masterIpv4CidrBlock from gcloud container clusters describe and the node target tags from the existing gke- rules, then:

gcloud compute firewall-rules create allow-webhook-8443 \
  --action ALLOW \
  --direction INGRESS \
  --source-ranges CONTROL_PLANE_RANGE \
  --rules tcp:8443 \
  --target-tags TARGET

Certificate mismatch: reissue the serving certificate from the CA in caBundle, or re-inject the CA the webhook's installer manages. If cert-manager owns it, the cert-manager.io/inject-ca-from annotation on the configuration is what refills that field — check the annotation is still present before you patch anything by hand.

Slow handler: raise the timeout toward the documented ceiling, and treat that as buying time rather than fixing latency.

kubectl patch validatingwebhookconfiguration example-admission --type=json -p='[{"op":"replace","path":"/webhooks/0/timeoutSeconds","value":20}]'

Locked out with a production deploy waiting: switching that one entry to failurePolicy: Ignore restores writes immediately and lets unvalidated objects through until you switch it back. Deleting the configuration object does the same thing more bluntly, and a Helm release or operator will usually recreate it on the next reconcile.

A worked example: a private cluster and a webhook on 8443

A team adds a policy controller to a private GKE cluster. It installs cleanly, the Pod is Running, its logs are quiet. Every kubectl apply against a matched resource then fails with failed calling webhook ending in context deadline exceeded after almost exactly ten seconds. Endpoints show 10.20.1.23:8443, and a curl probe from a Pod returns 200, which rules out the workload. The ten-second wall matches the documented default timeout, and the port is 8443 — not one of the two the control plane is allowed to open. They add the firewall rule above for tcp:8443 from the control-plane CIDR to the node tag. The next apply returns ingress.networking.k8s.io/site created with no change to the manifest.

Confirm admission works again before you walk away

Re-run the write that failed, then exercise the webhook deliberately with an object you expect it to reject:

kubectl apply -f ingress.yaml
kubectl apply -f deliberately-invalid.yaml

A success on the first and admission webhook "validate.example.com" denied the request on the second is the result you want. A second success means the webhook is being skipped, which is not the same as being fixed.

Scope the webhook so it cannot lock you out

The Kubernetes good-practices guidance is to exclude system namespaces and the webhook's own namespace so it cannot block its own recovery:

namespaceSelector:
  matchExpressions:
  - key: kubernetes.io/metadata.name
    operator: NotIn
    values:
    - kube-system
    - webhook-system

The same guidance asks for a small timeout, narrow rules, objectSelector and namespaceSelector filtering so the webhook is invoked less often, and more than one replica behind the Service. Watch the certificate expiry too — a webhook that has run for a year without complaint fails the day its cert rolls over.

How this differs from kubectl's x509 error and from a denial

kubectl: x509: certificate signed by unknown authority points the opposite way down the same connection: there it is your client that does not trust the API server's certificate, and the fix is in your kubeconfig. Here the API server is the client and the webhook is the server, so the fix is caBundle and the serving cert inside the cluster — your kubeconfig is fine, which is why kubectl get keeps working while writes fail.

The second look-alike is admission webhook "..." denied the request. That is the webhook working: it received your object, applied its rule, and said no. failurePolicy has no bearing on it, and the fix is in your manifest rather than in the cluster.

Next time a write dies on a webhook, walk these checks back in order: read the tail of the message, check the endpoints, probe the Service from a Pod, then compare the CA against the serving certificate.

Related questions

Can I just delete the ValidatingWebhookConfiguration to unblock deploys?

It works and it is a workaround, not a fix. Deleting the object removes the webhook from the admission path, so writes go through unvalidated — every guarantee that webhook was installed to provide is off until it returns. Anything managed by Helm or an operator will also recreate it on the next reconcile, so the unblock is often temporary anyway. Switching the single failing entry to failurePolicy: Ignore is the narrower version of the same move.

Should I set failurePolicy: Ignore permanently so this never blocks us again?

Only if you can live with the requests it lets through. The Kubernetes guidance frames it as a trade: Fail is safer for validation you actually depend on but risks disruption when the webhook is unavailable, while Ignore keeps the cluster writable and may admit objects that should have been rejected. Security and policy controllers are usually left on Fail and made highly available instead; convenience webhooks are reasonable candidates for Ignore.

A curl from a Pod reaches the webhook, so why does the API server still time out?

Because they are not on the same network path. Your Pod is inside the cluster network; on a managed control plane the API server is not. Google's GKE documentation states that by default the firewall does not allow TCP connections to nodes except on ports 443 and 10250, and that a webhook listening on any other port fails without a custom rule. A Pod probe that succeeds while the API server times out points at that rule, not at the webhook.

I set timeoutSeconds to 60 and the API server rejected the change.

The documented range is 1 to 30 seconds, with a default of 10, so 60 is outside what the field accepts. If a handler genuinely needs longer than 30 seconds, the timeout is not the problem worth solving — move the slow work out of the admission path, cache what the handler looks up, or narrow the rules so it is invoked on fewer requests.

Only some namespaces fail, and the same manifest applies cleanly elsewhere.

That is scoping doing its job, which usually means the webhook is fine and your expectation of where it applies is off. Read the namespaceSelector, objectSelector and rules on the entry and compare them against the namespace and labels you are applying into. One more field catches people out: matchPolicy defaults to Exact, so a request sent through a different API group or version than the rules list will not match the webhook at all.

References

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

More in this category

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.

Git
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)

The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang

The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.

Docker
curl: (60) SSL certificate problem: unable to get local issuer certificateFixed

curl: (60) SSL certificate problem: unable to get local issuer certificate

curl walked the certificate chain the server sent and reached a certificate whose issuer is not in the CA store it is reading, so it refused the connection with exit code 60. The issuer is missing for one of four reasons: the server sends only its leaf certificate without the intermediate (browsers hide this by fetching it themselves), a TLS-inspecting proxy re-signed the site with a company CA that the container or runner does not trust, curl is reading a different CA bundle than you think (CURL_CA_BUNDLE, SSL_CERT_FILE, a vendored curl), or the ca-certificates package is too old. curl -v shows which store was used and openssl s_client shows what the server sent; fix the side that is missing the link and confirm with -w '%{ssl_verify_result}'.

curl