Skip to content

feat(kubeovn-webhook): add client-certificate verification, off by default - #2698

Merged
Andrei Kvapil (kvaps) merged 5 commits into
mainfrom
claude/slack-session-F3Fhj
Aug 24, 2026
Merged

Andrei Kvapil (kvaps) merged 5 commits into
mainfrom
claude/slack-session-F3Fhj

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented May 20, 2026 •

Copy link
Copy Markdown
Member

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. 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

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

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.

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.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Security Hardening: Implemented a new NetworkPolicy to restrict ingress traffic to the webhook pod, ensuring only connections from the kube-system namespace are permitted.
  • mTLS Authentication: Added optional mTLS support via a new --client-ca-file flag, allowing for secure client certificate verification.
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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai

coderabbitai Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The webhook now rate-limits certificate observations, logs peer addresses, and forwards every request. It supports optional client CA verification through --client-ca-file. Helm exposes this option through clientCAFile, which defaults to empty.

Changes

Webhook mTLS Configuration

Layer / File(s) Summary
Certificate observation and forwarding
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go, packages/system/kubeovn-webhook/images/kubeovn-webhook/observation_test.go
Separate synchronized rate limiters handle certificate-present and certificate-absent requests. Logs include peer addresses and verification status. Tests cover timing, concurrency, logging, and unconditional forwarding.
Webhook TLS and certificate handling
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go, packages/system/kubeovn-webhook/images/kubeovn-webhook/cert.go
The webhook supports optional CA-file loading, enforces TLS 1.2 or newer, requests client certificates, and documents restart requirements for client CA rotation.
Helm client CA configuration
packages/system/kubeovn-webhook/values.yaml, packages/system/kubeovn-webhook/templates/deployment.yaml
The chart documents clientCAFile and adds --client-ca-file only when the value is configured.

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

Merge Risk: ⚪ Minimal · up to 2eb53

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
Loading

Suggested reviewers: lllamnyp

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: configurable client-certificate verification with enforcement disabled by default.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/slack-session-F3Fhj

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.

@dosubot dosubot Bot added area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) kind/bug Categorizes issue or PR as related to a bug security Security-related issues and features security/confirmed Confirmed vulnerability security/fixed Fix released labels May 20, 2026
@kvaps
Andrei Kvapil (kvaps) force-pushed the claude/slack-session-F3Fhj branch from e1045bf to b6ba7cf Compare May 20, 2026 16:44
@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review size/M This PR changes 30-99 lines, ignoring generated files labels May 20, 2026

@gemini-code-assist gemini-code-assist 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.

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.

Comment thread packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go Outdated
Comment thread packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go Outdated
Comment thread packages/system/kubeovn-webhook/templates/networkpolicy.yaml Outdated
@lexfrei Aleksei Sviridkin (lexfrei) removed the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label May 25, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both hardening mechanisms in this PR are inert in a default Cozystack cluster, so the disclosure vector stays open:

  1. 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=false and programs no ACLs. The added templates/networkpolicy.yaml is never enforced.
  2. mTLS is dead code. --client-ca-file defaults to "" and is never passed in templates/deployment.yaml; no apiserver-side client-cert config exists, so the RequireAndVerifyClientCert branch 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.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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.

@kvaps

Copy link
Copy Markdown
Member Author

Rebased onto current main and reworked along the lines of the review. IvanHunters — your analysis was right on both counts; I verified each before changing anything.

NetworkPolicy: dropped, not fixed. packages/system/kubeovn/values.yaml:4 sets ENABLE_NP: false (upstream default is true), which reaches kube-ovn-controller as --enable-np=false (charts/kube-ovn/templates/controller-deploy.yaml:127), so no ACLs are programmed and the object would have been created and never enforced. Making it real means enabling ENABLE_NP cluster-wide, which is a much larger change than this disclosure justifies — and shipping something that looks like a control but enforces nothing is worse than shipping nothing.

Client-cert verification: now reachable, but off by default. clientCAFile is a chart value and the flag is passed when set, so the RequireAndVerifyClientCert branch is no longer dead code.

It stays off by default deliberately. This webhook is registered failurePolicy: Fail, and the API server presents a client certificate only when the cluster runs an AdmissionConfiguration naming a kubeConfigFile for it — which is not guaranteed across the variants this ships on. Requiring one where none is sent would fail every admission call and stop pod creation cluster-wide. So the default is now tls.RequestClientCert: the certificate is requested and logged, never required. Nothing is rejected, admission cannot break, and the log states per issuer whether enforcement is possible:

client certificate: presented, issuer "CN=kubernetes" — enforcement can be enabled with --client-ca-file for this CA
client certificate: none presented — client-ca-file enforcement is NOT yet safe on this cluster

Setting clientCAFile then flips it to RequireAndVerifyClientCert. The plan is to read those logs on a Talos cluster and on isp-full-generic, and enable enforcement in a follow-up where it is confirmed safe.

Also rebased past the certificate-reloader work that landed since May, so newReloadingTLSConfig is preserved and MinVersion: TLS1.2 is applied to it rather than replacing it with a static config.

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 — ovn.kubernetes.io/routes and ovn.kubernetes.io/port_security (admission.go:78-101) — the ServiceAccount holds namespaces: [get] only, so namespaces cannot be enumerated, and the Service is ClusterIP, not NodePort/LoadBalancer. The kube-system PoC as written returns no patch at all, since that namespace carries neither annotation. Real, and worth fixing, but low/medium in-cluster rather than critical — the severity and version range want correcting before it is published.

@github-actions github-actions Bot added the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label Aug 12, 2026

@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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e944619 and 04606e2.

📒 Files selected for processing (3)
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
  • packages/system/kubeovn-webhook/templates/deployment.yaml
  • packages/system/kubeovn-webhook/values.yaml

Comment thread packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go Outdated
Comment thread packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go Outdated
@kvaps Andrei Kvapil (kvaps) changed the title fix(kubeovn-webhook): add NetworkPolicy and mTLS support feat(kubeovn-webhook): add client-certificate verification, off by default Aug 19, 2026
Claude (claude) and others added 4 commits August 20, 2026 14:32
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]>
…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]>
@github-actions github-actions Bot added kind/feature Categorizes issue or PR as related to a new feature size/L This PR changes 100-499 lines, ignoring generated files and removed size/M This PR changes 30-99 lines, ignoring generated files labels Aug 20, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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-26 comment 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. Loading ClientCAs from a startup-only os.ReadFile brings 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 with failurePolicy: Fail pod 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 -tls secret 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 -tls secret), 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. Meanwhile values.yaml tells 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:80 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 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) or logClientCert (<none> vs issuer path), though the package already ships cert_test.go and 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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={{ . }}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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]>
@kvaps

Copy link
Copy Markdown
Member Author

Both blocking points confirmed against the code, and both are fixed in 2eb5374e. You were right that #3557 is the relevant precedent — not because this duplicates it, but because this PR quietly invalidated the premise it recorded.

The cert.go comment. It justified skipping client-CA reload with "the webhook configures no mTLS (no ClientCAs)", and this PR makes that false. Rewritten to state what is now true: the pool is read once at startup, a rotation of the API server's client CA leaves the webhook validating against the retired one, and with failurePolicy: Fail that stops pod creation cluster-wide. It also records that reloading has to land together with the mount, because neither is usable without the other.

The enable path. Verified: the pod mounts only webhook-certs, and the ca.crt in there is this webhook's serving CA, exactly as you said. Every possible value of the flag either aborts startup or rejects the API server. Rather than build the mount here, I trimmed the guidance — values.yaml now says to leave it empty, says why no path works, and defers enabling to the step that adds the volume, the reloading pool and the key together. Step one stays observation, which is what it claimed to be.

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 observation_test.go covers the limiter, the burst it exists to survive, concurrency, both branches, the enforcing wording, and that logging never blocks admission.

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 isp-full-generic. Tracked separately so it does not get lost.

@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.

🔇 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 noctx check. Import context and pass context.Background() to httptest.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

📥 Commits

Reviewing files that changed from the base of the PR and between 07fd4d9 and 2eb5374.

📒 Files selected for processing (4)
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/cert.go
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/observation_test.go
  • packages/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 IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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: ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.

@kvaps
Andrei Kvapil (kvaps) merged commit 4689b2b into main Aug 24, 2026
19 of 21 checks passed
@kvaps
Andrei Kvapil (kvaps) deleted the claude/slack-session-F3Fhj branch August 24, 2026 21:35
Andrei Kvapil (kvaps) added a commit that referenced this pull request Aug 26, 2026
)

## 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 -->
pull Bot pushed a commit to medampudi/cozystack that referenced this pull request Aug 27, 2026
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]>
Andrei Kvapil (kvaps) added a commit that referenced this pull request Aug 27, 2026
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]>
Andrei Kvapil (kvaps) added a commit to kvaps/dotfiles that referenced this pull request Sep 3, 2026
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]>
Andrei Kvapil (kvaps) added a commit that referenced this pull request Sep 7, 2026
…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 -->
Andrei Kvapil (kvaps) added a commit that referenced this pull request Sep 7, 2026
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]>
Andrei Kvapil (kvaps) added a commit that referenced this pull request Sep 7, 2026
)

## 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 -->
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) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug kind/feature Categorizes issue or PR as related to a new feature security/confirmed Confirmed vulnerability security/fixed Fix released security Security-related issues and features size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants