Skip to content

web: fix panic when client_allowed_sans is used with an optional client certificate - #436

Open
mrueg wants to merge 1 commit into
prometheus:masterfrom
mrueg:fix/verifypeercertificate-empty-rawcerts
Open

mrueg wants to merge 1 commit into
prometheus:masterfrom
mrueg:fix/verifypeercertificate-empty-rawcerts

Conversation

@mrueg

@mrueg mrueg commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

TLSConfig.VerifyPeerCertificate indexes rawCerts[0] without checking the length:

func (t *TLSConfig) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
	// sender cert comes first, see https://www.rfc-editor.org/rfc/rfc5246#section-7.4.2
	cert, err := x509.ParseCertificate(rawCerts[0])

crypto/tls invokes VerifyPeerCertificate even when the client sent no certificate, as long as the configured client auth type does not require one. In processCertsFromClient the "client didn't provide a certificate" bail-out is guarded by requiresClientCert(c.config.ClientAuth), and control still reaches the unconditional callback at the end of the function with an empty slice.

So this configuration panics for every client that connects without a certificate:

tls_server_config:
  cert_file: "server.crt"
  key_file: "server.key"
  client_auth_type: "VerifyClientCertIfGiven"   # or RequestClientCert
  client_ca_file: "client_ca.pem"
  client_allowed_sans:
    - "one"

net/http's per-connection recover catches it, so the process survives, but the connection is aborted with a stack trace in the log rather than a clean bad certificate alert.

Changes

  1. Return an error instead of panicking when rawCerts is empty.
  2. Reject the combination at configuration load time. client_allowed_sans is only meaningful when the client is required to present a certificate — and with NoClientCert the callback is never invoked at all, so the configured SANs were silently ignored. That silent-bypass case seems worth failing loudly on, but it is the one behavioural change here, so happy to drop it to just (1) if you'd rather not reject configs that start today.

Docs updated to state the constraint.

Testing

Every existing client_allowed_sans fixture uses RequireAndVerifyClientCert, which is why this went unnoticed. Added a unit test for the nil/empty slice, a real TLS handshake test where the client presents no certificate, and two config fixtures for the rejected combinations. Reverting the guard makes the handshake test fail with index out of range [0] with length 0.

@mrueg
mrueg force-pushed the fix/verifypeercertificate-empty-rawcerts branch from 2672aae to 0089e78 Compare September 2, 2026 11:31
Comment thread web/san_test.go Outdated
@@ -0,0 +1,122 @@
// Copyright 2025 The Prometheus Authors

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Any reason to not add these tests to tls_config_test.go?

@SuperQ SuperQ left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please cleanup the LLM slop comments.

Comment thread web/tls_config.go Outdated
Comment on lines +114 to +116
// crypto/tls calls VerifyPeerCertificate with an empty slice when the
// client declines to send a certificate and the client auth type does not
// require one. Without this check the rawCerts[0] access below panics.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overly verbose comment.

Suggested change
// crypto/tls calls VerifyPeerCertificate with an empty slice when the
// client declines to send a certificate and the client auth type does not
// require one. Without this check the rawCerts[0] access below panics.
// crypto/tls calls VerifyPeerCertificate with an empty slice when the
// client declines to send a certificate and the client auth type does not
// require one.

Comment thread web/tls_config.go Outdated
Comment on lines +302 to +307
// SAN matching is only meaningful when the client is required to
// present a certificate. With NoClientCert the server never asks for
// one, so VerifyPeerCertificate is never called and the configured
// SANs would be silently ignored. With RequestClientCert or
// VerifyClientCertIfGiven a client may decline to send a certificate,
// leaving nothing to match against.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overly verbose comment.

Suggested change
// SAN matching is only meaningful when the client is required to
// present a certificate. With NoClientCert the server never asks for
// one, so VerifyPeerCertificate is never called and the configured
// SANs would be silently ignored. With RequestClientCert or
// VerifyClientCertIfGiven a client may decline to send a certificate,
// leaving nothing to match against.

@mrueg
mrueg force-pushed the fix/verifypeercertificate-empty-rawcerts branch from 0089e78 to 14d54d9 Compare November 10, 2026 09:22
…nt cert

VerifyPeerCertificate indexed rawCerts[0] without checking the length.
crypto/tls calls VerifyPeerCertificate even when the client sent no
certificate, as long as the client auth type does not require one, so
combining client_allowed_sans with RequestClientCert or
VerifyClientCertIfGiven panicked on every certificate-less client. The
panic is recovered by net/http's per-connection handler, so the process
survives, but the connection is aborted with a stack trace in the log
instead of a clean TLS alert.

Return an error for an empty rawCerts slice, and reject the combination at
configuration load time: client_allowed_sans is only meaningful when the
client is required to present a certificate. With NoClientCert the callback
is never invoked at all, so the configured SANs were silently ignored,
which is the more dangerous half of this.

All existing client_allowed_sans test fixtures use RequireAndVerifyClientCert,
which is why this went unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
@mrueg
mrueg force-pushed the fix/verifypeercertificate-empty-rawcerts branch from 14d54d9 to 09fc8da Compare November 10, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants