feat(kubeovn-webhook): add client-certificate verification, off by default - #2698
feat(kubeovn-webhook): add client-certificate verification, off by default#2698Andrei Kvapil (kvaps) wants to merge 3 commits into
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses an unauthenticated information disclosure vulnerability by enforcing stricter network access controls and providing an optional mechanism for mutual TLS authentication. These changes significantly reduce the attack surface of the webhook by limiting network reachability and requiring verified client identities. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
📝 WalkthroughWalkthroughThe webhook now 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. ChangesWebhook mTLS Configuration
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
e1045bf to
b6ba7cf
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces mTLS support to the kubeovn-webhook by adding a --client-ca-file flag and configuring the TLS server to require and verify client certificates. It also adds a new NetworkPolicy to restrict ingress traffic to the webhook from the kube-system namespace. Feedback identifies that the new flag is not yet integrated into the Helm deployment templates, suggests enforcing a minimum TLS version of 1.2 for improved security, and recommends quoting string values in the Helm templates to comply with the style guide.
|
|
||
| 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.") |
There was a problem hiding this comment.
| tlsConfig := &tls.Config{ | ||
| Certificates: []tls.Certificate{tlsCert}, | ||
| } |
There was a problem hiding this comment.
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.
| tlsConfig := &tls.Config{ | |
| Certificates: []tls.Certificate{tlsCert}, | |
| } | |
| tlsConfig := &tls.Config{ | |
| Certificates: []tls.Certificate{tlsCert}, | |
| MinVersion: tls.VersionTLS12, | |
| } |
| name: {{ include "namespace-annotation-webhook.fullname" . }} | ||
| labels: | ||
| app.kubernetes.io/name: {{ include "namespace-annotation-webhook.name" . }} | ||
| app.kubernetes.io/instance: {{ .Release.Name }} |
There was a problem hiding this comment.
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
- Helm template correctness: missing
quote(link)
IvanHunters
left a comment
There was a problem hiding this comment.
Both hardening mechanisms in this PR are inert in a default Cozystack cluster, so the disclosure vector stays open:
- NetworkPolicy is a no-op. Cozystack sets
kube-ovn.func.ENABLE_NP: false(packages/system/kubeovn/values.yaml), so kube-ovn-controller runs with--enable-np=falseand programs no ACLs. The addedtemplates/networkpolicy.yamlis never enforced. - mTLS is dead code.
--client-ca-filedefaults to""and is never passed intemplates/deployment.yaml; no apiserver-side client-cert config exists, so theRequireAndVerifyClientCertbranch never runs.
Only MinVersion: tls.VersionTLS12 takes effect.
To make mTLS real it must be wired end-to-end (mount CA, pass the flag, configure the apiserver admission webhook to present a matching client cert) behind a values toggle, and not enabled half-way: failurePolicy: Fail + broken TLS would block all non-system pod creation.
For the NetworkPolicy: it only matters if NP enforcement is on, and namespaceSelector: kube-system won't match apiserver traffic (host-network, not a pod) — gate via CNI host/ipBlock on control-plane CIDRs instead. The NP is also unconditional (no toggle), so it becomes silently load-bearing if ENABLE_NP is ever flipped.
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>
5d61c37 to
04606e2
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Rebased onto current NetworkPolicy: dropped, not fixed. Client-cert verification: now reachable, but off by default. It stays off by default deliberately. This webhook is registered Setting Also rebased past the certificate-reloader work that landed since May, so One thing this PR does not do: it does not close the disclosure, it makes the fix deployable and tells us whether we can turn it on. Worth being explicit about that since the advisory is still open. Separately, and not for this PR: the advisory text overstates the impact. It says arbitrary namespace annotations are exfiltrated, but the handler only ever copies two — |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go`:
- Line 42: Update the seenClientIssuers handling to avoid retaining every
distinct issuer for the process lifetime. Replace the unbounded
LoadOrStore-based tracking with a bounded cache or rate-limited logging, while
preserving the intended duplicate-suppression behavior and preventing one log
entry per unverified issuer.
- Around line 39-46: Stop using unverified PeerCertificates metadata in the
observation path to identify the API server or select client-ca-file. In
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go:39-46, remove the
issuer-based seenClientIssuers trust decision and related identity confirmation;
in packages/system/kubeovn-webhook/images/kubeovn-webhook/main.go:78-81, ensure
enforcement is controlled only by trusted control-plane configuration; and in
packages/system/kubeovn-webhook/values.yaml:12-22, configure --client-ca-file
from that trusted setting rather than observation logs. Preserve observation
logging only as explicitly untrusted diagnostic information.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4a005c97-0868-459e-a9d6-0017924f9801
📒 Files selected for processing (3)
packages/system/kubeovn-webhook/images/kubeovn-webhook/main.gopackages/system/kubeovn-webhook/templates/deployment.yamlpackages/system/kubeovn-webhook/values.yaml
| 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) |
There was a problem hiding this comment.
🔒 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-webhookRepository: 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-webhookRepository: 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/systemRepository: 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-L81packages/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 { |
There was a problem hiding this comment.
🩺 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 --statRepository: 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.goRepository: 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.goRepository: 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.goRepository: 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.
What this PR does
Adds client-certificate verification to the kube-ovn webhook, and turns it on only when an operator supplies a CA bundle.
--client-ca-fileis new and empty by default. With it empty the server setstls.RequestClientCert: it asks callers for a certificate, records the issuer of whatever arrives, and serves the request either way. Set the flag and the server switches totls.RequireAndVerifyClientCert, so a caller without a certificate signed by that CA is refused at the TLS handshake.This is step one of two, and on its own it does not close GHSA-g883-q79m-8225. With the default empty value nothing is rejected — the webhook still answers an unauthenticated caller, and the only change is that the caller's issuer now appears in the logs. That is deliberate: switching straight to
RequireAndVerifyClientCertwould break every cluster whose API server sends no client certificate, and there is no way to know from here which those are. The logs are how we find out.Step two, in a follow-up: read a day of issuer logs on a Talos cluster and on
isp-full-generic, confirm the API server presents a certificate we can pin, then set the CA bundle so verification becomes mandatory. The advisory should be published as fixed only after that lands — not with this PR.An earlier revision also carried a NetworkPolicy restricting ingress to
kube-system. It was dropped, and the title has been corrected to match; only the TLS path is left here.Screenshots
N/A — no user-facing or UI change.
Downstream repositories
Walked the trigger map against the diff: no row matches. The change touches one system package and the Go image it builds — no app package, no
values.schema.json, noApplicationDefinition, no platform variant, no node prerequisite.Release note