apiserver: accept large identity header sets with Go 1.27

Go 1.27 rejects requests with more than 500 header values by default
(http.Server.MaxHeaderValueCount, golang/go#79936). Impersonation and
request-header authentication send one header line per group, so a
user with about 500 groups receives HTTP 431 before authentication
runs.

Set MaxHeaderValueCount to 16384 for secure and deprecated insecure
serving: 1 MiB MaxHeaderBytes at about 64 bytes per value (~16 for the
header name and ~48 for the value). This stays bounded while allowing
identities with thousands of groups.

The field is new in Go 1.27, so it is set through setMaxHeaderValueCount,
with a go1.27 build-tagged implementation and a no-op for older Go, so
the change backports to release branches built with Go 1.26.
This commit is contained in:
Chris Wedgwood committed 2026-10-06 20:35:26 -07:00
1 parent b5e5c04f3e
commit d1603e52ae
5 files changed
+175

No files matched your search

@@ -50,6 +50,8 @@ func (s *DeprecatedInsecureServingInfo) Serve(handler http.Handler, shutdownTime
ReadHeaderTimeout: 32 * time.Second, // just shy of requestTimeoutUpperBound
}
setMaxHeaderValueCount(insecureServer)
if len(s.Name) > 0 {
klog.Infof("Serving %s insecurely on %s", s.Name, s.Listener.Addr())
} else {
@@ -0,0 +1,29 @@
//go:build go1.27
/*
Copyright 2026 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import "net/http"
// 1 MiB MaxHeaderBytes / 64 bytes per value (~16 for header and ~48 for value) bounds tiny-header floods
// while allowing thousands of identity groups sent one per header line.
const maxHeaderValueCount = 16384
func setMaxHeaderValueCount(server *http.Server) {
server.MaxHeaderValueCount = maxHeaderValueCount
}
@@ -0,0 +1,24 @@
//go:build !go1.27
/*
Copyright 2026 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import "net/http"
// Go versions before 1.27 do not limit the number of header values.
func setMaxHeaderValueCount(*http.Server) {}
@@ -172,6 +172,8 @@ func (s *SecureServingInfo) Serve(handler http.Handler, shutdownTimeout time.Dur
ReadHeaderTimeout: 32 * time.Second, // just shy of requestTimeoutUpperBound
}
setMaxHeaderValueCount(secureServer)
if !s.DisableHTTP2 {
// At least 99% of serialized resources in surveyed clusters were smaller than 256kb.
// This should be big enough to accommodate most API POST requests in a single frame,
@@ -0,0 +1,118 @@
//go:build go1.27
/*
Copyright 2026 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"strconv"
"testing"
"time"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
certutil "k8s.io/client-go/util/cert"
netutils "k8s.io/utils/net"
)
func TestSecureServingIdentityHeaders(t *testing.T) {
certPEM, keyPEM, err := certutil.GenerateSelfSignedCertKey("localhost", []net.IP{netutils.ParseIPSloppy("127.0.0.1")}, nil)
if err != nil {
t.Fatal(err)
}
cert, err := dynamiccertificates.NewStaticCertKeyContent("serving-cert", certPEM, keyPEM)
if err != nil {
t.Fatal(err)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(certPEM) {
t.Fatal("failed to add serving certificate to trust roots")
}
for _, tc := range []struct {
name string
disableHTTP2 bool
protocol int
}{
{name: "HTTP/1.1", disableHTTP2: true, protocol: 1},
{name: "HTTP/2", protocol: 2},
} {
t.Run(tc.name, func(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
stopCh := make(chan struct{})
serving := &SecureServingInfo{Listener: listener, Cert: cert, DisableHTTP2: tc.disableHTTP2}
stoppedCh, listenerStoppedCh, err := serving.Serve(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Group-Count", strconv.Itoa(len(r.Header.Values("Impersonate-Group"))))
w.WriteHeader(http.StatusOK)
}), 5*time.Second, stopCh)
if err != nil {
_ = listener.Close()
t.Fatal(err)
}
t.Cleanup(func() {
close(stopCh)
<-listenerStoppedCh
<-stoppedCh
})
transport := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: roots},
ForceAttemptHTTP2: !tc.disableHTTP2,
}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
for _, count := range []int{1000, maxHeaderValueCount - 50, maxHeaderValueCount + 1} {
req, err := http.NewRequest(http.MethodGet, "https://"+listener.Addr().String(), nil)
if err != nil {
t.Fatal(err)
}
for i := range count {
req.Header.Add("Impersonate-Group", fmt.Sprintf("group-%d", i))
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
if err := resp.Body.Close(); err != nil {
t.Fatal(err)
}
if resp.ProtoMajor != tc.protocol {
t.Errorf("%d headers: protocol = %s, want HTTP/%d", count, resp.Proto, tc.protocol)
}
want := http.StatusOK
if count > maxHeaderValueCount {
want = http.StatusRequestHeaderFieldsTooLarge
}
if resp.StatusCode != want {
t.Errorf("%d headers: status = %d, want %d", count, resp.StatusCode, want)
}
if want == http.StatusOK {
if got := resp.Header.Get("Group-Count"); got != strconv.Itoa(count) {
t.Errorf("%d headers: handler received %s group header values", count, got)
}
}
}
})
}
}