From 8b5463e18b6e5e775825406cf67f6ca3726394c3 Mon Sep 17 00:00:00 2001 From: Ameer Ghani Date: Tue, 31 Mar 2026 18:57:30 +0000 Subject: [PATCH 1/3] grpc: Advertise h2 support in ALPN A compliant HTTP/2 server advertises support for h2 in the TLS ALPN field. See https://datatracker.ietf.org/doc/html/rfc7540#section-3.1. grpc-go did not always check for the presence and validity of this extension. It did so in a relatively recent version. This didn't affect boulder, because neither a boulder client nor boulder was advertising the ALPN. However, now that we're trying to update Consul, we run into a problem with failing healthchecks. Consul now uses an upgraded grpc-go with ALPN enforcement. When Consul makes a gRPC healthcheck against boulder, it rejects the healthcheck due to the missing ALPN. Fix this problem by appropriately advertising the h2 ALPN. Normally, grpc-go would handle this for us, but because we're handling a raw tls.Config struct, we have to do it ourselves. --- grpc/creds/creds.go | 1 + grpc/creds/creds_test.go | 4 ++-- grpc/server.go | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/grpc/creds/creds.go b/grpc/creds/creds.go index 31da6e234ba..a94ba909f42 100644 --- a/grpc/creds/creds.go +++ b/grpc/creds/creds.go @@ -69,6 +69,7 @@ func (tc *clientTransportCredentials) ClientHandshake(ctx context.Context, addr ServerName: host, RootCAs: tc.roots, Certificates: tc.clients, + NextProtos: []string{"h2"}, }) err = conn.HandshakeContext(ctx) if err != nil { diff --git a/grpc/creds/creds_test.go b/grpc/creds/creds_test.go index 0cbf92b6152..f8b83f1a344 100644 --- a/grpc/creds/creds_test.go +++ b/grpc/creds/creds_test.go @@ -106,9 +106,9 @@ func TestClientTransportCredentials(t *testing.T) { roots.AddCert(certB) serverA := httptest.NewUnstartedServer(nil) - serverA.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derA}, PrivateKey: priv}}} + serverA.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derA}, PrivateKey: priv}}, NextProtos: []string{"h2"}} serverB := httptest.NewUnstartedServer(nil) - serverB.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derB}, PrivateKey: priv}}} + serverB.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derB}, PrivateKey: priv}}, NextProtos: []string{"h2"}} tc := NewClientCredentials(roots, []tls.Certificate{}, "") diff --git a/grpc/server.go b/grpc/server.go index b17ce0d366d..834b38bb680 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -126,6 +126,10 @@ func (sb *serverBuilder) Build(tlsConfig *tls.Config, statsRegistry prometheus.R return nil, errNilTLS } + // Advertise support for h2 in the ALPN, required by HTTP/2. grpc-go can't do + // this for us, since we're handling a raw tls.Config. + tlsConfig.NextProtos = []string{"h2"} + // Collect all names which should be allowed to connect to the server at all. // This is the names which are allowlisted at the server level, plus the union // of all names which are allowlisted for any individual service. From 1b20c196bd549cafeda3ffa798687e328ede64ee Mon Sep 17 00:00:00 2001 From: Ameer Ghani Date: Tue, 31 Mar 2026 19:38:41 +0000 Subject: [PATCH 2/3] PR feedback --- cmd/config.go | 2 ++ grpc/server.go | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index e006a12adca..364c74d1e13 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -218,6 +218,8 @@ func (t *TLSConfig) Load(scope prometheus.Registerer) (*tls.Config, error) { Certificates: []tls.Certificate{cert}, // Set the only acceptable TLS to v1.3. MinVersion: tls.VersionTLS13, + // HTTP/2 requires us to advertise h2 in the TLS ALPN. + NextProtos: []string{"h2"}, }, nil } diff --git a/grpc/server.go b/grpc/server.go index 834b38bb680..b17ce0d366d 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -126,10 +126,6 @@ func (sb *serverBuilder) Build(tlsConfig *tls.Config, statsRegistry prometheus.R return nil, errNilTLS } - // Advertise support for h2 in the ALPN, required by HTTP/2. grpc-go can't do - // this for us, since we're handling a raw tls.Config. - tlsConfig.NextProtos = []string{"h2"} - // Collect all names which should be allowed to connect to the server at all. // This is the names which are allowlisted at the server level, plus the union // of all names which are allowlisted for any individual service. From f1f3046d3a80bf151b94fc359fb9b70a6f9744c3 Mon Sep 17 00:00:00 2001 From: Ameer Ghani Date: Tue, 31 Mar 2026 21:05:30 +0000 Subject: [PATCH 3/3] Back out client change --- grpc/creds/creds.go | 1 - grpc/creds/creds_test.go | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/grpc/creds/creds.go b/grpc/creds/creds.go index a94ba909f42..31da6e234ba 100644 --- a/grpc/creds/creds.go +++ b/grpc/creds/creds.go @@ -69,7 +69,6 @@ func (tc *clientTransportCredentials) ClientHandshake(ctx context.Context, addr ServerName: host, RootCAs: tc.roots, Certificates: tc.clients, - NextProtos: []string{"h2"}, }) err = conn.HandshakeContext(ctx) if err != nil { diff --git a/grpc/creds/creds_test.go b/grpc/creds/creds_test.go index f8b83f1a344..0cbf92b6152 100644 --- a/grpc/creds/creds_test.go +++ b/grpc/creds/creds_test.go @@ -106,9 +106,9 @@ func TestClientTransportCredentials(t *testing.T) { roots.AddCert(certB) serverA := httptest.NewUnstartedServer(nil) - serverA.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derA}, PrivateKey: priv}}, NextProtos: []string{"h2"}} + serverA.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derA}, PrivateKey: priv}}} serverB := httptest.NewUnstartedServer(nil) - serverB.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derB}, PrivateKey: priv}}, NextProtos: []string{"h2"}} + serverB.TLS = &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{derB}, PrivateKey: priv}}} tc := NewClientCredentials(roots, []tls.Certificate{}, "")