Skip to content

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

Open
Andrei Kvapil (kvaps) wants to merge 3 commits into
mainfrom
claude/slack-session-F3Fhj
Open

feat(kubeovn-webhook): add client-certificate verification, off by default#2698
Andrei Kvapil (kvaps) wants to merge 3 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.

@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 logs client-certificate presence and issuers. It requests certificates by default and enforces verification when a configured CA bundle is supplied. Helm conditionally passes the CA file argument to the container.

Changes

Webhook mTLS Configuration

Layer / File(s) Summary
Webhook TLS and certificate handling
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
The server requests client certificates by default, logs distinct issuers, and enforces certificates signed by the configured CA bundle.
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) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebhookClient
  participant HTTPServer
  participant logClientCert
  participant ExistingHandler
  WebhookClient->>HTTPServer: TLS request with optional client certificate
  HTTPServer->>logClientCert: Forward request
  logClientCert->>logClientCert: Record certificate issuer or absence
  logClientCert->>ExistingHandler: Forward request
Loading

Possibly related PRs

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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the added optional client-certificate verification, which is a significant change in the pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • 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.


flag.StringVar(&tlsCertFile, "tls-cert-file", "/etc/webhook/certs/tls.crt", "TLS certificate file.")
flag.StringVar(&tlsKeyFile, "tls-key-file", "/etc/webhook/certs/tls.key", "TLS key file.")
flag.StringVar(&clientCAFile, "client-ca-file", "", "CA certificate for verifying client certificates (mTLS). If empty, client certificate verification is disabled.")

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.

medium

The --client-ca-file flag is defined here to support mTLS, but it appears that the corresponding Helm templates (e.g., the Deployment) have not been updated to pass this flag to the container. Without these changes, the mTLS feature cannot be easily enabled or configured via the Helm chart.

Comment on lines +40 to +42
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
}

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.

low

It is a security best practice to explicitly set a minimum TLS version (e.g., TLS 1.2) in the tls.Config to prevent the use of older, insecure protocols.

Suggested change
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
MinVersion: tls.VersionTLS12,
}

Comment on lines +4 to +7
name: {{ include "namespace-annotation-webhook.fullname" . }}
labels:
app.kubernetes.io/name: {{ include "namespace-annotation-webhook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}

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.

low

It is recommended to quote string values in Helm templates to ensure they are correctly interpreted as strings and to handle any special characters.

References
  1. Helm template correctness: missing quote (link)

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

Claude (claude) and others added 3 commits August 12, 2026 21:48
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 <noreply@anthropic.com>
Prevent use of older, insecure TLS protocols.

https://claude.ai/code/session_013DRdaUJMYxonsjVpWxJSq3
Signed-off-by: Claude <noreply@anthropic.com>
…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 <kvapss@gmail.com>
@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 on lines +39 to +46
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
key = r.TLS.PeerCertificates[0].Issuer.String()
}
if _, loaded := seenClientIssuers.LoadOrStore(key, struct{}{}); !loaded {
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 — enforcement can be enabled with --client-ca-file for this CA", key)

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- main.go outline/context ---'
wc -l packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
cat -n packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go | sed -n '1,125p'
printf '%s\n' '--- values.yaml ---'
cat -n packages/system/kubeovn-webhook/values.yaml | sed -n '1,70p'
printf '%s\n' '--- related symbols/usages ---'
rg -n --glob '*.go' 'seenClientIssuers|logClientCert|ClientAuth|client-ca-file|failurePolicy|RequireAndVerifyClientCert|RequestClientCert' packages/system/kubeovn-webhook
printf '%s\n' '--- manifests/config references ---'
rg -n 'client-ca-file|failurePolicy|AdmissionConfiguration|kubeConfigFile|kubeovn-webhook' packages/system/kubeovn-webhook

Repository: cozystack/cozystack

Length of output: 11908


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
p = Path("packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go")
text = p.read_text()
for term in ("seenClientIssuers", "logClientCert", "ClientAuth"):
    print(f"--- occurrences of {term} ---")
    for i, line in enumerate(text.splitlines(), 1):
        if term in line:
            print(f"{i}: {line}")
PY
printf '%s\n' '--- repository metadata for this component ---'
git ls-files packages/system/kubeovn-webhook
printf '%s\n' '--- comments and configuration mentioning certificate validation ---'
rg -n -i 'certificate|issuer|verify|trust|ca bundle|ca-file|client auth' packages/system/kubeovn-webhook

Repository: cozystack/cozystack

Length of output: 10219


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- webhook exposure and TLS arguments ---'
cat -n packages/system/kubeovn-webhook/templates/service.yaml
cat -n packages/system/kubeovn-webhook/templates/deployment.yaml | sed -n '35,75p'
cat -n packages/system/kubeovn-webhook/templates/mutatingwebhookconfiguration.yaml | sed -n '1,45p'
printf '%s\n' '--- related network policy and service references ---'
rg -n -i 'kubeovn-webhook|namespace-annotation-webhook|NetworkPolicy|network policy' packages/system/kubeovn-webhook packages/system/kubeovn
printf '%s\n' '--- chart values/schema references ---'
rg -n 'clientCAFile|client-ca-file' packages/system

Repository: cozystack/cozystack

Length of output: 16723


🏁 Script executed:

#!/bin/bash
set -eu
if ! command -v go >/dev/null 2>&1; then
  echo "go unavailable"
  exit 0
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/main.go" <<'EOF'
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"time"
)

func cert(subject, issuer pkix.Name, ca bool, key *ecdsa.PrivateKey, parent *x509.Certificate, parentKey *ecdsa.PrivateKey) (tls.Certificate, *x509.Certificate) {
	now := time.Now()
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(1), Subject: subject, Issuer: issuer,
		NotBefore: now.Add(-time.Minute), NotAfter: now.Add(time.Hour),
		KeyUsage: x509.KeyUsageDigitalSignature,
		BasicConstraintsValid: true, IsCA: ca,
	}
	if ca { tmpl.KeyUsage |= x509.KeyUsageCertSign }
	if parent == nil { parent, parentKey = tmpl, key }
	der, err := x509.CreateCertificate(rand.Reader, tmpl, parent, &key.PublicKey, parentKey)
	if err != nil { panic(err) }
	c, err := x509.ParseCertificate(der)
	if err != nil { panic(err) }
	return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}, c
}

func main() {
	caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	caName := pkix.Name{CommonName: "trusted-ca"}
	caTLS, ca := cert(caName, caName, true, caKey, nil, nil)
	_ = caTLS
	clientKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	// This client certificate is self-signed and has a deliberately chosen issuer.
	clientName := pkix.Name{CommonName: "attacker"}
	clientTLS, client := cert(clientName, caName, false, clientKey, ca, caKey)

	serverKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	serverTLS, _ := cert(pkix.Name{CommonName: "server"}, pkix.Name{CommonName: "server"}, false, serverKey, nil, nil)

	for _, auth := range []tls.ClientAuthType{tls.RequestClientCert, tls.RequireAndVerifyClientCert} {
		cfg := &tls.Config{Certificates: []tls.Certificate{serverTLS}, ClientAuth: auth}
		if auth == tls.RequireAndVerifyClientCert {
			cfg.ClientCAs = x509.NewCertPool()
			cfg.ClientCAs.AddCert(ca)
		}
		ln, err := tls.Listen("tcp", "127.0.0.1:0", cfg)
		if err != nil { panic(err) }
		done := make(chan string, 1)
		go func() {
			conn, err := ln.Accept()
			if err != nil { done <- "accept error: " + err.Error(); return }
			tc := conn.(*tls.Conn)
			err = tc.Handshake()
			if err != nil { done <- "server handshake error: " + err.Error(); return }
			state := tc.ConnectionState()
			issuer := "<none>"
			if len(state.PeerCertificates) > 0 { issuer = state.PeerCertificates[0].Issuer.String() }
			done <- fmt.Sprintf("auth=%v issuer=%q verified_chains=%d", auth, issuer, len(state.VerifiedChains))
			conn.Close()
		}()
		clientCfg := &tls.Config{InsecureSkipVerify: true, Certificates: []tls.Certificate{clientTLS}} // probe only
		conn, err := tls.Dial("tcp", ln.Addr().String(), clientCfg)
		if err == nil { conn.Close() }
		fmt.Println(<-done)
		ln.Close()
	}
	_ = pem.Block{}
}
EOF
(cd "$tmpdir" && go run main.go)

Repository: cozystack/cozystack

Length of output: 269


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- local Go API documentation ---'
CGO_ENABLED=0 go doc crypto/tls.ClientAuthType

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/main.go" <<'EOF'
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"time"
)

func makeCert(name, issuer string, ca bool, parent *x509.Certificate, parentKey *ecdsa.PrivateKey) (tls.Certificate, *x509.Certificate, *ecdsa.PrivateKey) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil { panic(err) }
	now := time.Now()
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{CommonName: name},
		Issuer: pkix.Name{CommonName: issuer},
		NotBefore: now.Add(-time.Minute), NotAfter: now.Add(time.Hour),
		KeyUsage: x509.KeyUsageDigitalSignature,
		BasicConstraintsValid: true, IsCA: ca,
	}
	if ca { tmpl.KeyUsage |= x509.KeyUsageCertSign }
	if parent == nil { parent, parentKey = tmpl, key }
	der, err := x509.CreateCertificate(rand.Reader, tmpl, parent, &key.PublicKey, parentKey)
	if err != nil { panic(err) }
	cert, err := x509.ParseCertificate(der)
	if err != nil { panic(err) }
	return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}, cert, key
}

func main() {
	trustedTLS, trustedCA, trustedKey := makeCert("trusted-ca", "trusted-ca", true, nil, nil)
	_ = trustedTLS
	attackerTLS, _, _ := makeCert("attacker", "attacker-chosen-issuer", false, nil, nil)
	serverTLS, _, _ := makeCert("server", "server", false, nil, nil)

	for _, auth := range []tls.ClientAuthType{tls.RequestClientCert, tls.RequireAndVerifyClientCert} {
		cfg := &tls.Config{Certificates: []tls.Certificate{serverTLS}, ClientAuth: auth}
		if auth == tls.RequireAndVerifyClientCert {
			cfg.ClientCAs = x509.NewCertPool()
			cfg.ClientCAs.AddCert(trustedCA)
		}
		ln, err := tls.Listen("tcp", "127.0.0.1:0", cfg)
		if err != nil { panic(err) }
		result := make(chan string, 1)
		go func() {
			conn, err := ln.Accept()
			if err != nil { result <- "accept error: " + err.Error(); return }
			tc := conn.(*tls.Conn)
			err = tc.Handshake()
			if err != nil { result <- "server handshake error: " + err.Error(); return }
			state := tc.ConnectionState()
			issuer := "<none>"
			if len(state.PeerCertificates) > 0 { issuer = state.PeerCertificates[0].Issuer.String() }
			result <- fmt.Sprintf("auth=%v issuer=%q verified_chains=%d", auth, issuer, len(state.VerifiedChains))
			conn.Close()
		}()
		clientCfg := &tls.Config{InsecureSkipVerify: true, Certificates: []tls.Certificate{attackerTLS}} // probe only
		conn, dialErr := tls.Dial("tcp", ln.Addr().String(), clientCfg)
		if dialErr == nil { conn.Close() }
		fmt.Printf("client_error=%v; %s\n", dialErr, <-result)
		ln.Close()
	}
	_ = net.IPv4(127, 0, 0, 1)
	_ = pem.Block{}
	_ = trustedKey
}
EOF
(cd "$tmpdir" && CGO_ENABLED=0 go run main.go)

Repository: cozystack/cozystack

Length of output: 149


Do not treat observation-mode certificate metadata as authenticated API server identity.

tls.RequestClientCert does not verify client certificates. Therefore, PeerCertificates[0].Issuer is untrusted metadata. A reachable caller can influence the log and poison seenClientIssuers before the API server connects.

Use trusted control-plane configuration to select --client-ca-file. Do not use the observation log to confirm API server identity.

📍 Affects 2 files
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go#L39-L46 (this comment)
  • packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go#L78-L81
  • packages/system/kubeovn-webhook/values.yaml#L12-L22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go` around lines
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.

if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
key = r.TLS.PeerCertificates[0].Issuer.String()
}
if _, loaded := seenClientIssuers.LoadOrStore(key, struct{}{}); !loaded {

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
sed -n '1,180p' packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go
printf '%s\n' '--- related configuration and references ---'
rg -n -C 4 'seenClientIssuers|RequestClientCert|ClientAuth|issuer|observation|observation mode' \
  packages/system/kubeovn-webhook
printf '%s\n' '--- repository status summary ---'
git diff --stat

Repository: cozystack/cozystack

Length of output: 13637


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- Go TLS documentation ---'
go doc crypto/tls.ClientAuthType
go doc crypto/tls.Config.ClientAuth
printf '%s\n' '--- standalone TLS behavior probe ---'
cat > /tmp/request-client-cert-probe.go <<'GO'
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"sync"
	"time"
)

func cert(issuer string) (tls.Certificate, error) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return tls.Certificate{}, err
	}
	template := &x509.Certificate{
		SerialNumber:          big.NewInt(1),
		Subject:               pkix.Name{CommonName: "client"},
		Issuer:                pkix.Name{CommonName: issuer},
		NotBefore:             time.Now().Add(-time.Minute),
		NotAfter:              time.Now().Add(time.Hour),
		KeyUsage:              x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
	}
	der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
	if err != nil {
		return tls.Certificate{}, err
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyDER, err := x509.MarshalPKCS8PrivateKey(key)
	if err != nil {
		return tls.Certificate{}, err
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
	return tls.X509KeyPair(certPEM, keyPEM)
}

func main() {
	serverCert, err := cert("server")
	if err != nil {
		panic(err)
	}
	for _, mode := range []tls.ClientAuthType{
		tls.RequestClientCert,
		tls.RequireAndVerifyClientCert,
	} {
		clientCert, err := cert("attacker-" + mode.String())
		if err != nil {
			panic(err)
		}
		serverConn, clientConn := net.Pipe()
		server := tls.Server(serverConn, &tls.Config{
			Certificates: []tls.Certificate{serverCert},
			ClientAuth:   mode,
		})
		client := tls.Client(clientConn, &tls.Config{
			InsecureSkipVerify: true,
			Certificates:       []tls.Certificate{clientCert},
		})
		var wg sync.WaitGroup
		wg.Add(1)
		go func() {
			defer wg.Done()
			_ = server.Handshake()
		}()
		clientErr := client.Handshake()
		wg.Wait()
		peerCount := len(server.ConnectionState().PeerCertificates)
		issuer := "<none>"
		if peerCount > 0 {
			issuer = server.ConnectionState().PeerCertificates[0].Issuer.String()
		}
		fmt.Printf("mode=%s client_handshake=%v peer_certificates=%d issuer=%q\n",
			mode, clientErr, peerCount, issuer)
		server.Close()
		client.Close()
	}
}
GO
go run /tmp/request-client-cert-probe.go
rm -f /tmp/request-client-cert-probe.go

Repository: cozystack/cozystack

Length of output: 820


🏁 Script executed:

#!/bin/bash
set -eu
cat > /tmp/request-client-cert-probe.go <<'GO'
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"sync"
	"time"
)

func cert(issuer string) (tls.Certificate, error) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return tls.Certificate{}, err
	}
	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{CommonName: "client"},
		Issuer: pkix.Name{CommonName: issuer},
		NotBefore: time.Now().Add(-time.Minute),
		NotAfter:  time.Now().Add(time.Hour),
		KeyUsage:  x509.KeyUsageDigitalSignature,
		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
	}
	der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
	if err != nil {
		return tls.Certificate{}, err
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyDER, err := x509.MarshalPKCS8PrivateKey(key)
	if err != nil {
		return tls.Certificate{}, err
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
	return tls.X509KeyPair(certPEM, keyPEM)
}

func main() {
	serverCert, err := cert("server")
	if err != nil {
		panic(err)
	}
	for _, mode := range []tls.ClientAuthType{
		tls.RequestClientCert,
		tls.RequireAndVerifyClientCert,
	} {
		clientCert, err := cert("attacker-" + mode.String())
		if err != nil {
			panic(err)
		}
		serverConn, clientConn := net.Pipe()
		server := tls.Server(serverConn, &tls.Config{
			Certificates: []tls.Certificate{serverCert},
			ClientAuth:   mode,
		})
		client := tls.Client(clientConn, &tls.Config{
			InsecureSkipVerify: true,
			Certificates:       []tls.Certificate{clientCert},
		})
		var wg sync.WaitGroup
		wg.Add(1)
		go func() {
			defer wg.Done()
			_ = server.Handshake()
		}()
		clientErr := client.Handshake()
		wg.Wait()
		peerCount := len(server.ConnectionState().PeerCertificates)
		issuer := "<none>"
		if peerCount > 0 {
			issuer = server.ConnectionState().PeerCertificates[0].Issuer.String()
		}
		fmt.Printf("mode=%s client_handshake=%v peer_certificates=%d issuer=%q\n",
			mode, clientErr, peerCount, issuer)
		server.Close()
		client.Close()
	}
}
GO
CGO_ENABLED=0 go run /tmp/request-client-cert-probe.go
rm -f /tmp/request-client-cert-probe.go

Repository: cozystack/cozystack

Length of output: 2295


🏁 Script executed:

#!/bin/bash
set -eu
cat > /tmp/request-client-cert-probe.go <<'GO'
package main

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"sync"
	"time"
)

func cert(name string) (tls.Certificate, error) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return tls.Certificate{}, err
	}
	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: name},
		NotBefore:    time.Now().Add(-time.Minute),
		NotAfter:     time.Now().Add(time.Hour),
		KeyUsage:     x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
	}
	der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
	if err != nil {
		return tls.Certificate{}, err
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyDER, err := x509.MarshalPKCS8PrivateKey(key)
	if err != nil {
		return tls.Certificate{}, err
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
	return tls.X509KeyPair(certPEM, keyPEM)
}

func main() {
	serverCert, err := cert("server")
	if err != nil {
		panic(err)
	}
	for i := 0; i < 3; i++ {
		issuerName := fmt.Sprintf("untrusted-issuer-%d", i)
		clientCert, err := cert(issuerName)
		if err != nil {
			panic(err)
		}
		serverConn, clientConn := net.Pipe()
		server := tls.Server(serverConn, &tls.Config{
			Certificates: []tls.Certificate{serverCert},
			ClientAuth:   tls.RequestClientCert,
		})
		client := tls.Client(clientConn, &tls.Config{
			InsecureSkipVerify: true,
			Certificates:       []tls.Certificate{clientCert},
		})
		var wg sync.WaitGroup
		wg.Add(1)
		go func() {
			defer wg.Done()
			if err := server.Handshake(); err != nil {
				panic(err)
			}
		}()
		if err := client.Handshake(); err != nil {
			panic(err)
		}
		_ = client.Close()
		wg.Wait()
		state := server.ConnectionState()
		fmt.Printf("issuer=%q peer_certificates=%d\n",
			state.PeerCertificates[0].Issuer.String(), len(state.PeerCertificates))
		_ = server.Close()
	}
}
GO
CGO_ENABLED=0 go run /tmp/request-client-cert-probe.go
rm -f /tmp/request-client-cert-probe.go

Repository: cozystack/cozystack

Length of output: 310


Bound seenClientIssuers.

seenClientIssuers retains every distinct issuer for the process lifetime. With tls.RequestClientCert, callers can submit distinct unverified issuers, causing unbounded memory growth and one log line per issuer. Use a bounded cache or rate-limited logging without retaining every issuer.

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

In `@packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go` at 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.

@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
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 security/confirmed Confirmed vulnerability security/fixed Fix released security Security-related issues and features size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants