Skip to content

fix(kube-ovn): reload kubeovn-webhook serving certificate on cert-manager renewal - #3557

Merged
IvanHunters merged 6 commits into
mainfrom
fix/kubeovn-webhook-cert-reload
Aug 10, 2026
Merged

fix(kube-ovn): reload kubeovn-webhook serving certificate on cert-manager renewal#3557
IvanHunters merged 6 commits into
mainfrom
fix/kubeovn-webhook-cert-reload

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

The kube-ovn-webhook loaded its TLS serving certificate once at process startup (tls.LoadX509KeyPair into a static tls.Config.Certificates) and never re-read it. cert-manager renews the backing Secret ahead of expiry, but a running pod kept serving the certificate it loaded at boot. Once that certificate expired — roughly one year after install, on any cluster whose webhook pods had not been restarted after the renewal — the pod served an expired certificate, the kube-apiserver's TLS call to the webhook failed verification, and because the MutatingWebhookConfiguration uses failurePolicy: Fail, every pod creation in tenant namespaces was rejected. In practice this blocks virt-launcher pods, so no VMI can start.

This PR:

  • Serves the certificate through a reloading tls.Config.GetCertificate callback that re-reads the key pair when the mounted files change, and keeps serving the last good certificate if a reload fails (e.g. a torn read while the volume is being updated). cert-manager renewals are now honoured without a pod restart.
  • Widens the certificate renewBefore from 24h to 720h, so renewal happens well ahead of expiry instead of one day before.
  • Adds a regression test that fails against the previous static implementation (the server keeps presenting the old certificate after the files are replaced) and passes once the certificate is reloaded; verified non-vacuous by mutation.

Screenshots

Not applicable (no UI changes).

Downstream repositories

  • No downstream repository is affected by this change

Walked the trigger map file-by-file against the diff: the change is confined to the system/kubeovn-webhook image and its cert-manager Certificate renewBefore; it touches no package under apps//extra/, no values.schema.json, no app values.yaml default, no ApplicationDefinition, no platform values, no CRD, and no hack/ tooling.

Release note

fix(kube-ovn): kubeovn-webhook now reloads its serving certificate when cert-manager renews it, so the webhook no longer serves an expired certificate that blocked all pod creation (including VMIs) roughly one year after install

Summary by CodeRabbit

  • New Features

    • Webhook TLS certificates now reload automatically when renewed, without requiring a server restart.
  • Bug Fixes

    • Certificate reload failures now fail the TLS handshake instead of serving an outdated certificate.
    • Added server timeouts to improve connection handling.
  • Configuration

    • Certificates are now renewed earlier to help prevent expiration-related service disruptions.

The webhook loaded its TLS key pair once at startup and served it as a
static tls.Config.Certificates entry for the lifetime of the process. A
cert-manager renewal of the backing Secret was never picked up by a
running pod, so once the certificate expired (about a year after install)
the pod kept presenting the expired certificate. The kube-apiserver's TLS
call to the webhook then failed and, because the MutatingWebhookConfiguration
uses failurePolicy: Fail, every pod creation in tenant namespaces was
rejected (including virt-launcher pods, blocking all VMIs).

Serve the certificate through a reloading tls.Config.GetCertificate
callback that re-reads the key pair when the mounted files change and
keeps serving the last good certificate on a failed reload. Add a
regression test that fails against the static implementation and passes
once the certificate is reloaded.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
renewBefore: 24h on a one-year certificate leaves a one-day window for
cert-manager to rotate the Secret and for the webhook to pick it up.
Widen it to 720h (30 days) so renewal happens well ahead of expiry.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
@IvanHunters IvanHunters added kind/bug Categorizes issue or PR as related to a bug backport Should change be backported on previous release labels Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e539889-7652-4c52-9f43-df6f856196bc

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6bb47 and 4151885.

📒 Files selected for processing (3)
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/cert.go
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/cert_test.go
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
💤 Files with no reviewable changes (1)
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/cert_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go

📝 Walkthrough

Walkthrough

This change adds runtime TLS certificate reloading to the Kube-OVN webhook, uses the reloading configuration at server startup, adds integration coverage, configures server timeouts, and increases the cert-manager renewal window.

Changes

Kube-OVN webhook TLS reload

Layer / File(s) Summary
Runtime TLS reload path
packages/system/kubeovn-webhook/images/kubeovn-webhook/cert.go, packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
newReloadingTLSConfig validates the initial certificate/key pair and reloads both files during each TLS handshake. The webhook server uses this configuration and sets header, read, write, and idle timeouts.
Reload validation coverage
packages/system/kubeovn-webhook/images/kubeovn-webhook/cert_test.go
Adds helpers for generating certificates, writing certificate files, reading handshake certificates, and verifying that a renewed certificate is served.
Certificate renewal timing
packages/system/kubeovn-webhook/templates/certmanager.yaml
Changes the cert-manager Certificate resource renewBefore value from 24h to 720h.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant http.Server
  participant tls.Config.GetCertificate
  participant CertFiles
  Client->>http.Server: start TLS handshake
  http.Server->>tls.Config.GetCertificate: request server certificate
  tls.Config.GetCertificate->>CertFiles: load certificate and key files
  alt files load successfully
    CertFiles-->>tls.Config.GetCertificate: current certificate
    tls.Config.GetCertificate-->>http.Server: certificate
    http.Server-->>Client: complete handshake
  else file loading fails
    CertFiles-->>tls.Config.GetCertificate: read error
    tls.Config.GetCertificate-->>http.Server: handshake error
  end
Loading

Possibly related issues

  • cozystack/cozystack issue 3556 — The PR implements certificate reloading and the renewBefore change described by the issue.

Suggested reviewers: lexfrei

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: reloading the kubeovn-webhook serving certificate after cert-manager renewal.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kubeovn-webhook-cert-reload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) size/L This PR changes 100-499 lines, ignoring generated files labels Aug 5, 2026
Address review of the reload path:
- Observe the cert file mtime before reading the key pair, so a Secret
  swap racing the read is retried on the next handshake instead of being
  cached under a newer mtime and missed.
- Record the load attempt regardless of outcome, so a persistently
  unreadable file is retried only once its mtime advances, not on every
  handshake.
- Detect change with mtime inequality instead of a strictly-newer
  comparison, catching equal-mtime replacements and backward clock steps.
- Log stat failures instead of swallowing them, so a broken mount is not
  invisible until expiry.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
A previous revision recorded the certificate file mtime even when the
reload failed, to avoid re-reading a persistently bad file on every
handshake. That conflated two failure modes: a transient, content-
independent error (fd exhaustion, a torn read) on a VALID renewed file
advanced the recorded mtime, so the staleness check never fired again
and the renewed certificate was never loaded. The cached certificate
then expired within renewBefore, and failurePolicy: Fail blocked all
tenant pod creation - exactly the outage this reloader prevents.

Advance the recorded mtime only on a successful load, and bound retries
of a failed load with a wall-clock interval instead. A transient failure
is now retried until it succeeds, while a persistently unreadable file
is still retried only once per interval, not on every handshake. Keep a
single reload in flight to avoid a thundering herd, and rate-limit the
failure log. Add a regression test that fails when the mtime is advanced
on failure.

Detect change with mtime inequality, so a backward clock step is also
picked up.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go (1)

35-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add timeouts to the http.Server.

The server sets no read, write, or idle timeout. A client that opens a connection and sends headers slowly holds a goroutine and a file descriptor for an unbounded time. The webhook is cluster-internal, so the exposure is limited, but the timeouts are cheap to add while this literal is being changed.

⏱️ Proposed change to bound request handling
 	server := &http.Server{
-		Addr:      ":8443",
-		TLSConfig: tlsConfig,
-		Handler:   mux,
+		Addr:              ":8443",
+		TLSConfig:         tlsConfig,
+		Handler:           mux,
+		ReadHeaderTimeout: 10 * time.Second,
+		ReadTimeout:       30 * time.Second,
+		WriteTimeout:      30 * time.Second,
+		IdleTimeout:       60 * time.Second,
 	}

This change requires the time import in main.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go` around lines
35 - 38, Update the `http.Server` literal in `main` to set explicit
`ReadTimeout`, `WriteTimeout`, and `IdleTimeout` values, and add the required
`time` import in `main.go`. Keep the existing `Addr`, `TLSConfig`, and `Handler`
setup unchanged while using the `server` symbol to bound slow or idle client
connections.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go`:
- Around line 35-38: Update the `http.Server` literal in `main` to set explicit
`ReadTimeout`, `WriteTimeout`, and `IdleTimeout` values, and add the required
`time` import in `main.go`. Keep the existing `Addr`, `TLSConfig`, and `Handler`
setup unchanged while using the `server` symbol to bound slow or idle client
connections.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a756297-42d6-49dc-a677-3acf19536952

📥 Commits

Reviewing files that changed from the base of the PR and between ee26a50 and 5b6bb47.

📒 Files selected for processing (4)
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/cert.go
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/cert_test.go
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
  • packages/system/kubeovn-webhook/templates/certmanager.yaml

@lllamnyp Timofei Larkin (lllamnyp) left a comment

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.

The bug this fixes is real and well diagnosed: the webhook loaded its TLS serving cert once at startup into a static tls.Config.Certificates, so a cert-manager renewal of the Secret was never picked up by a running pod, and once the cert expired the apiserver's TLS call to the webhook failed — with failurePolicy: Fail, that takes down pod creation cluster-wide. Good catch, and the renewBefore: 24h → 720h change in templates/certmanager.yaml is a sensible independent hardening on top of it (30 days of margin for a renewal retry vs. 1 day).

On the implementation: I think cert.go's certReloader (mtime tracking, serialized refresh, retry backoff, rate-limited logging, last-good-certificate fallback — 155 lines, plus 245 lines of tests) can collapse to a GetCertificate closure that just re-reads the files on every handshake, with no watcher, cache, or extra state:

package main

import "crypto/tls"

// newReloadingTLSConfig returns a tls.Config that reloads the certificate and key
// from disk on every TLS handshake, so a cert-manager renewal of the mounted Secret
// is picked up by the running process without a restart.
func newReloadingTLSConfig(certFile, keyFile string) (*tls.Config, error) {
	if _, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil {
		return nil, err
	}
	return &tls.Config{
		GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
			cert, err := tls.LoadX509KeyPair(certFile, keyFile)
			if err != nil {
				return nil, err
			}
			return &cert, nil
		},
	}, nil
}

Same function signature, so main.go doesn't need to change at all — this is a deletion inside cert.go, not a rewrite. A few things that make me confident this is sufficient rather than under-engineered:

  • GetCertificate is called on every handshake whenever Certificates is left unset, per crypto/tls docs — no SNI dependency, no gap. I verified this empirically too: a standalone listener using exactly the closure above serves a stale cert before a file rewrite and the renewed one immediately after, across two separate connections, no polling involved.
  • The torn-read scenario certReloader guards against (reading a cert/key pair mid-write, cert.go:81-107, tested in cert_test.go:165-245) isn't reachable here: templates/deployment.yaml mounts webhook-certs as a plain secret: volume with no subPath, and Kubernetes' atomic writer for Secret volumes swaps the whole ..data directory via a single symlink flip. A reader always sees a complete old generation or a complete new one, never a mixture — of a single file or of the cert/key pair. That's what the two extra tests are actually exercising: that the fallback machinery works internally, not that it's needed for anything this deployment can produce.
  • The per-handshake LoadX509KeyPair cost (a few KB of file I/O plus a PEM/key parse) is negligible next to the asymmetric crypto a TLS handshake is already doing (ECDHE key exchange, certificate signing) — it shouldn't need caching to stay cheap.
  • No mTLS is configured on this webhook (mutatingwebhookconfiguration.yaml has no client-cert verification, and neither version of cert.go sets ClientCAs), so GetCertificate's well-known limitation — it doesn't refresh CA pools, only server/client leaf certs — doesn't bite here.

One real trade-off worth naming: if the mounted cert genuinely becomes unreadable (Secret deleted, permissions changed — not a normal renewal), the closure above fails every handshake and logs on each one, where the current certReloader keeps serving the last good cert and rate-limits the log line. That's a narrow, admin-error-only scenario, and failing loudly there seems fine to me, but it's the one place the extra code isn't purely redundant.

Happy to open a follow-up PR with the smaller diff if that's useful, or if there's a reason to keep the extra machinery I'm missing, I'd like to hear it.

(Review drafted with Claude Code assistance.)

…osure

Collapse the certReloader (mtime tracking, serialized refresh, retry backoff,
rate-limited logging, last-good-certificate fallback) into a GetCertificate
callback that simply re-reads the key pair from disk on every handshake. The
signature is unchanged, so main.go is untouched.

The extra machinery guarded scenarios this deployment cannot produce:

- Torn reads are unreachable: the cert/key files come from a plain Secret
  volume with no subPath, and Kubernetes' atomic writer swaps the whole ..data
  directory via a single symlink flip, so a reader always sees a complete old
  or complete new generation of both files.
- The per-handshake LoadX509KeyPair cost is negligible next to the asymmetric
  crypto the handshake already performs, so no cache is needed.
- The webhook configures no mTLS, so GetCertificate not refreshing client CA
  pools does not apply.

If the mounted files genuinely become unreadable (an operator error, not a
renewal) the handshake now fails loudly instead of silently serving a stale
certificate. The core regression test is retained and remains non-vacuous: it
fails against a static tls.Config.Certificates and passes with the reloading
callback.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
The webhook http.Server set no read, write, or idle timeout, so a client that
opens a connection and sends headers slowly could hold a goroutine and file
descriptor indefinitely. Add ReadHeaderTimeout, ReadTimeout, WriteTimeout, and
IdleTimeout.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
@IvanHunters
IvanHunters merged commit 9807152 into main Aug 10, 2026
16 of 17 checks passed
@IvanHunters
IvanHunters deleted the fix/kubeovn-webhook-cert-reload branch August 10, 2026 09:49
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

myasnikovdaniil added a commit that referenced this pull request Aug 18, 2026
…certificate on cert-manager renewal (#3730)

# Description
Backport of #3557 to `release-1.6`.
myasnikovdaniil added a commit that referenced this pull request Aug 19, 2026
…certificate on cert-manager renewal (#3879)

# Description
Backport of #3557 to `release-1.5`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) backport Should change be backported on previous release backport-previous Backport target — previous release line kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants