diff --git a/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go b/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go index 12e6b250e04..3c86b026f59 100644 --- a/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go +++ b/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go @@ -35,7 +35,9 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/request/headerrequest" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/dynamiccertificates" + utilfeature "k8s.io/apiserver/pkg/util/feature" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" @@ -262,10 +264,13 @@ func getConfigMapDataFor(authenticationInfo ClusterAuthenticationInfo) (map[stri if err != nil { return nil, err } - data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value()) - if err != nil { - return nil, err + if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) && len(authenticationInfo.RequestHeaderUIDHeaders.Value()) > 0 { + data["requestheader-uid-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUIDHeaders.Value()) + if err != nil { + return nil, err + } } + data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value()) if err != nil { return nil, err @@ -305,9 +310,12 @@ func getClusterAuthenticationInfoFor(data map[string]string) (ClusterAuthenticat if err != nil { return ClusterAuthenticationInfo{}, err } - ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"]) - if err != nil { - return ClusterAuthenticationInfo{}, err + + if utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) { + ret.RequestHeaderUIDHeaders, err = jsonDeserializeStringSlice(data["requestheader-uid-headers"]) + if err != nil { + return ClusterAuthenticationInfo{}, err + } } if caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 { diff --git a/pkg/features/versioned_kube_features.go b/pkg/features/versioned_kube_features.go index 9b8bbc6958f..ecee44abf7b 100644 --- a/pkg/features/versioned_kube_features.go +++ b/pkg/features/versioned_kube_features.go @@ -306,6 +306,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, }, + genericfeatures.RemoteRequestHeaderUID: { + {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, + }, + genericfeatures.ResilientWatchCacheInitialization: { {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, }, diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index d0b064dc2c9..c23343346e4 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -149,6 +149,13 @@ const ( // to a chunking list request. RemainingItemCount featuregate.Feature = "RemainingItemCount" + // owner: @stlaz + // + // Enable kube-apiserver to accept UIDs via request header authentication. + // This will also make the kube-apiserver's API aggregator add UIDs via standard + // headers when forwarding requests to the servers serving the aggregated API. + RemoteRequestHeaderUID featuregate.Feature = "RemoteRequestHeaderUID" + // owner: @wojtek-t // // Enables resilient watchcache initialization to avoid controlplane @@ -359,6 +366,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, }, + RemoteRequestHeaderUID: { + {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, + }, + ResilientWatchCacheInitialization: { {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, }, diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go b/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go index 6b53908355c..f88f73e72b1 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go @@ -29,8 +29,10 @@ import ( "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/authentication/authenticatorfactory" "k8s.io/apiserver/pkg/authentication/request/headerrequest" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/dynamiccertificates" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -68,9 +70,6 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error { if err := checkForWhiteSpaceOnly("requestheader-username-headers", s.UsernameHeaders...); err != nil { allErrors = append(allErrors, err) } - if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil { - allErrors = append(allErrors, err) - } if err := checkForWhiteSpaceOnly("requestheader-group-headers", s.GroupHeaders...); err != nil { allErrors = append(allErrors, err) } @@ -84,10 +83,6 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error { if len(s.UsernameHeaders) > 0 && !caseInsensitiveHas(s.UsernameHeaders, "X-Remote-User") { klog.Warningf("--requestheader-username-headers is set without specifying the standard X-Remote-User header - API aggregation will not work") } - if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") { - // this was added later and so we are able to error out - allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work")) - } if len(s.GroupHeaders) > 0 && !caseInsensitiveHas(s.GroupHeaders, "X-Remote-Group") { klog.Warningf("--requestheader-group-headers is set without specifying the standard X-Remote-Group header - API aggregation will not work") } @@ -95,6 +90,20 @@ func (s *RequestHeaderAuthenticationOptions) Validate() []error { klog.Warningf("--requestheader-extra-headers-prefix is set without specifying the standard X-Remote-Extra- header prefix - API aggregation will not work") } + if !utilfeature.DefaultFeatureGate.Enabled(features.RemoteRequestHeaderUID) { + if len(s.UIDHeaders) > 0 { + allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers requires the %q feature to be enabled", features.RemoteRequestHeaderUID)) + } + } else { + if err := checkForWhiteSpaceOnly("requestheader-uid-headers", s.UIDHeaders...); err != nil { + allErrors = append(allErrors, err) + } + if len(s.UIDHeaders) > 0 && !caseInsensitiveHas(s.UIDHeaders, "X-Remote-Uid") { + // this was added later and so we are able to error out + allErrors = append(allErrors, fmt.Errorf("--requestheader-uid-headers is set without specifying the standard X-Remote-Uid header - API aggregation will not work")) + } + } + return allErrors } @@ -126,7 +135,7 @@ func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { "List of request headers to inspect for usernames. X-Remote-User is common.") fs.StringSliceVar(&s.UIDHeaders, "requestheader-uid-headers", s.UIDHeaders, ""+ - "List of request headers to inspect for UIDs. X-Remote-Uid is suggested.") + "List of request headers to inspect for UIDs. X-Remote-Uid is suggested. Requires the RemoteRequestHeaderUID feature to be enabled.") fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+ "List of request headers to inspect for groups. X-Remote-Group is suggested.") diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go index 5292ec86489..d95a271af76 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go @@ -159,7 +159,12 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { proxyRoundTripper := handlingInfo.proxyRoundTripper upgrade := httpstream.IsUpgradeRequest(req) - proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra(), proxyRoundTripper) + var userUID string + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.RemoteRequestHeaderUID) { + userUID = user.GetUID() + } + + proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), userUID, user.GetGroups(), user.GetExtra(), proxyRoundTripper) if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerTracing) && !upgrade { tracingWrapper := tracing.WrapperFor(r.tracerProvider) @@ -170,7 +175,7 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // NOT use the proxyRoundTripper. It's a direct dial that bypasses the proxyRoundTripper. This means that we have to // attach the "correct" user headers to the request ahead of time. if upgrade { - transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra()) + transport.SetAuthProxyHeaders(newReq, user.GetName(), userUID, user.GetGroups(), user.GetExtra()) } handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w}) diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy_test.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy_test.go index 632727018a5..ecc85c7e198 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy_test.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy_test.go @@ -34,7 +34,9 @@ import ( "sync/atomic" "testing" + "github.com/google/go-cmp/cmp" "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/client-go/transport" @@ -53,8 +55,11 @@ import ( "k8s.io/apiserver/pkg/endpoints/filters" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/server/egressselector" + utilfeature "k8s.io/apiserver/pkg/util/feature" utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol" apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy" + "k8s.io/component-base/featuregate" + featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" @@ -130,6 +135,8 @@ func TestProxyHandler(t *testing.T) { expectedBody string expectedCalled bool expectedHeaders map[string][]string + + enableFeatureGates []featuregate.Feature }{ "no target": { expectedStatusCode: http.StatusNotFound, @@ -174,6 +181,40 @@ func TestProxyHandler(t *testing.T) { }, expectedStatusCode: http.StatusOK, expectedCalled: true, + expectedHeaders: map[string][]string{ + "X-Forwarded-Proto": {"https"}, + "X-Forwarded-Uri": {"/request/path"}, + "X-Forwarded-For": {"127.0.0.1"}, + "X-Remote-User": {"username"}, + "User-Agent": {"Go-http-client/1.1"}, + "Accept-Encoding": {"gzip"}, + "X-Remote-Group": {"one", "two"}, + }, + }, + "[RemoteRequestHeaderUID] proxy with user, insecure": { + user: &user.DefaultInfo{ + Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", + Groups: []string{"one", "two"}, + }, + path: "/request/path", + apiService: &apiregistration.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, + Spec: apiregistration.APIServiceSpec{ + Service: &apiregistration.ServiceReference{Port: pointer.Int32Ptr(443)}, + Group: "foo", + Version: "v1", + InsecureSkipTLSVerify: true, + }, + Status: apiregistration.APIServiceStatus{ + Conditions: []apiregistration.APIServiceCondition{ + {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, + }, + }, + }, + enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID}, + expectedStatusCode: http.StatusOK, + expectedCalled: true, expectedHeaders: map[string][]string{ "X-Forwarded-Proto": {"https"}, "X-Forwarded-Uri": {"/request/path"}, @@ -208,6 +249,40 @@ func TestProxyHandler(t *testing.T) { }, expectedStatusCode: http.StatusOK, expectedCalled: true, + expectedHeaders: map[string][]string{ + "X-Forwarded-Proto": {"https"}, + "X-Forwarded-Uri": {"/request/path"}, + "X-Forwarded-For": {"127.0.0.1"}, + "X-Remote-User": {"username"}, + "User-Agent": {"Go-http-client/1.1"}, + "Accept-Encoding": {"gzip"}, + "X-Remote-Group": {"one", "two"}, + }, + }, + "[RemoteRequestHeaderUID] proxy with user, cabundle": { + user: &user.DefaultInfo{ + Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", + Groups: []string{"one", "two"}, + }, + path: "/request/path", + apiService: &apiregistration.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "v1.foo"}, + Spec: apiregistration.APIServiceSpec{ + Service: &apiregistration.ServiceReference{Name: "test-service", Namespace: "test-ns", Port: pointer.Int32Ptr(443)}, + Group: "foo", + Version: "v1", + CABundle: testCACrt, + }, + Status: apiregistration.APIServiceStatus{ + Conditions: []apiregistration.APIServiceCondition{ + {Type: apiregistration.Available, Status: apiregistration.ConditionTrue}, + }, + }, + }, + enableFeatureGates: []featuregate.Feature{features.RemoteRequestHeaderUID}, + expectedStatusCode: http.StatusOK, + expectedCalled: true, expectedHeaders: map[string][]string{ "X-Forwarded-Proto": {"https"}, "X-Forwarded-Uri": {"/request/path"}, @@ -320,7 +395,11 @@ func TestProxyHandler(t *testing.T) { target.Reset() legacyregistry.Reset() - func() { + t.Run(name, func(t *testing.T) { + for _, f := range tc.enableFeatureGates { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true) + } + targetServer := httptest.NewUnstartedServer(target) serviceCert := tc.serviceCertOverride if serviceCert == nil { @@ -354,37 +433,37 @@ func TestProxyHandler(t *testing.T) { resp, err := http.Get(server.URL + tc.path) if err != nil { - t.Errorf("%s: %v", name, err) + t.Errorf("%v", err) return } if e, a := tc.expectedStatusCode, resp.StatusCode; e != a { body, _ := httputil.DumpResponse(resp, true) - t.Logf("%s: %v", name, string(body)) - t.Errorf("%s: expected %v, got %v", name, e, a) + t.Logf("%v", string(body)) + t.Errorf("expected %v, got %v", e, a) return } bytes, err := io.ReadAll(resp.Body) if err != nil { - t.Errorf("%s: %v", name, err) + t.Errorf("%v", err) return } if !strings.Contains(string(bytes), tc.expectedBody) { - t.Errorf("%s: expected %q, got %q", name, tc.expectedBody, string(bytes)) + t.Errorf("expected %q, got %q", tc.expectedBody, string(bytes)) return } if e, a := tc.expectedCalled, target.called; e != a { - t.Errorf("%s: expected %v, got %v", name, e, a) + t.Errorf("expected %v, got %v", e, a) return } // this varies every test delete(target.headers, "X-Forwarded-Host") if e, a := tc.expectedHeaders, target.headers; !reflect.DeepEqual(e, a) { - t.Errorf("%s: expected %v, got %v", name, e, a) + t.Errorf("expected != got %v", cmp.Diff(e, a)) return } if e, a := targetServer.Listener.Addr().String(), target.host; tc.expectedCalled && !reflect.DeepEqual(e, a) { - t.Errorf("%s: expected %v, got %v", name, e, a) + t.Errorf("expected %v, got %v", e, a) return } @@ -397,7 +476,7 @@ func TestProxyHandler(t *testing.T) { t.Errorf("expected the x509_missing_san_total to be 1, but it's %d", errorCounter) } } - }() + }) } }