Repository navigation
feat(kubeovn-webhook): add client-certificate verification, off by default - #2698
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses an unauthenticated information disclosure vulnerability by enforcing stricter network access controls and providing an optional mechanism for mutual TLS authentication. These changes significantly reduce the attack surface of the webhook by limiting network reachability and requiring verified client identities. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
📝 WalkthroughWalkthroughThe webhook now rate-limits certificate observations, logs peer addresses, and forwards every request. It supports optional client CA verification through ChangesWebhook mTLS Configuration
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to The PR’s runtime behavior has no supplied merge-blocking concern; the remaining bounded issue is a minor test-lint cleanup for passing an explicit context. Sequence Diagram(s)sequenceDiagram
participant WebhookClient
participant HTTPServer
participant logClientCert
participant ExistingHandler
WebhookClient->>HTTPServer: Send request with optional client certificate
HTTPServer->>logClientCert: Forward request and enforcement state
logClientCert->>logClientCert: Apply certificate observation rate limit
logClientCert->>ExistingHandler: Forward every request
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
e1045bf to
b6ba7cf
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces mTLS support to the kubeovn-webhook by adding a --client-ca-file flag and configuring the TLS server to require and verify client certificates. It also adds a new NetworkPolicy to restrict ingress traffic to the webhook from the kube-system namespace. Feedback identifies that the new flag is not yet integrated into the Helm deployment templates, suggests enforcing a minimum TLS version of 1.2 for improved security, and recommends quoting string values in the Helm templates to comply with the style guide.
IvanHunters
left a comment
There was a problem hiding this comment.
Both hardening mechanisms in this PR are inert in a default Cozystack cluster, so the disclosure vector stays open:
- NetworkPolicy is a no-op. Cozystack sets
kube-ovn.func.ENABLE_NP: false(packages/system/kubeovn/values.yaml), so kube-ovn-controller runs with--enable-np=falseand programs no ACLs. The addedtemplates/networkpolicy.yamlis never enforced. - mTLS is dead code.
--client-ca-filedefaults to""and is never passed intemplates/deployment.yaml; no apiserver-side client-cert config exists, so theRequireAndVerifyClientCertbranch never runs.
Only MinVersion: tls.VersionTLS12 takes effect.
To make mTLS real it must be wired end-to-end (mount CA, pass the flag, configure the apiserver admission webhook to present a matching client cert) behind a values toggle, and not enabled half-way: failurePolicy: Fail + broken TLS would block all non-system pod creation.
For the NetworkPolicy: it only matters if NP enforcement is on, and namespaceSelector: kube-system won't match apiserver traffic (host-network, not a pod) — gate via CNI host/ipBlock on control-plane CIDRs instead. The NP is also unconditional (no toggle), so it becomes silently load-bearing if ENABLE_NP is ever flipped.
5d61c37 to
04606e2
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Rebased onto current NetworkPolicy: dropped, not fixed. Client-cert verification: now reachable, but off by default. It stays off by default deliberately. This webhook is registered Setting Also rebased past the certificate-reloader work that landed since May, so One thing this PR does not do: it does not close the disclosure, it makes the fix deployable and tells us whether we can turn it on. Worth being explicit about that since the advisory is still open. Separately, and not for this PR: the advisory text overstates the impact. It says arbitrary namespace annotations are exfiltrated, but the handler only ever copies two — |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go`:
- Line 42: Update the seenClientIssuers handling to avoid retaining every
distinct issuer for the process lifetime. Replace the unbounded
LoadOrStore-based tracking with a bounded cache or rate-limited logging, while
preserving the intended duplicate-suppression behavior and preventing one log
entry per unverified issuer.
- Around line 39-46: Stop using unverified PeerCertificates metadata in the
observation path to identify the API server or select client-ca-file. In
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go:39-46, remove the
issuer-based seenClientIssuers trust decision and related identity confirmation;
in packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go:78-81, ensure
enforcement is controlled only by trusted control-plane configuration; and in
packages/system/kubeovn-webhook/values.yaml:12-22, configure --client-ca-file
from that trusted setting rather than observation logs. Preserve observation
logging only as explicitly untrusted diagnostic information.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4a005c97-0868-459e-a9d6-0017924f9801
📒 Files selected for processing (3)
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.gopackages/system/kubeovn-webhook/templates/deployment.yamlpackages/system/kubeovn-webhook/values.yaml
Address unauthenticated access vulnerability (GHSA-g883-q79m-8225) by: - Adding NetworkPolicy to restrict ingress to kube-system namespace only - Adding optional mTLS support via --client-ca-file flag for client certificate verification https://claude.ai/code/session_013DRdaUJMYxonsjVpWxJSq3 Signed-off-by: Claude <[email protected]>
Prevent use of older, insecure TLS protocols. https://claude.ai/code/session_013DRdaUJMYxonsjVpWxJSq3 Signed-off-by: Claude <[email protected]>
…ve first Review of the original commits found both hardening mechanisms inert, and that was correct: the NetworkPolicy could never be enforced because packages/system/kubeovn/values.yaml sets ENABLE_NP: false, so kube-ovn-controller runs with --enable-np=false and programs no ACLs; and --client-ca-file was never passed by the chart, so the RequireAndVerifyClientCert branch was unreachable. Only the TLS 1.2 floor did anything. The NetworkPolicy is dropped rather than fixed. Enforcing it would mean turning on ENABLE_NP for the whole cluster, which is a far larger change than this disclosure warrants, and shipping an object that looks like a control but enforces nothing is worse than not shipping it. Client-cert verification is now reachable: clientCAFile is a chart value and the flag is passed when set. It stays off by default, because turning it on blind is dangerous — this webhook is registered failurePolicy: Fail, so if the API server does not present a certificate (it only does when the cluster runs an AdmissionConfiguration naming a kubeConfigFile for this webhook), requiring one stops pod creation cluster-wide. So the default is tls.RequestClientCert: certificates are requested and logged, never required. Nothing is rejected and admission cannot break, while the log states per issuer whether enforcement is possible on this cluster. Setting clientCAFile then flips it to RequireAndVerifyClientCert. Addresses GHSA-g883-q79m-8225. This commit makes the fix deployable and observable; enforcement is a follow-up once the logs confirm the API server presents a certificate on each supported variant. Assisted-By: Claude Signed-off-by: Andrei Kvapil <[email protected]>
Both points came out of review of this branch, and both live in the observation path added for the staged rollout. The set of seen issuers was unbounded. With tls.RequestClientCert the server accepts whatever certificate a caller offers, so the issuer string is supplied by that caller: anyone able to open a connection could mint fresh issuers and grow the map and the log without limit. Cap it, and say once when the cap is reached -- a cap hit on a quiet cluster is itself the signal that the issuers are fabricated. The log also invited the operator to pin '--client-ca-file' to whichever CA it had just printed. That CA is unverified by construction, so an attacker who can reach the port can propose one. Say plainly in the log and in the comment that the value is a claim rather than an identity, and that it has to be confirmed out of band. Enforcement still depends only on --client-ca-file, which is trusted control-plane configuration. Signed-off-by: Andrei Kvapil <[email protected]>
04606e2 to
07fd4d9
Compare
IvanHunters
left a comment
There was a problem hiding this comment.
NOT LGTM. Requesting changes.
The default path (empty --client-ca-file → tls.RequestClientCert) is safe and well-argued: nothing is rejected, so admission cannot break, and logging the issuer is a sound observe-first step. No objection there. The problems are on the enablement path this PR sets up (the flag plus the values.yaml guidance), and on one comment that this change quietly turns false.
Two items should change before merge; the rest are non-blocking.
Blocking
-
The client CA pool is read once at startup and never reloaded, and this PR makes the
cert.go:25-26comment false. That comment ("The webhook configures no mTLS (no ClientCAs), so GetCertificate's limitation of not refreshing client CA pools does not apply") was the stated reason in #3557 for not handling client CAs. LoadingClientCAsfrom a startup-onlyos.ReadFilebrings that limitation straight back: once step two turns on enforcement, a rotation of the API server's client CA leaves the webhook pinned to the stale pool, it rejects the API server, and withfailurePolicy: Failpod creation stops cluster-wide until the pods restart. Fix the comment now, and either reload the pool the way the serving cert is reloaded or document the restart requirement. -
The feature cannot be enabled through the chart without an outage. The pod mounts only the
-tlssecret at/etc/webhook/certs; there is no volume for an API-server client CA. A path that does not exist →os.ReadFile→log.Fatalf→ both replicas CrashLoop → outage. The one CA that is present,/etc/webhook/certs/ca.crt, is the webhook's own serving CA (cert-manager writes it into the-tlssecret), not the API server's client CA, so pointing at it starts cleanly and then rejects every API-server handshake: the same outage, delayed to the first admission call. Meanwhilevalues.yamltells the operator to "Set it to the in-pod path of a CA bundle", and no supported path satisfies that. Either land the mount mechanism together with the flag, or trim the enable-guidance until step two provides it.
Non-blocking
-
The 32-issuer cap is first-come-first-served. Anyone who can reach :8443 (the exact threat model these comments describe) can open 32 connections carrying 32 fabricated issuers before the API server's first call, permanently suppressing the observation signal the whole feature exists to produce. Consider recording only
<none>and verified-looking issuers, or keying the cap by something the caller cannot forge. -
main.go:80logs "UNVERIFIED — nothing has checked this certificate ... before pinning it with --client-ca-file" unconditionally, including when--client-ca-fileis already set andRequireAndVerifyClientCerthas verified the chain during the handshake. Gate the wording on whether enforcement is active. -
The observation lines carry no
r.RemoteAddr. "none presented" fires for any certless caller (a tenant curl, a scanner, a probe), not just the API server, so on a cluster where the API server does present a cert, one stray in-cluster probe logs a categorical "NOT yet safe" the operator cannot attribute. Logging the peer address next to both branches makes it correlatable. -
No unit tests for
noteClientIssuer(dedup, cap boundary, one-shot warning) orlogClientCert(<none>vs issuer path), though the package already shipscert_test.goand this is pure, trivially testable logic.
Checked and not issues: the image digest not moving in values.yaml is expected (it is regenerated at release time); this package has no values.schema.json; ClientAuth/ClientCAs on the outer config compose correctly with the GetCertificate-based reloading config; MinVersion TLS 1.2 is fine.
| log.Fatalf("Failed to parse client CA certificate") | ||
| } | ||
| tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert | ||
| tlsConfig.ClientCAs = clientCAPool |
There was a problem hiding this comment.
[MAJOR] Client CA pool loaded once, never reloaded — and this falsifies the cert.go comment
[MAJOR] ClientCAs is populated from a startup-only os.ReadFile, so unlike the serving certificate (re-read per handshake in cert.go) the client CA pool is never refreshed. This directly falsifies cert.go:25-26, which justified skipping client-CA handling with "The webhook configures no mTLS (no ClientCAs), so GetCertificate's limitation of not refreshing client CA pools does not apply". Once step two enables enforcement, a rotation of the API server's client CA leaves the webhook pinned to the stale pool: it rejects the API server, and with failurePolicy: Fail pod creation stops cluster-wide until the pods restart. Fix the now-false comment, and either reload the pool the way the serving cert is reloaded or document the restart requirement.
| - "--port-security={{ .Values.portSecurity }}" | ||
| - "--routes={{ .Values.routes }}" | ||
| {{- with .Values.clientCAFile }} | ||
| - "--client-ca-file={{ . }}" |
There was a problem hiding this comment.
[MAJOR] No way to mount a client CA — enabling the flag causes a cluster-wide outage
[MAJOR] The pod mounts only the -tls secret at /etc/webhook/certs; there is no volume for an API-server client CA. A clientCAFile path that does not exist in the pod → os.ReadFile → log.Fatalf → both replicas CrashLoop → webhook unreachable → failurePolicy: Fail blocks all pod creation. The one CA that IS present, /etc/webhook/certs/ca.crt, is the webhook's own serving CA (cert-manager writes it into the -tls secret), not the API server's client CA — pointing at it starts cleanly and then rejects every API-server handshake, the same outage delayed to the first admission call. There is no correct value an operator can set today. Land a mount mechanism (e.g. an extraVolumes/configmap path) together with this flag.
| # "client certificate: presented, issuer ..." → enforcement is possible | ||
| # "client certificate: none presented" → it is not, yet | ||
| # | ||
| # Set it to the in-pod path of a CA bundle to switch to |
There was a problem hiding this comment.
[MAJOR] Enable-guidance points at a path no supported config can provide
[MAJOR] "Set it to the in-pod path of a CA bundle" documents an action the chart cannot fulfil: the deployment mounts no volume for a client CA, and the only present CA file (/etc/webhook/certs/ca.crt) is the wrong one (the webhook's serving CA). Following this guidance today produces a cluster-wide pod-creation outage (see the deployment.yaml comment). Trim this guidance until step two ships the mount mechanism, or add the mechanism here.
| if _, seen := seenClientIssuers[key]; seen { | ||
| return false | ||
| } | ||
| if len(seenClientIssuers) >= maxSeenClientIssuers { |
There was a problem hiding this comment.
[MINOR] First-come cap lets an attacker suppress the observation signal
[MINOR] The 32-slot cap is first-come-first-served with no distinction between callers. Anyone who can reach :8443 — the exact threat model described above — can open 32 connections with 32 fabricated issuers before the API server's first admission call; afterwards neither the real issuer nor the <none> line is ever logged. The operator following the documented "read the webhook log" procedure sees only the cap warning and can never learn whether their API server presents a cert. The observe-first plan is defeasible by the party it observes. Consider recording only <none> and verified-looking issuers, or keying the cap on something the caller cannot forge.
| if key == "<none>" { | ||
| log.Printf("client certificate: none presented — client-ca-file enforcement is NOT yet safe on this cluster") | ||
| } else { | ||
| log.Printf("client certificate: presented, issuer %q (UNVERIFIED — nothing has checked this certificate, and any caller can claim any issuer). Confirm out of band that this is your API server's CA before pinning it with --client-ca-file", key) |
There was a problem hiding this comment.
[MINOR] Misleading UNVERIFIED log under enforcement mode
[MINOR] This line logs "UNVERIFIED — nothing has checked this certificate ... before pinning it with --client-ca-file" unconditionally, including when --client-ca-file is already set and RequireAndVerifyClientCert has in fact verified the chain during the handshake. After enabling enforcement the first verified API-server call logs a false "nothing has checked this certificate" plus advice to do a step the operator already did. Gate the wording on whether enforcement is active.
| } | ||
| if noteClientIssuer(key) { | ||
| if key == "<none>" { | ||
| log.Printf("client certificate: none presented — client-ca-file enforcement is NOT yet safe on this cluster") |
There was a problem hiding this comment.
[MINOR] Observation logs omit the peer address
[MINOR] "none presented — client-ca-file enforcement is NOT yet safe on this cluster" fires for any certless caller — a tenant curl, a scanner, a monitoring probe — not just the API server. On a cluster where the API server does present a cert, one stray in-cluster probe logs a categorical "NOT yet safe" verdict the operator cannot attribute. Logging r.RemoteAddr alongside both branches would let the operator correlate with the API-server IP.
…tising an enable path that does not exist Review found the observation could be silenced and the feature could not be turned on. Both are fixed here; enforcement itself stays out of scope for this step. Rate-limit the observation instead of remembering issuers. Keying on the issuer was the mistake: with RequestClientCert that string comes from the caller, so a bounded set of first-seen values can be filled with 32 fabricated issuers before the API server's first call, permanently suppressing the signal the feature exists to produce, while an unbounded one grows without limit. A rate limit cannot be starved -- a burst costs the current interval and the next real handshake is logged in the one after. The two outcomes are limited separately so a flood of one cannot hide the other, and both lines now carry the peer address, since 'none presented' fires for any certless caller and was not attributable. Say 'verified' rather than 'UNVERIFIED' once enforcement is on, where the handshake really has checked the chain. Stop telling operators to point --client-ca-file at an in-pod path. No such path exists: the pod mounts only its own serving secret, whose ca.crt is this webhook's serving CA, so every value either aborts startup or rejects the API server -- and with failurePolicy: Fail that stops pod creation cluster-wide. Say what is actually true and defer enabling to the step that adds the volume. Correct the claim in cert.go that this webhook configures no ClientCAs. It can now, that pool is read once at startup and never reloaded, and the comment justified skipping reload on the old premise. Record that reloading has to land together with the mount, since neither is usable alone. Cover the observation with tests: rate limiting, the burst it exists to survive, concurrency, both branches, the enforcing wording, and that admission is never blocked by logging. Signed-off-by: Andrei Kvapil <[email protected]>
|
Both blocking points confirmed against the code, and both are fixed in The The enable path. Verified: the pod mounts only The 32-issuer cap — you are right, and it was my change that introduced it. Bounding a set keyed on caller-supplied data still leaves it forgeable: 32 connections before the API server's first call and the signal is gone for the process lifetime. Replaced with rate limiting, which cannot be starved — a burst costs the current interval, the next real handshake is logged in the one after. The two outcomes are limited separately so a flood of one cannot hide the other. The remaining three are in as well: the peer address is logged on both branches, the wording says "verified" when enforcement is actually on, and The item I did not act on is enforcement itself, deliberately: the mount, the reloading pool and pointing the key at it are one change, and it belongs in step two after a day of issuer logs from Talos and |
There was a problem hiding this comment.
🔇 Additional comments (4)
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go (1)
19-57: LGTM!Also applies to: 77-91, 100-107, 121-147
packages/system/kubeovn-webhook/images/kubeovn-webhook/observation_test.go (2)
97-97: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
⚠️ Unverified finding
Sandbox verification was unavailable.Use
httptest.NewRequestWithContext.Line 97 violates the configured
noctxcheck. Importcontextand passcontext.Background()tohttptest.NewRequestWithContext.Proposed fix
import ( + "context" "crypto/tls" ... - r := httptest.NewRequest(http.MethodPost, "/mutate-pods", nil) + r := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mutate-pods", nil)
16-92: LGTM!Also applies to: 107-196
packages/system/kubeovn-webhook/images/kubeovn-webhook/cert.go (1)
25-34: LGTM!
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3f12e3e1-8e8e-4713-bbb3-e0a6c88d1212
📒 Files selected for processing (4)
packages/system/kubeovn-webhook/images/kubeovn-webhook/cert.gopackages/system/kubeovn-webhook/images/kubeovn-webhook/main.gopackages/system/kubeovn-webhook/images/kubeovn-webhook/observation_test.gopackages/system/kubeovn-webhook/values.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/system/kubeovn-webhook/values.yaml
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
IvanHunters
left a comment
There was a problem hiding this comment.
LGTM. This is step one of the GHSA-g883-q79m-8225 fix, it's off by default, and it changes no behaviour on upgrade. Splitting observe from enforce is the right call for a webhook registered failurePolicy: Fail. Verified locally: go test ./..., go vet, and gofmt are all clean, and rendering the chart with the default empty clientCAFile leaves the flag out. Two non-blocking notes below, neither holds up the merge.
No upgrade regression. The base image sets no ClientAuth (so NoClientCert); with the default empty clientCAFile this PR moves it to tls.RequestClientCert, which asks for a certificate but serves the caller either way, so an API server that presents nothing today keeps working.
[MINOR] packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go:59 — the doc comment says logClientCert "reports, once per distinct issuer", but nothing in the code keys on the issuer. There are two global limiters (noCertObservations, withCertObservations), so the cert branch logs at most one line per 30s window whatever the issuer. It also contradicts the observation-state comment right above it at main.go:21-28, which deliberately argues against keying on the issuer. The code is the safe version here: a map keyed on an attacker-controlled issuer string is exactly the unbounded, forgeable memory-DoS that comment warns about. So the fix is the comment, not the code (something like "at most once per interval per outcome"). It's worth doing precisely because a later reader who "reconciles" the code to this comment would hand that DoS right back.
[NIT] packages/system/kubeovn-webhook/values.yaml:28 and templates/deployment.yaml:54 — setting clientCAFile today wires --client-ca-file, but nothing mounts a CA at that path, so os.ReadFile fails and log.Fatalf (main.go:132) crashes both replicas; under failurePolicy: Fail that stops pod creation cluster-wide. The values.yaml comment already spells this out as the deliberate step-one boundary (the mount and the reload land together in step two), and it's off by default, so it isn't a blocker. If you want the footgun un-pullable before step two, a values.schema.json (or a template guard) that fails helm template on a non-empty clientCAFile would turn a runtime cluster-wide outage into a render error. Optional.
| withCertObservations observationLimiter | ||
| ) | ||
|
|
||
| // logClientCert reports, once per distinct issuer, whether the caller presented |
There was a problem hiding this comment.
[MINOR] doc comment says per-issuer, code is per-outcome
The comment says logClientCert "reports, once per distinct issuer", but nothing keys on the issuer. There are two global limiters (noCertObservations, withCertObservations), so the cert branch logs at most one line per 30s window whatever the issuer. It also contradicts the observation-state comment right above it at main.go:21-28, which deliberately argues against keying on the issuer. The code is the safe version: a map keyed on an attacker-controlled issuer string is the unbounded, forgeable memory-DoS that comment warns about. Fix the comment, not the code (something like "at most once per interval per outcome"). Worth doing because a later reader who reconciles the code to this comment would hand that DoS right back.
| # carrying the API server's client CA, a pool that reloads when that CA rotates, | ||
| # and this key pointed at the mounted path. Until then the flag exists so the | ||
| # observation above can run, not so it can be switched on. | ||
| clientCAFile: "" |
There was a problem hiding this comment.
[NIT] clientCAFile is a documented footgun without an in-pod CA mount
Setting clientCAFile today wires --client-ca-file (templates/deployment.yaml:54), but nothing mounts a CA at that path, so os.ReadFile fails and log.Fatalf (main.go:132) crashes both replicas; under failurePolicy: Fail that stops pod creation cluster-wide. The comment above already spells this out as the deliberate step-one boundary and it's off by default, so not a blocker. Optional: a values.schema.json (or template guard) that fails helm template on a non-empty clientCAFile would turn a runtime cluster-wide outage into a render error.
) ## What this PR does Closes the exposure behind GHSA-g883-q79m-8225 by constraining who can reach the kube-ovn webhook's serving port. The `/mutate-pods` handler returns namespace-derived annotations to any caller that reaches it, and until now nothing stopped an arbitrary in-cluster pod from being that caller. ### Why the client-certificate path does not close it #2698 added `--client-ca-file`, deliberately off by default, as observation mode. Turning it on requires the API server to **present** a certificate, which it does only when its `AdmissionConfiguration` names a `kubeConfigFile` for the webhook plugins. Checked against a running Cozystack control plane rather than assumed: the `AdmissionConfiguration` there carries a `PodSecurity` plugin block and nothing else, so no client certificate is offered to admission webhooks at all. Flipping to `RequireAndVerifyClientCert` on such a cluster would reject the API server itself, and with `failurePolicy: Fail` that stops pod creation cluster-wide. This matters for how the remaining work was scoped: the plan had been to observe issuer logs for a day and then pin a CA. No amount of observation changes the outcome, because the absence is a property of the control plane's configuration, not of the traffic. Making `--client-ca-file` usable is a separate, larger change to how the platform configures the API server, and it is not a prerequisite for closing the exposure. ### What the policy does Denies pod traffic and world traffic to the serving port, and needs no carve-out for the legitimate caller: the API server runs on the host network, so it is not a Cilium endpoint and `fromEndpoints` never matches it. Unlike the ingress-nginx admission webhook this component exists only on the management cluster, so there is no konnectivity-tunnelled caller to exempt either — and consequently no forgeable pod label anywhere in the selector. It is a `CiliumNetworkPolicy` rather than a core `NetworkPolicy`, and that distinction is the reason it works at all: `packages/system/kubeovn/values.yaml` sets `ENABLE_NP: false`, so kube-ovn programs no ACLs and a core NetworkPolicy would be created and never enforced. The values comment that claimed NetworkPolicy was simply not an option is corrected — it was true only of the core kind. `webhookPolicy.enabled` lets a cluster whose networking variant ships no Cilium turn the object off. Deny-only with default-deny explicitly disabled, matching the ingress-nginx admission policy: putting these pods behind a default-deny would mean enumerating every port they serve and breaking traffic on the first one forgotten. ### Verification Eight helm-unittest cases cover the port, the all-namespaces span, the absence of any label-based exemption, the world deny, the disabled default-deny, the Cilium kind, the selector and the off switch. The rendered object was additionally accepted by a live API server via `kubectl apply --dry-run=server`, so the CRD validates it as written. Not verified: that a denied pod actually fails to reach the port at runtime. That needs a live negative test against the policy, which is the same gap the ingress-nginx admission policy carries. ### Screenshots Not applicable — no UI change. ### Downstream repositories Walked the trigger map against the diff. Nothing is reached: the change is one chart template, its values and its tests under `packages/system/`, with no package added or removed under `packages/apps/` or `packages/extra/`, no `packages/core/platform/values.yaml` key, no platform variant or bundle change, nothing under `hack/`, and no node prerequisite. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note fix(kubeovn-webhook): the kube-ovn webhook's serving port is now closed to in-cluster pods and external traffic by a CiliumNetworkPolicy. It previously answered any in-cluster pod with namespace-derived annotations (GHSA-g883-q79m-8225). Disable with webhookPolicy.enabled=false on clusters whose networking variant ships no Cilium. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Cilium network policy to protect the namespace-annotation webhook. * The policy is enabled by default and restricts access to the webhook port, blocking unauthorized in-cluster and external traffic. * Standardized the webhook port across its deployment, service, and network policy configuration. * **Bug Fixes** * Improved webhook network isolation when standard Kubernetes NetworkPolicy enforcement is unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
The /mutate-pods handler returns namespace-derived annotations to any caller that reaches it (GHSA-g883-q79m-8225), and until now nothing stopped an arbitrary in-cluster pod from being that caller. Client-certificate verification, added in cozystack#2698, cannot close this on its own. Enabling --client-ca-file requires the API server to *present* a certificate, which it does only when its AdmissionConfiguration names a kubeConfigFile for the webhook plugins. Checked on a running Cozystack control plane: the AdmissionConfiguration carries a PodSecurity block and nothing else, so no certificate is offered at all. Flipping to RequireAndVerifyClientCert there would reject the API server itself, and with failurePolicy: Fail that stops pod creation cluster-wide. No amount of log observation changes that — it is a property of the control plane's configuration, not of the traffic. So the network is where the caller can actually be constrained. The policy denies pod and world traffic to the serving port and needs no carve-out: the API server runs on the host network, so it is not a Cilium endpoint and fromEndpoints never matches it. Unlike the ingress-nginx admission webhook this component exists only on the management cluster, so there is no konnectivity-tunnelled caller to exempt either, and no forgeable pod label anywhere in the selector. It is a CiliumNetworkPolicy rather than a core NetworkPolicy, and that distinction is why it works at all: kube-ovn runs with ENABLE_NP: false and programs no ACLs, so a core NetworkPolicy would be created and never enforced. The values comment claiming NetworkPolicy was simply not an option is corrected — it was true only of the core kind. Deny-only with default-deny disabled, matching the ingress-nginx admission policy: putting these pods behind a default-deny would mean enumerating every port they serve and breaking traffic on the first one forgotten. Signed-off-by: Andrei Kvapil <[email protected]>
Backport of #3955. The automated cherry-pick carried only the first commit of that PR and committed the values.yaml conflict, so this resolution brings the branch to what actually merged to main: the port comes from a single helper feeding the container port, the Service targetPort and the policy, the rationale sits in template comments rather than being rendered into every manifest, and the dead egress default-deny toggle is gone. Two deliberate differences from main. The image stays on the digest this release line already ships rather than main's older one, and the --client-ca-file argument is left out along with the values key it reads: both belong to #2698, which was never backported here, and the image on this line does not take the flag. Assisted-By: Claude Signed-off-by: Andrei Kvapil <[email protected]>
Add a global commit-msg hook wired through core.hooksPath. It strips Claude Code session URLs and Claude-Session trailers, drops sign-off and co-author lines attributed to Claude or an anthropic.com address, removes the harness-generated attribution signature, and normalises a product-named Assisted-by trailer to the neutral Assisted-by: LLM. Both a session link and a sign-off in Claude's name reached public history in cozystack/cozystack#2698; the CLAUDE.md rules alone did not hold, so enforce them mechanically. The hook re-runs a repository's own commit-msg afterwards, which core.hooksPath would otherwise disable. Update CLAUDE.md accordingly: use Assisted-by: LLM rather than a model name, commit under the personal identity, never derive commit identity from the session context, and never sign a commit as Claude. Signed-off-by: Andrei Kvapil <[email protected]>
…fault (#2698) ## What this PR does Adds client-certificate verification to the kube-ovn webhook, and turns it on only when an operator supplies a CA bundle. `--client-ca-file` is new and empty by default. With it empty the server sets `tls.RequestClientCert`: it asks callers for a certificate, records the issuer of whatever arrives, and serves the request either way. Set the flag and the server switches to `tls.RequireAndVerifyClientCert`, so a caller without a certificate signed by that CA is refused at the TLS handshake. **This is step one of two, and on its own it does not close [GHSA-g883-q79m-8225](https://github.com/cozystack/cozystack/security/advisories/GHSA-g883-q79m-8225).** With the default empty value nothing is rejected — the webhook still answers an unauthenticated caller, and the only change is that the caller's issuer now appears in the logs. That is deliberate: switching straight to `RequireAndVerifyClientCert` would break every cluster whose API server sends no client certificate, and there is no way to know from here which those are. The logs are how we find out. Step two, in a follow-up: read a day of issuer logs on a Talos cluster and on `isp-full-generic`, confirm the API server presents a certificate we can pin, then set the CA bundle so verification becomes mandatory. The advisory should be published as fixed only after that lands — not with this PR. An earlier revision also carried a NetworkPolicy restricting ingress to `kube-system`. It was dropped, and the title has been corrected to match; only the TLS path is left here. ### Screenshots N/A — no user-facing or UI change. ### Downstream repositories - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: Walked the trigger map against the diff: no row matches. The change touches one system package and the Go image it builds — no app package, no `values.schema.json`, no `ApplicationDefinition`, no platform variant, no node prerequisite. ### Release note ```release-note feat(kubeovn-webhook): the webhook now accepts --client-ca-file. Left empty (the default) it requests and logs client certificates without requiring them; set to a CA bundle it rejects callers that do not present a certificate signed by that CA. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added optional client-certificate verification for the pod mutation webhook through configurable CA settings. * Added separate rate limits for requests with and without client certificates. * Added remote-address logging and clearer verification status reporting. * **Security** * Requests continue to be forwarded while certificate observations are recorded when verification is disabled. * Serving-certificate updates remain supported; client CA changes require a webhook restart. * **Documentation** * Documented client CA configuration and current certificate rotation limitations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
The /mutate-pods handler returns namespace-derived annotations to any caller that reaches it (GHSA-g883-q79m-8225), and until now nothing stopped an arbitrary in-cluster pod from being that caller. Client-certificate verification, added in #2698, cannot close this on its own. Enabling --client-ca-file requires the API server to *present* a certificate, which it does only when its AdmissionConfiguration names a kubeConfigFile for the webhook plugins. Checked on a running Cozystack control plane: the AdmissionConfiguration carries a PodSecurity block and nothing else, so no certificate is offered at all. Flipping to RequireAndVerifyClientCert there would reject the API server itself, and with failurePolicy: Fail that stops pod creation cluster-wide. No amount of log observation changes that — it is a property of the control plane's configuration, not of the traffic. So the network is where the caller can actually be constrained. The policy denies pod and world traffic to the serving port and needs no carve-out: the API server runs on the host network, so it is not a Cilium endpoint and fromEndpoints never matches it. Unlike the ingress-nginx admission webhook this component exists only on the management cluster, so there is no konnectivity-tunnelled caller to exempt either, and no forgeable pod label anywhere in the selector. It is a CiliumNetworkPolicy rather than a core NetworkPolicy, and that distinction is why it works at all: kube-ovn runs with ENABLE_NP: false and programs no ACLs, so a core NetworkPolicy would be created and never enforced. The values comment claiming NetworkPolicy was simply not an option is corrected — it was true only of the core kind. Deny-only with default-deny disabled, matching the ingress-nginx admission policy: putting these pods behind a default-deny would mean enumerating every port they serve and breaking traffic on the first one forgotten. Signed-off-by: Andrei Kvapil <[email protected]>
) ## What this PR does Closes the exposure behind GHSA-g883-q79m-8225 by constraining who can reach the kube-ovn webhook's serving port. The `/mutate-pods` handler returns namespace-derived annotations to any caller that reaches it, and until now nothing stopped an arbitrary in-cluster pod from being that caller. ### Why the client-certificate path does not close it #2698 added `--client-ca-file`, deliberately off by default, as observation mode. Turning it on requires the API server to **present** a certificate, which it does only when its `AdmissionConfiguration` names a `kubeConfigFile` for the webhook plugins. Checked against a running Cozystack control plane rather than assumed: the `AdmissionConfiguration` there carries a `PodSecurity` plugin block and nothing else, so no client certificate is offered to admission webhooks at all. Flipping to `RequireAndVerifyClientCert` on such a cluster would reject the API server itself, and with `failurePolicy: Fail` that stops pod creation cluster-wide. This matters for how the remaining work was scoped: the plan had been to observe issuer logs for a day and then pin a CA. No amount of observation changes the outcome, because the absence is a property of the control plane's configuration, not of the traffic. Making `--client-ca-file` usable is a separate, larger change to how the platform configures the API server, and it is not a prerequisite for closing the exposure. ### What the policy does Denies pod traffic and world traffic to the serving port, and needs no carve-out for the legitimate caller: the API server runs on the host network, so it is not a Cilium endpoint and `fromEndpoints` never matches it. Unlike the ingress-nginx admission webhook this component exists only on the management cluster, so there is no konnectivity-tunnelled caller to exempt either — and consequently no forgeable pod label anywhere in the selector. It is a `CiliumNetworkPolicy` rather than a core `NetworkPolicy`, and that distinction is the reason it works at all: `packages/system/kubeovn/values.yaml` sets `ENABLE_NP: false`, so kube-ovn programs no ACLs and a core NetworkPolicy would be created and never enforced. The values comment that claimed NetworkPolicy was simply not an option is corrected — it was true only of the core kind. `webhookPolicy.enabled` lets a cluster whose networking variant ships no Cilium turn the object off. Deny-only with default-deny explicitly disabled, matching the ingress-nginx admission policy: putting these pods behind a default-deny would mean enumerating every port they serve and breaking traffic on the first one forgotten. ### Verification Eight helm-unittest cases cover the port, the all-namespaces span, the absence of any label-based exemption, the world deny, the disabled default-deny, the Cilium kind, the selector and the off switch. The rendered object was additionally accepted by a live API server via `kubectl apply --dry-run=server`, so the CRD validates it as written. Not verified: that a denied pod actually fails to reach the port at runtime. That needs a live negative test against the policy, which is the same gap the ingress-nginx admission policy carries. ### Screenshots Not applicable — no UI change. ### Downstream repositories Walked the trigger map against the diff. Nothing is reached: the change is one chart template, its values and its tests under `packages/system/`, with no package added or removed under `packages/apps/` or `packages/extra/`, no `packages/core/platform/values.yaml` key, no platform variant or bundle change, nothing under `hack/`, and no node prerequisite. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note fix(kubeovn-webhook): the kube-ovn webhook's serving port is now closed to in-cluster pods and external traffic by a CiliumNetworkPolicy. It previously answered any in-cluster pod with namespace-derived annotations (GHSA-g883-q79m-8225). Disable with webhookPolicy.enabled=false on clusters whose networking variant ships no Cilium. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Cilium network policy to protect the namespace-annotation webhook. * The policy is enabled by default and restricts access to the webhook port, blocking unauthorized in-cluster and external traffic. * Standardized the webhook port across its deployment, service, and network policy configuration. * **Bug Fixes** * Improved webhook network isolation when standard Kubernetes NetworkPolicy enforcement is unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
Adds client-certificate verification to the kube-ovn webhook, and turns it on only when an operator supplies a CA bundle.
--client-ca-fileis new and empty by default. With it empty the server setstls.RequestClientCert: it asks callers for a certificate, records the issuer of whatever arrives, and serves the request either way. Set the flag and the server switches totls.RequireAndVerifyClientCert, so a caller without a certificate signed by that CA is refused at the TLS handshake.This is step one of two, and on its own it does not close GHSA-g883-q79m-8225. With the default empty value nothing is rejected — the webhook still answers an unauthenticated caller, and the only change is that the caller's issuer now appears in the logs. That is deliberate: switching straight to
RequireAndVerifyClientCertwould break every cluster whose API server sends no client certificate, and there is no way to know from here which those are. The logs are how we find out.Step two, in a follow-up: read a day of issuer logs on a Talos cluster and on
isp-full-generic, confirm the API server presents a certificate we can pin, then set the CA bundle so verification becomes mandatory. The advisory should be published as fixed only after that lands — not with this PR.An earlier revision also carried a NetworkPolicy restricting ingress to
kube-system. It was dropped, and the title has been corrected to match; only the TLS path is left here.Screenshots
N/A — no user-facing or UI change.
Downstream repositories
Walked the trigger map against the diff: no row matches. The change touches one system package and the Go image it builds — no app package, no
values.schema.json, noApplicationDefinition, no platform variant, no node prerequisite.Release note
Summary by CodeRabbit