Skip to content

Commit 020978b

Browse files
author
Ali
committed
config: add allow_incompatible_key_usage TLS option
Let's Encrypt announced they will stop issuing certificates with the TLS Client Authentication Extended Key Usage (EKU) in 2026. Modern Go TLS rejects peer certificates that do not carry the expected EKU, causing failures in components that use the same LE certificate for both client and server roles (e.g. Alertmanager cluster/gossip mTLS). Add AllowIncompatibleKeyUsage to TLSConfig. When true the built-in EKU check is bypassed while the full certificate chain trust, expiry, and (on client connections) hostname verification are preserved. Implementation: setting InsecureSkipVerify=true prevents Go's TLS stack from running its EKU assertion, while a VerifyPeerCertificate callback re-implements the remaining checks with x509.ExtKeyUsageAny so the EKU field is accepted regardless of its value. The new bool serialises as allow_incompatible_key_usage in both YAML and JSON config files. Fixes prometheus/alertmanager#5151 Signed-off-by: Ali <alliasgher123@gmail.com>
1 parent 9a26ab2 commit 020978b

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

config/http_config.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,45 @@ func NewTLSConfigWithContext(ctx context.Context, cfg *TLSConfig, optFuncs ...TL
11921192
}
11931193
}
11941194

1195+
if cfg.AllowIncompatibleKeyUsage && !cfg.InsecureSkipVerify {
1196+
// Go's TLS library always checks Extended Key Usage (EKU) as part of
1197+
// its built-in peer certificate verification. To skip only the EKU
1198+
// check while preserving chain trust, hostname, and expiry validation,
1199+
// we must set InsecureSkipVerify=true (which disables all built-in
1200+
// verification) and then re-implement the verification ourselves via
1201+
// VerifyPeerCertificate — omitting the KeyUsages constraint.
1202+
serverName := tlsConfig.ServerName
1203+
tlsConfig.InsecureSkipVerify = true //nolint:gosec // EKU-only bypass; chain+hostname still verified below.
1204+
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
1205+
if len(rawCerts) == 0 {
1206+
return nil
1207+
}
1208+
certs := make([]*x509.Certificate, len(rawCerts))
1209+
for i, raw := range rawCerts {
1210+
cert, err := x509.ParseCertificate(raw)
1211+
if err != nil {
1212+
return fmt.Errorf("tls: failed to parse peer certificate: %w", err)
1213+
}
1214+
certs[i] = cert
1215+
}
1216+
opts := x509.VerifyOptions{
1217+
// Access tlsConfig.RootCAs at call time so callers who
1218+
// modify RootCAs after NewTLSConfig returns are respected.
1219+
Roots: tlsConfig.RootCAs,
1220+
Intermediates: x509.NewCertPool(),
1221+
DNSName: serverName,
1222+
// ExtKeyUsageAny bypasses EKU checking. An empty slice
1223+
// defaults to ExtKeyUsageServerAuth per the x509 package docs.
1224+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
1225+
}
1226+
for _, cert := range certs[1:] {
1227+
opts.Intermediates.AddCert(cert)
1228+
}
1229+
_, err := certs[0].Verify(opts)
1230+
return err
1231+
}
1232+
}
1233+
11951234
return tlsConfig, nil
11961235
}
11971236

@@ -1222,6 +1261,16 @@ type TLSConfig struct {
12221261
ServerName string `yaml:"server_name,omitempty" json:"server_name,omitempty"`
12231262
// Disable target certificate validation.
12241263
InsecureSkipVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify"`
1264+
// AllowIncompatibleKeyUsage disables the Extended Key Usage (EKU) check on
1265+
// peer certificates while still verifying the certificate chain, expiry,
1266+
// and (for client connections) the server hostname.
1267+
//
1268+
// This is useful when connecting to services that use a certificate without
1269+
// the expected EKU — for example Let's Encrypt certificates after they
1270+
// dropped TLS Client Authentication support in 2026. The full certificate
1271+
// chain trust and hostname verification still apply; only the EKU assertion
1272+
// is skipped.
1273+
AllowIncompatibleKeyUsage bool `yaml:"allow_incompatible_key_usage,omitempty" json:"allow_incompatible_key_usage,omitempty"`
12251274
// Minimum TLS version.
12261275
MinVersion TLSVersion `yaml:"min_version,omitempty" json:"min_version,omitempty"`
12271276
// Maximum TLS version.

config/http_config_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ package config
1515

1616
import (
1717
"context"
18+
"crypto/ecdsa"
19+
"crypto/elliptic"
20+
"crypto/rand"
1821
"crypto/tls"
1922
"crypto/x509"
23+
"crypto/x509/pkix"
2024
"encoding/base64"
2125
"encoding/json"
2226
"errors"
2327
"fmt"
2428
"io"
29+
"math/big"
2530
"net"
2631
"net/http"
2732
"net/http/httptest"
@@ -2321,3 +2326,95 @@ func TestMultipleHeaders(t *testing.T) {
23212326
_, err = client.Get(ts.URL)
23222327
require.NoErrorf(t, err, "can't fetch URL: %v", err)
23232328
}
2329+
2330+
// TestTLSConfigAllowIncompatibleKeyUsage verifies that when
2331+
// AllowIncompatibleKeyUsage is set, a TLS connection to a server whose
2332+
// certificate lacks the expected Extended Key Usage (e.g. a Let's Encrypt
2333+
// certificate used for mutual TLS after LE dropped clientAuth EKU support)
2334+
// succeeds where it would otherwise fail.
2335+
func TestTLSConfigAllowIncompatibleKeyUsage(t *testing.T) {
2336+
// Generate a self-signed CA + server cert that has ONLY serverAuth EKU
2337+
// (no clientAuth). This simulates a Let's Encrypt-style cert.
2338+
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
2339+
require.NoError(t, err)
2340+
caTemplate := &x509.Certificate{
2341+
SerialNumber: big.NewInt(1),
2342+
Subject: pkix.Name{CommonName: "test-ca"},
2343+
NotBefore: time.Now().Add(-time.Hour),
2344+
NotAfter: time.Now().Add(time.Hour),
2345+
IsCA: true,
2346+
KeyUsage: x509.KeyUsageCertSign,
2347+
BasicConstraintsValid: true,
2348+
}
2349+
caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
2350+
require.NoError(t, err)
2351+
caCert, err := x509.ParseCertificate(caDER)
2352+
require.NoError(t, err)
2353+
2354+
srvKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
2355+
require.NoError(t, err)
2356+
srvTemplate := &x509.Certificate{
2357+
SerialNumber: big.NewInt(2),
2358+
Subject: pkix.Name{CommonName: "127.0.0.1"},
2359+
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
2360+
NotBefore: time.Now().Add(-time.Hour),
2361+
NotAfter: time.Now().Add(time.Hour),
2362+
// clientAuth only — no serverAuth. This triggers the EKU mismatch a
2363+
// TLS client sees when the peer cert lacks ExtKeyUsageServerAuth, which
2364+
// is the scenario for LE certs used in mutual-TLS gossip rings.
2365+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
2366+
}
2367+
srvDER, err := x509.CreateCertificate(rand.Reader, srvTemplate, caCert, &srvKey.PublicKey, caKey)
2368+
require.NoError(t, err)
2369+
2370+
srvTLSCert := tls.Certificate{
2371+
Certificate: [][]byte{srvDER},
2372+
PrivateKey: srvKey,
2373+
}
2374+
caPool := x509.NewCertPool()
2375+
caPool.AddCert(caCert)
2376+
2377+
// Start a TLS test server using the serverAuth-only certificate.
2378+
serverTLSCfg := &tls.Config{Certificates: []tls.Certificate{srvTLSCert}}
2379+
listener, err := tls.Listen("tcp", "127.0.0.1:0", serverTLSCfg)
2380+
require.NoError(t, err)
2381+
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2382+
w.WriteHeader(http.StatusOK)
2383+
})}
2384+
go srv.Serve(listener) //nolint:errcheck
2385+
defer srv.Close()
2386+
addr := "https://" + listener.Addr().String()
2387+
2388+
t.Run("without AllowIncompatibleKeyUsage fails on EKU mismatch", func(t *testing.T) {
2389+
cfg := HTTPClientConfig{
2390+
TLSConfig: TLSConfig{
2391+
// CA is trusted but EKU check will reject the server cert.
2392+
InsecureSkipVerify: false,
2393+
},
2394+
}
2395+
// Use the generated CA so hostname+chain pass; only EKU should fail.
2396+
tlsCfg, err := NewTLSConfig(&cfg.TLSConfig)
2397+
require.NoError(t, err)
2398+
tlsCfg.RootCAs = caPool
2399+
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsCfg}}
2400+
_, err = client.Get(addr)
2401+
require.Error(t, err, "expected EKU error without AllowIncompatibleKeyUsage")
2402+
require.Contains(t, err.Error(), "incompatible key usage")
2403+
})
2404+
2405+
t.Run("with AllowIncompatibleKeyUsage succeeds", func(t *testing.T) {
2406+
cfg := TLSConfig{
2407+
AllowIncompatibleKeyUsage: true,
2408+
// ServerName override for IP-addressed connection.
2409+
}
2410+
tlsCfg, err := NewTLSConfig(&cfg)
2411+
require.NoError(t, err)
2412+
require.True(t, tlsCfg.InsecureSkipVerify, "InsecureSkipVerify should be true internally")
2413+
require.NotNil(t, tlsCfg.VerifyPeerCertificate, "VerifyPeerCertificate should be set")
2414+
tlsCfg.RootCAs = caPool
2415+
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsCfg}}
2416+
resp, err := client.Get(addr)
2417+
require.NoError(t, err, "connection should succeed when AllowIncompatibleKeyUsage=true")
2418+
resp.Body.Close()
2419+
})
2420+
}

0 commit comments

Comments
 (0)