Webhook Certificate Rotation#
The kubeovn-webhook (packages/system/kubeovn-webhook) has a latent TLS certificate rotation gap: the webhook process loads its serving certificate exactly once at startup and never reloads it, even when cert-manager renews the backing Secret. This causes a hard outage roughly one year after install or the last pod restart.
The Problem#
main.go loads the cert/key pair with tls.LoadX509KeyPair and assigns it as a static slice in tls.Config.Certificates. There is no GetCertificate callback, no fsnotify file watcher, and no reload loop β once the in-memory certificate expires, every TLS handshake to the webhook fails.
The cert-manager Certificate object is configured with :
duration: 8760h(1 year) for the serving certificaterenewBefore: 24hβ cert-manager rotates the Secret only ~24 hours before expiry
This 24-hour window is extremely tight: if the webhook pods are not restarted within that window (which is unlikely under normal operations), they continue to serve the expired in-memory certificate after expiry.
The Deployment has no reloader sidecar, no checksum annotation, and no secret.reloader.stakater.com/reload annotation β nothing triggers a rollout when the Secret changes.
Blast Radius#
The MutatingWebhookConfiguration has failurePolicy: Fail and intercepts CREATE on all pods in every namespace except those labeled cozystack.io/system=true or named kube-system. When the expired certificate causes TLS verification to fail, all pod creation in tenant namespaces is rejected β including virt-launcher pods, which blocks VM start operations.
Error seen in kube-apiserver logs:
Internal error occurred: failed calling webhook "kube-ovn-webhook.cozystack.io":
failed to call webhook: <TLS error: certificate has expired or is not yet valid>
Certificate Architecture#
cert-manager provisions certificates through two objects defined in certmanager.yaml:
- Root CA Certificate β
duration: 43800h(5 years),rotationPolicy: Never - Serving Certificate β
duration: 8760h,renewBefore: 24h, signed by the CA issuer
The MutatingWebhookConfiguration carries cert-manager.io/inject-ca-from so cert-manager keeps the webhook's caBundle up to date automatically β but this only controls what the API server uses to trust the webhook, not what the webhook itself serves.
Proposed Fixes#
Three mitigations are documented in Issue #3556 :
| Fix | Approach | Notes |
|---|---|---|
| In-process reload (preferred) | Replace static Certificates: []tls.Certificate{tlsCert} with a tls.Config.GetCertificate callback that re-reads tls.crt/tls.key on each handshake (with caching), or use an fsnotify watcher | Kubernetes already updates the mounted secret volume when cert-manager writes the new cert β the webhook just needs to read it |
Wider renewBefore (defense-in-depth) | Increase renewBefore in certmanager.yaml from 24h to e.g. 720h (30 days) | Alone this doesn't fix the reload gap, but gives a much larger window for manual or automated restarts |
| Stakater Reloader (stopgap) | Add secret.reloader.stakater.com/reload: <secret-name> annotation to the Deployment | Triggers a rolling restart on Secret change; avoids the expired-cert scenario but causes brief downtime on each renewal |
Relevant Files#
| File | Purpose |
|---|---|
main.go | Entry point; static cert load at lines 31β40 |
certmanager.yaml | cert-manager Certificate and Issuer definitions |
deployment.yaml | Deployment spec; secret volume mount, no reloader annotations |
mutatingwebhookconfiguration.yaml | Webhook config with failurePolicy: Fail and CA injection annotation |
| Issue #3556 | Bug report with full reproduction steps and fix proposals |