mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-10-28 15:05:03 +08:00
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:
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user