diff --git a/cmd/kube-apiserver/app/testing/testserver.go b/cmd/kube-apiserver/app/testing/testserver.go index 3c3b9e55ecd..9b97da28169 100644 --- a/cmd/kube-apiserver/app/testing/testserver.go +++ b/cmd/kube-apiserver/app/testing/testserver.go @@ -214,6 +214,7 @@ func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions, } s.SecureServing.ServerCert.CertDirectory = result.TmpDir + reqHeaderFromFlags := s.Authentication.RequestHeader if instanceOptions.EnableCertAuth { // set up default headers for request header auth reqHeaders := serveroptions.NewDelegatingAuthenticationOptions() @@ -347,6 +348,23 @@ func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions, return result, err } + // the RequestHeader options pointer gets replaced in the case of EnableCertAuth override + // and so flags are connected to a struct that no longer appears in the ServerOptions struct + // we're using. + // We still want to make it possible to configure the headers config for the RequestHeader authenticator. + if usernameHeaders := reqHeaderFromFlags.UsernameHeaders; len(usernameHeaders) > 0 { + s.Authentication.RequestHeader.UsernameHeaders = usernameHeaders + } + if uidHeaders := reqHeaderFromFlags.UIDHeaders; len(uidHeaders) > 0 { + s.Authentication.RequestHeader.UIDHeaders = uidHeaders + } + if groupHeaders := reqHeaderFromFlags.GroupHeaders; len(groupHeaders) > 0 { + s.Authentication.RequestHeader.GroupHeaders = groupHeaders + } + if extraHeaders := reqHeaderFromFlags.ExtraHeaderPrefixes; len(extraHeaders) > 0 { + s.Authentication.RequestHeader.ExtraHeaderPrefixes = extraHeaders + } + if err := utilversion.DefaultComponentGlobalsRegistry.Set(); err != nil { return result, err } diff --git a/cmd/kube-controller-manager/app/options/options_test.go b/cmd/kube-controller-manager/app/options/options_test.go index be9f0c12082..874a4292fe9 100644 --- a/cmd/kube-controller-manager/app/options/options_test.go +++ b/cmd/kube-controller-manager/app/options/options_test.go @@ -430,6 +430,7 @@ func TestAddFlags(t *testing.T) { ClientCert: apiserveroptions.ClientCertAuthenticationOptions{}, RequestHeader: apiserveroptions.RequestHeaderAuthenticationOptions{ UsernameHeaders: []string{"x-remote-user"}, + UIDHeaders: nil, GroupHeaders: []string{"x-remote-group"}, ExtraHeaderPrefixes: []string{"x-remote-extra-"}, }, diff --git a/pkg/controlplane/apiserver/config.go b/pkg/controlplane/apiserver/config.go index c204e5058ef..fe99c7d4362 100644 --- a/pkg/controlplane/apiserver/config.go +++ b/pkg/controlplane/apiserver/config.go @@ -337,6 +337,7 @@ func CreateConfig( config.ClusterAuthenticationInfo.RequestHeaderExtraHeaderPrefixes = requestHeaderConfig.ExtraHeaderPrefixes config.ClusterAuthenticationInfo.RequestHeaderGroupHeaders = requestHeaderConfig.GroupHeaders config.ClusterAuthenticationInfo.RequestHeaderUsernameHeaders = requestHeaderConfig.UsernameHeaders + config.ClusterAuthenticationInfo.RequestHeaderUIDHeaders = requestHeaderConfig.UIDHeaders } // setup admission diff --git a/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go b/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go index 49539171391..12e6b250e04 100644 --- a/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go +++ b/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go @@ -77,6 +77,8 @@ type ClusterAuthenticationInfo struct { // RequestHeaderUsernameHeaders are the headers used by this kube-apiserver to determine username RequestHeaderUsernameHeaders headerrequest.StringSliceProvider + // RequestHeaderUIDHeaders are the headers used by this kube-apiserver to determine UID + RequestHeaderUIDHeaders headerrequest.StringSliceProvider // RequestHeaderGroupHeaders are the headers used by this kube-apiserver to determine groups RequestHeaderGroupHeaders headerrequest.StringSliceProvider // RequestHeaderExtraHeaderPrefixes are the headers used by this kube-apiserver to determine user.extra @@ -224,6 +226,7 @@ func combinedClusterAuthenticationInfo(lhs, rhs ClusterAuthenticationInfo) (Clus RequestHeaderExtraHeaderPrefixes: combineUniqueStringSlices(lhs.RequestHeaderExtraHeaderPrefixes, rhs.RequestHeaderExtraHeaderPrefixes), RequestHeaderGroupHeaders: combineUniqueStringSlices(lhs.RequestHeaderGroupHeaders, rhs.RequestHeaderGroupHeaders), RequestHeaderUsernameHeaders: combineUniqueStringSlices(lhs.RequestHeaderUsernameHeaders, rhs.RequestHeaderUsernameHeaders), + RequestHeaderUIDHeaders: combineUniqueStringSlices(lhs.RequestHeaderUIDHeaders, rhs.RequestHeaderUIDHeaders), } var err error @@ -259,6 +262,10 @@ 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 + } data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value()) if err != nil { return nil, err @@ -298,6 +305,10 @@ 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 caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 { ret.RequestHeaderCA, err = dynamiccertificates.NewStaticCAContent("existing", []byte(caBundle)) diff --git a/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller_test.go b/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller_test.go index 959fe3b35cb..d593799ee82 100644 --- a/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller_test.go +++ b/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller_test.go @@ -101,6 +101,7 @@ func TestWriteClientCAs(t *testing.T) { clusterAuthInfo: ClusterAuthenticationInfo{ ClientCA: someRandomCAProvider, RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{"alfa", "bravo", "charlie"}, + RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{"golf", "hotel", "india"}, RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{"delta"}, RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{"echo", "foxtrot"}, RequestHeaderCA: anotherRandomCAProvider, @@ -112,6 +113,7 @@ func TestWriteClientCAs(t *testing.T) { Data: map[string]string{ "client-ca-file": string(someRandomCA), "requestheader-username-headers": `["alfa","bravo","charlie"]`, + "requestheader-uid-headers": `["golf","hotel","india"]`, "requestheader-group-headers": `["delta"]`, "requestheader-extra-headers-prefix": `["echo","foxtrot"]`, "requestheader-client-ca-file": string(anotherRandomCA), @@ -132,6 +134,7 @@ func TestWriteClientCAs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, Data: map[string]string{ "requestheader-username-headers": `[]`, + "requestheader-uid-headers": `[]`, "requestheader-group-headers": `[]`, "requestheader-extra-headers-prefix": `[]`, "requestheader-client-ca-file": string(anotherRandomCA), @@ -166,6 +169,7 @@ func TestWriteClientCAs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, Data: map[string]string{ "requestheader-username-headers": `[]`, + "requestheader-uid-headers": `[]`, "requestheader-group-headers": `[]`, "requestheader-extra-headers-prefix": `[]`, "requestheader-client-ca-file": string(anotherRandomCA), @@ -201,6 +205,7 @@ func TestWriteClientCAs(t *testing.T) { name: "overwrite extension-apiserver-authentication requestheader", clusterAuthInfo: ClusterAuthenticationInfo{ RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{}, + RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{}, RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{}, RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{}, RequestHeaderCA: anotherRandomCAProvider, @@ -211,6 +216,7 @@ func TestWriteClientCAs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, Data: map[string]string{ "requestheader-username-headers": `[]`, + "requestheader-uid-headers": `[]`, "requestheader-group-headers": `[]`, "requestheader-extra-headers-prefix": `[]`, "requestheader-client-ca-file": string(someRandomCA), @@ -223,6 +229,7 @@ func TestWriteClientCAs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, Data: map[string]string{ "requestheader-username-headers": `[]`, + "requestheader-uid-headers": `[]`, "requestheader-group-headers": `[]`, "requestheader-extra-headers-prefix": `[]`, "requestheader-client-ca-file": string(someRandomCA) + string(anotherRandomCA), @@ -253,6 +260,7 @@ func TestWriteClientCAs(t *testing.T) { name: "skip on no change", clusterAuthInfo: ClusterAuthenticationInfo{ RequestHeaderUsernameHeaders: headerrequest.StaticStringSlice{}, + RequestHeaderUIDHeaders: headerrequest.StaticStringSlice{}, RequestHeaderGroupHeaders: headerrequest.StaticStringSlice{}, RequestHeaderExtraHeaderPrefixes: headerrequest.StaticStringSlice{}, RequestHeaderCA: anotherRandomCAProvider, @@ -263,6 +271,7 @@ func TestWriteClientCAs(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, Data: map[string]string{ "requestheader-username-headers": `[]`, + "requestheader-uid-headers": `[]`, "requestheader-group-headers": `[]`, "requestheader-extra-headers-prefix": `[]`, "requestheader-client-ca-file": string(anotherRandomCA), @@ -332,6 +341,7 @@ func TestWriteConfigMapDeleted(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: "extension-apiserver-authentication"}, Data: map[string]string{ "requestheader-username-headers": `[]`, + "requestheader-uid-headers": `[]`, "requestheader-group-headers": `[]`, "requestheader-extra-headers-prefix": `[]`, "requestheader-client-ca-file": string(anotherRandomCA), diff --git a/pkg/kubeapiserver/authenticator/config.go b/pkg/kubeapiserver/authenticator/config.go index 9b1655b139a..fe583eca253 100644 --- a/pkg/kubeapiserver/authenticator/config.go +++ b/pkg/kubeapiserver/authenticator/config.go @@ -110,6 +110,7 @@ func (config Config) New(serverLifecycle context.Context) (authenticator.Request config.RequestHeaderConfig.CAContentProvider.VerifyOptions, config.RequestHeaderConfig.AllowedClientNames, config.RequestHeaderConfig.UsernameHeaders, + config.RequestHeaderConfig.UIDHeaders, config.RequestHeaderConfig.GroupHeaders, config.RequestHeaderConfig.ExtraHeaderPrefixes, ) diff --git a/pkg/kubeapiserver/options/authentication_test.go b/pkg/kubeapiserver/options/authentication_test.go index 7185f385c7a..beedd013d8e 100644 --- a/pkg/kubeapiserver/options/authentication_test.go +++ b/pkg/kubeapiserver/options/authentication_test.go @@ -303,6 +303,7 @@ func TestToAuthenticationConfig(t *testing.T) { }, RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{ UsernameHeaders: []string{"x-remote-user"}, + UIDHeaders: []string{"x-remote-uid"}, GroupHeaders: []string{"x-remote-group"}, ExtraHeaderPrefixes: []string{"x-remote-extra-"}, ClientCAFile: "testdata/root.pem", @@ -352,6 +353,7 @@ func TestToAuthenticationConfig(t *testing.T) { RequestHeaderConfig: &authenticatorfactory.RequestHeaderConfig{ UsernameHeaders: headerrequest.StaticStringSlice{"x-remote-user"}, + UIDHeaders: headerrequest.StaticStringSlice{"x-remote-uid"}, GroupHeaders: headerrequest.StaticStringSlice{"x-remote-group"}, ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"x-remote-extra-"}, CAContentProvider: nil, // this is nil because you can't compare functions @@ -397,6 +399,7 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) { "--client-ca-file=client-cacert", "--requestheader-client-ca-file=testdata/root.pem", "--requestheader-username-headers=x-remote-user-custom", + "--requestheader-uid-headers=x-remote-uid-custom", "--requestheader-group-headers=x-remote-group-custom", "--requestheader-allowed-names=kube-aggregator", "--service-account-key-file=cert", @@ -430,6 +433,7 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) { RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{ ClientCAFile: "testdata/root.pem", UsernameHeaders: []string{"x-remote-user-custom"}, + UIDHeaders: []string{"x-remote-uid-custom"}, GroupHeaders: []string{"x-remote-group-custom"}, AllowedNames: []string{"kube-aggregator"}, }, diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go index 76ef44732ed..b74b8b0d494 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go @@ -77,6 +77,7 @@ func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.Secur c.RequestHeaderConfig.CAContentProvider.VerifyOptions, c.RequestHeaderConfig.AllowedClientNames, c.RequestHeaderConfig.UsernameHeaders, + c.RequestHeaderConfig.UIDHeaders, c.RequestHeaderConfig.GroupHeaders, c.RequestHeaderConfig.ExtraHeaderPrefixes, ) diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/requestheader.go b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/requestheader.go index 766bde51727..f217b94efad 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/requestheader.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/requestheader.go @@ -24,6 +24,8 @@ import ( type RequestHeaderConfig struct { // UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins. UsernameHeaders headerrequest.StringSliceProvider + // UsernameHeaders are the headers to check (in order, case-insensitively) for an identity UID. The first header with a value wins. + UIDHeaders headerrequest.StringSliceProvider // GroupHeaders are the headers to check (case-insensitively) for a group names. All values will be used. GroupHeaders headerrequest.StringSliceProvider // ExtraHeaderPrefixes are the head prefixes to check (case-insentively) for filling in diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader.go b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader.go index 18261639394..57bf9ca300f 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader.go @@ -53,6 +53,9 @@ type requestHeaderAuthRequestHandler struct { // nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins. nameHeaders StringSliceProvider + // nameHeaders are the headers to check (in order, case-insensitively) for an identity UID. The first header with a value wins. + uidHeaders StringSliceProvider + // groupHeaders are the headers to check (case-insensitively) for group membership. All values of all headers will be added. groupHeaders StringSliceProvider @@ -61,11 +64,15 @@ type requestHeaderAuthRequestHandler struct { extraHeaderPrefixes StringSliceProvider } -func New(nameHeaders, groupHeaders, extraHeaderPrefixes []string) (authenticator.Request, error) { +func New(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes []string) (authenticator.Request, error) { trimmedNameHeaders, err := trimHeaders(nameHeaders...) if err != nil { return nil, err } + trimmedUIDHeaders, err := trimHeaders(uidHeaders...) + if err != nil { + return nil, err + } trimmedGroupHeaders, err := trimHeaders(groupHeaders...) if err != nil { return nil, err @@ -77,14 +84,16 @@ func New(nameHeaders, groupHeaders, extraHeaderPrefixes []string) (authenticator return NewDynamic( StaticStringSlice(trimmedNameHeaders), + StaticStringSlice(trimmedUIDHeaders), StaticStringSlice(trimmedGroupHeaders), StaticStringSlice(trimmedExtraHeaderPrefixes), ), nil } -func NewDynamic(nameHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request { +func NewDynamic(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request { return &requestHeaderAuthRequestHandler{ nameHeaders: nameHeaders, + uidHeaders: uidHeaders, groupHeaders: groupHeaders, extraHeaderPrefixes: extraHeaderPrefixes, } @@ -103,8 +112,8 @@ func trimHeaders(headerNames ...string) ([]string, error) { return ret, nil } -func NewDynamicVerifyOptionsSecure(verifyOptionFn x509request.VerifyOptionFunc, proxyClientNames, nameHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request { - headerAuthenticator := NewDynamic(nameHeaders, groupHeaders, extraHeaderPrefixes) +func NewDynamicVerifyOptionsSecure(verifyOptionFn x509request.VerifyOptionFunc, proxyClientNames, nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) authenticator.Request { + headerAuthenticator := NewDynamic(nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes) return x509request.NewDynamicCAVerifier(verifyOptionFn, headerAuthenticator, proxyClientNames) } @@ -114,25 +123,30 @@ func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) if len(name) == 0 { return nil, false, nil } + uid := headerValue(req.Header, a.uidHeaders.Value()) groups := allHeaderValues(req.Header, a.groupHeaders.Value()) extra := newExtra(req.Header, a.extraHeaderPrefixes.Value()) // clear headers used for authentication - ClearAuthenticationHeaders(req.Header, a.nameHeaders, a.groupHeaders, a.extraHeaderPrefixes) + ClearAuthenticationHeaders(req.Header, a.nameHeaders, a.uidHeaders, a.groupHeaders, a.extraHeaderPrefixes) return &authenticator.Response{ User: &user.DefaultInfo{ Name: name, + UID: uid, Groups: groups, Extra: extra, }, }, true, nil } -func ClearAuthenticationHeaders(h http.Header, nameHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) { +func ClearAuthenticationHeaders(h http.Header, nameHeaders, uidHeaders, groupHeaders, extraHeaderPrefixes StringSliceProvider) { for _, headerName := range nameHeaders.Value() { h.Del(headerName) } + for _, headerName := range uidHeaders.Value() { + h.Del(headerName) + } for _, headerName := range groupHeaders.Value() { h.Del(headerName) } diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller.go b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller.go index dc844ee73bf..38d6cbe71a1 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller.go @@ -45,6 +45,7 @@ const ( // RequestHeaderAuthRequestProvider a provider that knows how to dynamically fill parts of RequestHeaderConfig struct type RequestHeaderAuthRequestProvider interface { UsernameHeaders() []string + UIDHeaders() []string GroupHeaders() []string ExtraHeaderPrefixes() []string AllowedClientNames() []string @@ -54,6 +55,7 @@ var _ RequestHeaderAuthRequestProvider = &RequestHeaderAuthRequestController{} type requestHeaderBundle struct { UsernameHeaders []string + UIDHeaders []string GroupHeaders []string ExtraHeaderPrefixes []string AllowedClientNames []string @@ -80,6 +82,7 @@ type RequestHeaderAuthRequestController struct { exportedRequestHeaderBundle atomic.Value usernameHeadersKey string + uidHeadersKey string groupHeadersKey string extraHeaderPrefixesKey string allowedClientNamesKey string @@ -90,7 +93,7 @@ func NewRequestHeaderAuthRequestController( cmName string, cmNamespace string, client kubernetes.Interface, - usernameHeadersKey, groupHeadersKey, extraHeaderPrefixesKey, allowedClientNamesKey string) *RequestHeaderAuthRequestController { + usernameHeadersKey, uidHeadersKey, groupHeadersKey, extraHeaderPrefixesKey, allowedClientNamesKey string) *RequestHeaderAuthRequestController { c := &RequestHeaderAuthRequestController{ name: "RequestHeaderAuthRequestController", @@ -100,6 +103,7 @@ func NewRequestHeaderAuthRequestController( configmapNamespace: cmNamespace, usernameHeadersKey: usernameHeadersKey, + uidHeadersKey: uidHeadersKey, groupHeadersKey: groupHeadersKey, extraHeaderPrefixesKey: extraHeaderPrefixesKey, allowedClientNamesKey: allowedClientNamesKey, @@ -152,6 +156,10 @@ func (c *RequestHeaderAuthRequestController) UsernameHeaders() []string { return c.loadRequestHeaderFor(c.usernameHeadersKey) } +func (c *RequestHeaderAuthRequestController) UIDHeaders() []string { + return c.loadRequestHeaderFor(c.uidHeadersKey) +} + func (c *RequestHeaderAuthRequestController) GroupHeaders() []string { return c.loadRequestHeaderFor(c.groupHeadersKey) } @@ -278,6 +286,11 @@ func (c *RequestHeaderAuthRequestController) getRequestHeaderBundleFromConfigMap return nil, err } + uidHeaderCurrentValue, err := deserializeStrings(cm.Data[c.uidHeadersKey]) + if err != nil { + return nil, err + } + groupHeadersCurrentValue, err := deserializeStrings(cm.Data[c.groupHeadersKey]) if err != nil { return nil, err @@ -296,6 +309,7 @@ func (c *RequestHeaderAuthRequestController) getRequestHeaderBundleFromConfigMap return &requestHeaderBundle{ UsernameHeaders: usernameHeaderCurrentValue, + UIDHeaders: uidHeaderCurrentValue, GroupHeaders: groupHeadersCurrentValue, ExtraHeaderPrefixes: extraHeaderPrefixesCurrentValue, AllowedClientNames: allowedClientNamesCurrentValue, @@ -312,6 +326,8 @@ func (c *RequestHeaderAuthRequestController) loadRequestHeaderFor(key string) [] switch key { case c.usernameHeadersKey: return headerBundle.UsernameHeaders + case c.uidHeadersKey: + return headerBundle.UIDHeaders case c.groupHeadersKey: return headerBundle.GroupHeaders case c.extraHeaderPrefixesKey: diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller_test.go index 36dfbf1ec29..861c751080c 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller_test.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_controller_test.go @@ -19,10 +19,10 @@ package headerrequest import ( "context" "encoding/json" - "k8s.io/apimachinery/pkg/api/equality" "testing" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" corev1listers "k8s.io/client-go/listers/core/v1" @@ -34,6 +34,7 @@ const ( defConfigMapNamespace = "kube-system" defUsernameHeadersKey = "user-key" + defUIDHeadersKey = "uid-key" defGroupHeadersKey = "group-key" defExtraHeaderPrefixesKey = "extra-key" defAllowedClientNamesKey = "names-key" @@ -41,6 +42,7 @@ const ( type expectedHeadersHolder struct { usernameHeaders []string + uidHeaders []string groupHeaders []string extraHeaderPrefixes []string allowedClientNames []string @@ -55,9 +57,10 @@ func TestRequestHeaderAuthRequestController(t *testing.T) { }{ { name: "happy-path: headers values are populated form a config map", - cm: defaultConfigMap(t, []string{"user-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}), + cm: defaultConfigMap(t, []string{"user-val"}, []string{"uid-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}), expectedHeader: expectedHeadersHolder{ usernameHeaders: []string{"user-val"}, + uidHeaders: []string{"uid-val"}, groupHeaders: []string{"group-val"}, extraHeaderPrefixes: []string{"extra-val"}, allowedClientNames: []string{"names-val"}, @@ -66,7 +69,7 @@ func TestRequestHeaderAuthRequestController(t *testing.T) { { name: "passing an empty config map doesn't break the controller", cm: func() *corev1.ConfigMap { - c := defaultConfigMap(t, nil, nil, nil, nil) + c := defaultConfigMap(t, nil, nil, nil, nil, nil) c.Data = map[string]string{} return c }(), @@ -74,7 +77,7 @@ func TestRequestHeaderAuthRequestController(t *testing.T) { { name: "an invalid config map produces an error", cm: func() *corev1.ConfigMap { - c := defaultConfigMap(t, nil, nil, nil, nil) + c := defaultConfigMap(t, nil, nil, nil, nil, nil) c.Data = map[string]string{ defUsernameHeadersKey: "incorrect-json-array", } @@ -119,9 +122,10 @@ func TestRequestHeaderAuthRequestControllerPreserveState(t *testing.T) { }{ { name: "scenario 1: headers values are populated form a config map", - cm: defaultConfigMap(t, []string{"user-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}), + cm: defaultConfigMap(t, []string{"user-val"}, []string{"uid-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}), expectedHeader: expectedHeadersHolder{ usernameHeaders: []string{"user-val"}, + uidHeaders: []string{"uid-val"}, groupHeaders: []string{"group-val"}, extraHeaderPrefixes: []string{"extra-val"}, allowedClientNames: []string{"names-val"}, @@ -130,7 +134,7 @@ func TestRequestHeaderAuthRequestControllerPreserveState(t *testing.T) { { name: "scenario 2: an invalid config map produces an error but doesn't destroy the state (scenario 1)", cm: func() *corev1.ConfigMap { - c := defaultConfigMap(t, nil, nil, nil, nil) + c := defaultConfigMap(t, nil, nil, nil, nil, nil) c.Data = map[string]string{ defUsernameHeadersKey: "incorrect-json-array", } @@ -139,6 +143,7 @@ func TestRequestHeaderAuthRequestControllerPreserveState(t *testing.T) { expectErr: true, expectedHeader: expectedHeadersHolder{ usernameHeaders: []string{"user-val"}, + uidHeaders: []string{"uid-val"}, groupHeaders: []string{"group-val"}, extraHeaderPrefixes: []string{"extra-val"}, allowedClientNames: []string{"names-val"}, @@ -146,9 +151,10 @@ func TestRequestHeaderAuthRequestControllerPreserveState(t *testing.T) { }, { name: "scenario 3: some headers values have changed (prev set by scenario 1)", - cm: defaultConfigMap(t, []string{"user-val"}, []string{"group-val-scenario-3"}, []string{"extra-val"}, []string{"names-val"}), + cm: defaultConfigMap(t, []string{"user-val"}, []string{"uid-val"}, []string{"group-val-scenario-3"}, []string{"extra-val"}, []string{"names-val"}), expectedHeader: expectedHeadersHolder{ usernameHeaders: []string{"user-val"}, + uidHeaders: []string{"uid-val"}, groupHeaders: []string{"group-val-scenario-3"}, extraHeaderPrefixes: []string{"extra-val"}, allowedClientNames: []string{"names-val"}, @@ -156,9 +162,10 @@ func TestRequestHeaderAuthRequestControllerPreserveState(t *testing.T) { }, { name: "scenario 4: all headers values have changed (prev set by scenario 3)", - cm: defaultConfigMap(t, []string{"user-val-scenario-4"}, []string{"group-val-scenario-4"}, []string{"extra-val-scenario-4"}, []string{"names-val-scenario-4"}), + cm: defaultConfigMap(t, []string{"user-val-scenario-4"}, []string{"uid-val-scenario-4"}, []string{"group-val-scenario-4"}, []string{"extra-val-scenario-4"}, []string{"names-val-scenario-4"}), expectedHeader: expectedHeadersHolder{ usernameHeaders: []string{"user-val-scenario-4"}, + uidHeaders: []string{"uid-val-scenario-4"}, groupHeaders: []string{"group-val-scenario-4"}, extraHeaderPrefixes: []string{"extra-val-scenario-4"}, allowedClientNames: []string{"names-val-scenario-4"}, @@ -204,9 +211,10 @@ func TestRequestHeaderAuthRequestControllerSyncOnce(t *testing.T) { }{ { name: "headers values are populated form a config map", - cm: defaultConfigMap(t, []string{"user-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}), + cm: defaultConfigMap(t, []string{"user-val"}, []string{"uid-val"}, []string{"group-val"}, []string{"extra-val"}, []string{"names-val"}), expectedHeader: expectedHeadersHolder{ usernameHeaders: []string{"user-val"}, + uidHeaders: []string{"uid-val"}, groupHeaders: []string{"group-val"}, extraHeaderPrefixes: []string{"extra-val"}, allowedClientNames: []string{"names-val"}, @@ -238,7 +246,7 @@ func TestRequestHeaderAuthRequestControllerSyncOnce(t *testing.T) { } } -func defaultConfigMap(t *testing.T, usernameHeaderVal, groupHeadersVal, extraHeaderPrefixesVal, allowedClientNamesVal []string) *corev1.ConfigMap { +func defaultConfigMap(t *testing.T, usernameHeaderVal, uidHeaderVal, groupHeadersVal, extraHeaderPrefixesVal, allowedClientNamesVal []string) *corev1.ConfigMap { encode := func(val []string) string { encodedVal, err := json.Marshal(val) if err != nil { @@ -253,6 +261,7 @@ func defaultConfigMap(t *testing.T, usernameHeaderVal, groupHeadersVal, extraHea }, Data: map[string]string{ defUsernameHeadersKey: encode(usernameHeaderVal), + defUIDHeadersKey: encode(uidHeaderVal), defGroupHeadersKey: encode(groupHeadersVal), defExtraHeaderPrefixesKey: encode(extraHeaderPrefixesVal), defAllowedClientNamesKey: encode(allowedClientNamesVal), @@ -265,6 +274,7 @@ func newDefaultTarget() *RequestHeaderAuthRequestController { configmapName: defConfigMapName, configmapNamespace: defConfigMapNamespace, usernameHeadersKey: defUsernameHeadersKey, + uidHeadersKey: defUIDHeadersKey, groupHeadersKey: defGroupHeadersKey, extraHeaderPrefixesKey: defExtraHeaderPrefixesKey, allowedClientNamesKey: defAllowedClientNamesKey, diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_test.go index 24541a3f719..c698ca1d149 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_test.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader_test.go @@ -29,6 +29,7 @@ import ( func TestRequestHeader(t *testing.T) { testcases := map[string]struct { nameHeaders []string + uidHeaders []string groupHeaders []string extraPrefixHeaders []string requestHeaders http.Header @@ -128,13 +129,66 @@ func TestRequestHeader(t *testing.T) { }, expectedOk: true, }, - + "uid none": { + nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Bob"}, + }, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + UID: "", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedOk: true, + }, + "uid exact match": { + nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Bob"}, + // The keys in http.Header MUST be http.CanonicalHeaderKey. + // Hence X-Remote-Uid-1 instead of X-Remote-UID-1. + "X-Remote-Uid-1": {"8f5ea9d1-a5ed-4d02-80a2-26709216350b"}, + "X-Remote-Uid-2": {"c7644180-c774-4a9b-81e5-3eef76f087ab"}, + }, + finalHeaders: http.Header{ + "X-Remote-Uid-1": {"8f5ea9d1-a5ed-4d02-80a2-26709216350b"}, + "X-Remote-Uid-2": {"c7644180-c774-4a9b-81e5-3eef76f087ab"}, + }, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + UID: "", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedOk: true, + }, + "uid first match": { + nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid-1", "X-Remote-Uid-2"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Bob"}, + "X-Remote-Uid-1": {"8f5ea9d1-a5ed-4d02-80a2-26709216350b"}, + "X-Remote-Uid-2": {"c7644180-c774-4a9b-81e5-3eef76f087ab"}, + }, + expectedUser: &user.DefaultInfo{ + Name: "Bob", + UID: "8f5ea9d1-a5ed-4d02-80a2-26709216350b", + Groups: []string{}, + Extra: map[string][]string{}, + }, + expectedOk: true, + }, "extra prefix matches case-insensitive": { nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-UID"}, groupHeaders: []string{"X-Remote-Group-1", "X-Remote-Group-2"}, extraPrefixHeaders: []string{"X-Remote-Extra-1-", "X-Remote-Extra-2-"}, requestHeaders: http.Header{ "X-Remote-User": {"Bob"}, + "X-Remote-Uid": {"2ca80fb0-60ea-4ecf-951c-89af843b0402"}, "X-Remote-Group-1": {"one-a", "one-b"}, "X-Remote-Group-2": {"two-a", "two-b"}, "X-Remote-extra-1-key1": {"alfa", "bravo"}, @@ -146,6 +200,7 @@ func TestRequestHeader(t *testing.T) { }, expectedUser: &user.DefaultInfo{ Name: "Bob", + UID: "2ca80fb0-60ea-4ecf-951c-89af843b0402", Groups: []string{"one-a", "one-b", "two-a", "two-b"}, Extra: map[string][]string{ "key1": {"alfa", "bravo", "echo", "foxtrot"}, @@ -191,10 +246,12 @@ func TestRequestHeader(t *testing.T) { "escaped extra keys": { nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid"}, groupHeaders: []string{"X-Remote-Group"}, extraPrefixHeaders: []string{"X-Remote-Extra-"}, requestHeaders: http.Header{ "X-Remote-User": {"Bob"}, + "X-Remote-Uid": {"2ca80fb0-60ea-4ecf-951c-89af843b0402"}, "X-Remote-Group": {"one-a", "one-b"}, "X-Remote-Extra-Alpha": {"alphabetical"}, "X-Remote-Extra-Alph4num3r1c": {"alphanumeric"}, @@ -206,6 +263,7 @@ func TestRequestHeader(t *testing.T) { }, expectedUser: &user.DefaultInfo{ Name: "Bob", + UID: "2ca80fb0-60ea-4ecf-951c-89af843b0402", Groups: []string{"one-a", "one-b"}, Extra: map[string][]string{ "alpha": {"alphabetical"}, @@ -223,7 +281,7 @@ func TestRequestHeader(t *testing.T) { for k, testcase := range testcases { t.Run(k, func(t *testing.T) { - auth, err := New(testcase.nameHeaders, testcase.groupHeaders, testcase.extraPrefixHeaders) + auth, err := New(testcase.nameHeaders, testcase.uidHeaders, testcase.groupHeaders, testcase.extraPrefixHeaders) if err != nil { t.Fatal(err) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication.go index 64b3569d0d9..980e11f6e11 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication.go @@ -54,6 +54,7 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed } standardRequestHeaderConfig := &authenticatorfactory.RequestHeaderConfig{ UsernameHeaders: headerrequest.StaticStringSlice{"X-Remote-User"}, + UIDHeaders: headerrequest.StaticStringSlice{"X-Remote-Uid"}, GroupHeaders: headerrequest.StaticStringSlice{"X-Remote-Group"}, ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"X-Remote-Extra-"}, } @@ -90,6 +91,7 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed headerrequest.ClearAuthenticationHeaders( req.Header, standardRequestHeaderConfig.UsernameHeaders, + standardRequestHeaderConfig.UIDHeaders, standardRequestHeaderConfig.GroupHeaders, standardRequestHeaderConfig.ExtraHeaderPrefixes, ) @@ -99,6 +101,7 @@ func withAuthentication(handler http.Handler, auth authenticator.Request, failed headerrequest.ClearAuthenticationHeaders( req.Header, requestHeaderConfig.UsernameHeaders, + requestHeaderConfig.UIDHeaders, requestHeaderConfig.GroupHeaders, requestHeaderConfig.ExtraHeaderPrefixes, ) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go index 2ef45a6be88..75bc14c5ca5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go @@ -285,6 +285,7 @@ func TestAuthenticateRequestError(t *testing.T) { func TestAuthenticateRequestClearHeaders(t *testing.T) { testcases := map[string]struct { nameHeaders []string + uidHeaders []string groupHeaders []string extraPrefixHeaders []string requestHeaders http.Header @@ -334,13 +335,39 @@ func TestAuthenticateRequestClearHeaders(t *testing.T) { "X-Remote-Group": {"Users"}, }, }, + "uid none": { + nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Alice"}, + }, + }, + "uid all matches": { + nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid-1", "X-Remote-Uid-2"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Alice"}, + "X-Remote-Uid-1": {"one"}, + "X-Remote-Uid-2": {"two", "three"}, + }, + }, + "uid case-insensitive": { + nameHeaders: []string{"X-Remote-USER"}, + uidHeaders: []string{"X-REMOTE-UID-1"}, + requestHeaders: http.Header{ + "X-Remote-User": {"Alice"}, + "X-Remote-Uid-1": {"one"}, + }, + }, "extra prefix matches case-insensitive": { nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid-1"}, groupHeaders: []string{"X-Remote-Group-1", "X-Remote-Group-2"}, extraPrefixHeaders: []string{"X-Remote-Extra-1-", "X-Remote-Extra-2-"}, requestHeaders: http.Header{ "X-Remote-User": {"Bob"}, + "X-Remote-Uid-1": {"bobs-uid"}, "X-Remote-Group-1": {"one-a", "one-b"}, "X-Remote-Group-2": {"two-a", "two-b"}, "X-Remote-extra-1-key1": {"alfa", "bravo"}, @@ -354,12 +381,15 @@ func TestAuthenticateRequestClearHeaders(t *testing.T) { "extra prefix matches case-insensitive with unrelated headers": { nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid"}, groupHeaders: []string{"X-Remote-Group-1", "X-Remote-Group-2"}, extraPrefixHeaders: []string{"X-Remote-Extra-1-", "X-Remote-Extra-2-"}, requestHeaders: http.Header{ "X-Group-Remote": {"snorlax"}, // unrelated header "X-Group-Bear": {"panda"}, // another unrelated header + "X-Uid-Remote": {"bobs-unrelated-uid"}, "X-Remote-User": {"Bob"}, + "X-Remote-Uid": {"bobs-uid"}, "X-Remote-Group-1": {"one-a", "one-b"}, "X-Remote-Group-2": {"two-a", "two-b"}, "X-Remote-extra-1-key1": {"alfa", "bravo"}, @@ -372,15 +402,18 @@ func TestAuthenticateRequestClearHeaders(t *testing.T) { finalHeaders: http.Header{ "X-Group-Remote": {"snorlax"}, "X-Group-Bear": {"panda"}, + "X-Uid-Remote": {"bobs-unrelated-uid"}, }, }, "custom config but request contains standard headers": { nameHeaders: []string{"foo"}, + uidHeaders: []string{"footoo"}, groupHeaders: []string{"bar"}, extraPrefixHeaders: []string{"baz"}, requestHeaders: http.Header{ "X-Remote-User": {"Bob"}, + "X-Remote-Uid": {"bobs-uid"}, "X-Remote-Group-1": {"one-a", "one-b"}, "X-Remote-Group-2": {"two-a", "two-b"}, "X-Remote-extra-1-key1": {"alfa", "bravo"}, @@ -398,10 +431,12 @@ func TestAuthenticateRequestClearHeaders(t *testing.T) { "custom config but request contains standard and custom headers": { nameHeaders: []string{"one"}, + uidHeaders: []string{"onetoo"}, groupHeaders: []string{"two"}, extraPrefixHeaders: []string{"three-"}, requestHeaders: http.Header{ "X-Remote-User": {"Bob"}, + "X-Remote-Uid": {"bobs-uid"}, "X-Remote-Group-3": {"one-a", "one-b"}, "X-Remote-Group-4": {"two-a", "two-b"}, "X-Remote-extra-1-key1": {"alfa", "bravo"}, @@ -422,10 +457,12 @@ func TestAuthenticateRequestClearHeaders(t *testing.T) { "escaped extra keys": { nameHeaders: []string{"X-Remote-User"}, + uidHeaders: []string{"X-Remote-Uid"}, groupHeaders: []string{"X-Remote-Group"}, extraPrefixHeaders: []string{"X-Remote-Extra-"}, requestHeaders: http.Header{ "X-Remote-User": {"Bob"}, + "X-Remote-Uid": {"bobs-uid"}, "X-Remote-Group": {"one-a", "one-b"}, "X-Remote-Extra-Alpha": {"alphabetical"}, "X-Remote-Extra-Alph4num3r1c": {"alphanumeric"}, @@ -455,6 +492,7 @@ func TestAuthenticateRequestClearHeaders(t *testing.T) { nil, &authenticatorfactory.RequestHeaderConfig{ UsernameHeaders: headerrequest.StaticStringSlice(testcase.nameHeaders), + UIDHeaders: headerrequest.StaticStringSlice(testcase.uidHeaders), GroupHeaders: headerrequest.StaticStringSlice(testcase.groupHeaders), ExtraHeaderPrefixes: headerrequest.StaticStringSlice(testcase.extraPrefixHeaders), }, 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 c40e4cf4344..6b53908355c 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go @@ -56,6 +56,7 @@ type RequestHeaderAuthenticationOptions struct { ClientCAFile string UsernameHeaders []string + UIDHeaders []string GroupHeaders []string ExtraHeaderPrefixes []string AllowedNames []string @@ -67,6 +68,9 @@ 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) } @@ -80,6 +84,10 @@ 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") } @@ -117,6 +125,9 @@ func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { fs.StringSliceVar(&s.UsernameHeaders, "requestheader-username-headers", s.UsernameHeaders, ""+ "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.") + fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+ "List of request headers to inspect for groups. X-Remote-Group is suggested.") @@ -148,6 +159,7 @@ func (s *RequestHeaderAuthenticationOptions) ToAuthenticationRequestHeaderConfig return &authenticatorfactory.RequestHeaderConfig{ UsernameHeaders: headerrequest.StaticStringSlice(s.UsernameHeaders), + UIDHeaders: headerrequest.StaticStringSlice(s.UIDHeaders), GroupHeaders: headerrequest.StaticStringSlice(s.GroupHeaders), ExtraHeaderPrefixes: headerrequest.StaticStringSlice(s.ExtraHeaderPrefixes), CAContentProvider: caBundleProvider, @@ -233,7 +245,13 @@ func NewDelegatingAuthenticationOptions() *DelegatingAuthenticationOptions { CacheTTL: 10 * time.Second, ClientCert: ClientCertAuthenticationOptions{}, RequestHeader: RequestHeaderAuthenticationOptions{ - UsernameHeaders: []string{"x-remote-user"}, + UsernameHeaders: []string{"x-remote-user"}, + // we specifically don't default UID headers as these were introduced + // later (kube 1.32) and we don't want 3rd parties to be trusting the default headers + // before we can safely say that all KAS instances know they should + // remove them from an incoming request in its WithAuthentication handler. + // The defaulting will be enabled in a future (1.33+) version. + UIDHeaders: nil, GroupHeaders: []string{"x-remote-group"}, ExtraHeaderPrefixes: []string{"x-remote-extra-"}, }, @@ -423,6 +441,7 @@ func (s *DelegatingAuthenticationOptions) createRequestHeaderConfig(client kuber return &authenticatorfactory.RequestHeaderConfig{ CAContentProvider: dynamicRequestHeaderProvider, UsernameHeaders: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.UsernameHeaders)), + UIDHeaders: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.UIDHeaders)), GroupHeaders: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.GroupHeaders)), ExtraHeaderPrefixes: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.ExtraHeaderPrefixes)), AllowedClientNames: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.AllowedClientNames)), diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/authentication_dynamic_request_header.go b/staging/src/k8s.io/apiserver/pkg/server/options/authentication_dynamic_request_header.go index 0dac3402187..4ef3d9b36ab 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/authentication_dynamic_request_header.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/authentication_dynamic_request_header.go @@ -55,6 +55,7 @@ func newDynamicRequestHeaderController(client kubernetes.Interface) (*DynamicReq authenticationConfigMapNamespace, client, "requestheader-username-headers", + "requestheader-uid-headers", "requestheader-group-headers", "requestheader-extra-headers-prefix", "requestheader-allowed-names", diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/authentication_test.go b/staging/src/k8s.io/apiserver/pkg/server/options/authentication_test.go index aa8de47cda8..d60cee13926 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/authentication_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/authentication_test.go @@ -39,6 +39,7 @@ func TestToAuthenticationRequestHeaderConfig(t *testing.T) { name: "test when ClientCAFile is nil", testOptions: &RequestHeaderAuthenticationOptions{ UsernameHeaders: headerrequest.StaticStringSlice{"x-remote-user"}, + UIDHeaders: headerrequest.StaticStringSlice{"x-remote-uid"}, GroupHeaders: headerrequest.StaticStringSlice{"x-remote-group"}, ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"x-remote-extra-"}, AllowedNames: headerrequest.StaticStringSlice{"kube-aggregator"}, @@ -49,12 +50,14 @@ func TestToAuthenticationRequestHeaderConfig(t *testing.T) { testOptions: &RequestHeaderAuthenticationOptions{ ClientCAFile: "testdata/root.pem", UsernameHeaders: headerrequest.StaticStringSlice{"x-remote-user"}, + UIDHeaders: headerrequest.StaticStringSlice{"x-remote-uid"}, GroupHeaders: headerrequest.StaticStringSlice{"x-remote-group"}, ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"x-remote-extra-"}, AllowedNames: headerrequest.StaticStringSlice{"kube-aggregator"}, }, expectConfig: &authenticatorfactory.RequestHeaderConfig{ UsernameHeaders: headerrequest.StaticStringSlice{"x-remote-user"}, + UIDHeaders: headerrequest.StaticStringSlice{"x-remote-uid"}, GroupHeaders: headerrequest.StaticStringSlice{"x-remote-group"}, ExtraHeaderPrefixes: headerrequest.StaticStringSlice{"x-remote-extra-"}, CAContentProvider: nil, // this is nil because you can't compare functions diff --git a/staging/src/k8s.io/apiserver/pkg/util/peerproxy/peerproxy_handler.go b/staging/src/k8s.io/apiserver/pkg/util/peerproxy/peerproxy_handler.go index bc342165b21..ac5a05f1fae 100644 --- a/staging/src/k8s.io/apiserver/pkg/util/peerproxy/peerproxy_handler.go +++ b/staging/src/k8s.io/apiserver/pkg/util/peerproxy/peerproxy_handler.go @@ -251,7 +251,7 @@ func (h *peerProxyHandler) proxyRequestToDestinationAPIServer(req *http.Request, newReq.Header.Add(PeerProxiedHeader, "true") defer cancelFn() - proxyRoundTripper := transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), h.proxyTransport) + proxyRoundTripper := transport.NewAuthProxyRoundTripper(user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra(), h.proxyTransport) delegate := &epmetrics.ResponseWriterDelegator{ResponseWriter: rw} w := responsewriter.WrapForHTTP1Or2(delegate) diff --git a/staging/src/k8s.io/client-go/transport/round_trippers.go b/staging/src/k8s.io/client-go/transport/round_trippers.go index e2d1dcc9a9c..52fefb53163 100644 --- a/staging/src/k8s.io/client-go/transport/round_trippers.go +++ b/staging/src/k8s.io/client-go/transport/round_trippers.go @@ -86,6 +86,7 @@ func DebugWrappers(rt http.RoundTripper) http.RoundTripper { type authProxyRoundTripper struct { username string + uid string groups []string extra map[string][]string @@ -98,15 +99,17 @@ var _ utilnet.RoundTripperWrapper = &authProxyRoundTripper{} // authentication terminating proxy cases // assuming you pull the user from the context: // username is the user.Info.GetName() of the user +// uid is the user.Info.GetUID() of the user // groups is the user.Info.GetGroups() of the user // extra is the user.Info.GetExtra() of the user // extra can contain any additional information that the authenticator // thought was interesting, for example authorization scopes. // In order to faithfully round-trip through an impersonation flow, these keys // MUST be lowercase. -func NewAuthProxyRoundTripper(username string, groups []string, extra map[string][]string, rt http.RoundTripper) http.RoundTripper { +func NewAuthProxyRoundTripper(username, uid string, groups []string, extra map[string][]string, rt http.RoundTripper) http.RoundTripper { return &authProxyRoundTripper{ username: username, + uid: uid, groups: groups, extra: extra, rt: rt, @@ -115,14 +118,15 @@ func NewAuthProxyRoundTripper(username string, groups []string, extra map[string func (rt *authProxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req = utilnet.CloneRequest(req) - SetAuthProxyHeaders(req, rt.username, rt.groups, rt.extra) + SetAuthProxyHeaders(req, rt.username, rt.uid, rt.groups, rt.extra) return rt.rt.RoundTrip(req) } // SetAuthProxyHeaders stomps the auth proxy header fields. It mutates its argument. -func SetAuthProxyHeaders(req *http.Request, username string, groups []string, extra map[string][]string) { +func SetAuthProxyHeaders(req *http.Request, username, uid string, groups []string, extra map[string][]string) { req.Header.Del("X-Remote-User") + req.Header.Del("X-Remote-Uid") req.Header.Del("X-Remote-Group") for key := range req.Header { if strings.HasPrefix(strings.ToLower(key), strings.ToLower("X-Remote-Extra-")) { @@ -131,6 +135,9 @@ func SetAuthProxyHeaders(req *http.Request, username string, groups []string, ex } req.Header.Set("X-Remote-User", username) + if len(uid) > 0 { + req.Header.Set("X-Remote-Uid", uid) + } for _, group := range groups { req.Header.Add("X-Remote-Group", group) } diff --git a/staging/src/k8s.io/client-go/transport/round_trippers_test.go b/staging/src/k8s.io/client-go/transport/round_trippers_test.go index d0410452f8f..1e20f7094cf 100644 --- a/staging/src/k8s.io/client-go/transport/round_trippers_test.go +++ b/staging/src/k8s.io/client-go/transport/round_trippers_test.go @@ -306,12 +306,14 @@ func TestImpersonationRoundTripper(t *testing.T) { func TestAuthProxyRoundTripper(t *testing.T) { for n, tc := range map[string]struct { username string + uid string groups []string extra map[string][]string expectedExtra map[string][]string }{ "allfields": { username: "user", + uid: "7db46926-e803-4337-9a29-f9c1fab7d34a", groups: []string{"groupA", "groupB"}, extra: map[string][]string{ "one": {"alpha", "bravo"}, @@ -324,6 +326,7 @@ func TestAuthProxyRoundTripper(t *testing.T) { }, "escaped extra": { username: "user", + uid: "7db46926-e803-4337-9a29-f9c1fab7d34a", groups: []string{"groupA", "groupB"}, extra: map[string][]string{ "one": {"alpha", "bravo"}, @@ -336,6 +339,7 @@ func TestAuthProxyRoundTripper(t *testing.T) { }, "double escaped extra": { username: "user", + uid: "7db46926-e803-4337-9a29-f9c1fab7d34a", groups: []string{"groupA", "groupB"}, extra: map[string][]string{ "one": {"alpha", "bravo"}, @@ -349,7 +353,7 @@ func TestAuthProxyRoundTripper(t *testing.T) { } { rt := &testRoundTripper{} req := &http.Request{} - NewAuthProxyRoundTripper(tc.username, tc.groups, tc.extra, rt).RoundTrip(req) + _, _ = NewAuthProxyRoundTripper(tc.username, tc.uid, tc.groups, tc.extra, rt).RoundTrip(req) if rt.Request == nil { t.Errorf("%s: unexpected nil request: %v", n, rt) continue @@ -368,6 +372,15 @@ func TestAuthProxyRoundTripper(t *testing.T) { t.Errorf("%s expected %v, got %v", n, e, a) continue } + actualUID, ok := rt.Request.Header["X-Remote-Uid"] + if !ok { + t.Errorf("%s missing value", n) + continue + } + if e, a := []string{tc.uid}, actualUID; !reflect.DeepEqual(e, a) { + t.Errorf("%s expected %v, got %v", n, e, a) + continue + } actualGroups, ok := rt.Request.Header["X-Remote-Group"] if !ok { t.Errorf("%s missing value", n) diff --git a/staging/src/k8s.io/cloud-provider/options/options_test.go b/staging/src/k8s.io/cloud-provider/options/options_test.go index ec6135ce04f..94bb30b4320 100644 --- a/staging/src/k8s.io/cloud-provider/options/options_test.go +++ b/staging/src/k8s.io/cloud-provider/options/options_test.go @@ -133,6 +133,7 @@ func TestDefaultFlags(t *testing.T) { ClientCert: apiserveroptions.ClientCertAuthenticationOptions{}, RequestHeader: apiserveroptions.RequestHeaderAuthenticationOptions{ UsernameHeaders: []string{"x-remote-user"}, + UIDHeaders: nil, GroupHeaders: []string{"x-remote-group"}, ExtraHeaderPrefixes: []string{"x-remote-extra-"}, }, @@ -293,6 +294,7 @@ func TestAddFlags(t *testing.T) { ClientCert: apiserveroptions.ClientCertAuthenticationOptions{}, RequestHeader: apiserveroptions.RequestHeaderAuthenticationOptions{ UsernameHeaders: []string{"x-remote-user"}, + UIDHeaders: nil, GroupHeaders: []string{"x-remote-group"}, ExtraHeaderPrefixes: []string{"x-remote-extra-"}, }, 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 a59974f3005..5292ec86489 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,7 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { proxyRoundTripper := handlingInfo.proxyRoundTripper upgrade := httpstream.IsUpgradeRequest(req) - proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), proxyRoundTripper) + proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetUID(), user.GetGroups(), user.GetExtra(), proxyRoundTripper) if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerTracing) && !upgrade { tracingWrapper := tracing.WrapperFor(r.tracerProvider) @@ -170,7 +170,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.GetGroups(), user.GetExtra()) + transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetUID(), 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 ab2ccea11f2..632727018a5 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 @@ -154,6 +154,7 @@ func TestProxyHandler(t *testing.T) { "proxy with user, insecure": { user: &user.DefaultInfo{ Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", Groups: []string{"one", "two"}, }, path: "/request/path", @@ -178,6 +179,7 @@ func TestProxyHandler(t *testing.T) { "X-Forwarded-Uri": {"/request/path"}, "X-Forwarded-For": {"127.0.0.1"}, "X-Remote-User": {"username"}, + "X-Remote-Uid": {"6b60d791-1af9-4513-92e5-e4252a1e0a78"}, "User-Agent": {"Go-http-client/1.1"}, "Accept-Encoding": {"gzip"}, "X-Remote-Group": {"one", "two"}, @@ -186,6 +188,7 @@ func TestProxyHandler(t *testing.T) { "proxy with user, cabundle": { user: &user.DefaultInfo{ Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", Groups: []string{"one", "two"}, }, path: "/request/path", @@ -210,6 +213,7 @@ func TestProxyHandler(t *testing.T) { "X-Forwarded-Uri": {"/request/path"}, "X-Forwarded-For": {"127.0.0.1"}, "X-Remote-User": {"username"}, + "X-Remote-Uid": {"6b60d791-1af9-4513-92e5-e4252a1e0a78"}, "User-Agent": {"Go-http-client/1.1"}, "Accept-Encoding": {"gzip"}, "X-Remote-Group": {"one", "two"}, @@ -218,6 +222,7 @@ func TestProxyHandler(t *testing.T) { "service unavailable": { user: &user.DefaultInfo{ Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", Groups: []string{"one", "two"}, }, path: "/request/path", @@ -240,6 +245,7 @@ func TestProxyHandler(t *testing.T) { "service unresolveable": { user: &user.DefaultInfo{ Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", Groups: []string{"one", "two"}, }, path: "/request/path", @@ -263,6 +269,7 @@ func TestProxyHandler(t *testing.T) { "fail on bad serving cert": { user: &user.DefaultInfo{ Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", Groups: []string{"one", "two"}, }, path: "/request/path", @@ -284,6 +291,7 @@ func TestProxyHandler(t *testing.T) { "fail on bad serving cert w/o SAN and increase SAN error counter metrics": { user: &user.DefaultInfo{ Name: "username", + UID: "6b60d791-1af9-4513-92e5-e4252a1e0a78", Groups: []string{"one", "two"}, }, path: "/request/path", @@ -425,6 +433,7 @@ func newBrokenDialerAndSelector() (*mockEgressDialer, *egressselector.EgressSele func TestProxyUpgrade(t *testing.T) { upgradeUser := "upgradeUser" + upgradeUID := "upgradeUser-UID" testcases := map[string]struct { APIService *apiregistration.APIService NewEgressSelector func() (*mockEgressDialer, *egressselector.EgressSelector) @@ -534,6 +543,10 @@ func TestProxyUpgrade(t *testing.T) { if user != upgradeUser { t.Errorf("expected user %q, got %q", upgradeUser, user) } + uid := req.Header.Get("X-Remote-Uid") + if uid != upgradeUID { + t.Errorf("expected UID %q, got %q", upgradeUID, uid) + } body := make([]byte, 5) ws.Read(body) ws.Write([]byte("hello " + string(body))) @@ -576,7 +589,7 @@ func TestProxyUpgrade(t *testing.T) { } proxyHandler.updateAPIService(tc.APIService) - aggregator := httptest.NewServer(contextHandler(proxyHandler, &user.DefaultInfo{Name: upgradeUser})) + aggregator := httptest.NewServer(contextHandler(proxyHandler, &user.DefaultInfo{Name: upgradeUser, UID: upgradeUID})) defer aggregator.Close() ws, err := websocket.Dial("ws://"+aggregator.Listener.Addr().String()+path, "", "http://127.0.0.1/") diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/status/remote/remote_available_controller.go b/staging/src/k8s.io/kube-aggregator/pkg/controllers/status/remote/remote_available_controller.go index ade744708cd..a94e254cd8f 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/controllers/status/remote/remote_available_controller.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/status/remote/remote_available_controller.go @@ -305,7 +305,7 @@ func (c *AvailableConditionController) sync(key string) error { } // setting the system-masters identity ensures that we will always have access rights - transport.SetAuthProxyHeaders(newReq, "system:kube-aggregator", []string{"system:masters"}, nil) + transport.SetAuthProxyHeaders(newReq, "system:kube-aggregator", "", []string{"system:masters"}, nil) resp, err := discoveryClient.Do(newReq) if resp != nil { resp.Body.Close() diff --git a/test/integration/auth/requestheader_test.go b/test/integration/auth/requestheader_test.go new file mode 100644 index 00000000000..65198f2bc54 --- /dev/null +++ b/test/integration/auth/requestheader_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2024 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 auth + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "os" + "testing" + "time" + + authnv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + certutil "k8s.io/client-go/util/cert" + "k8s.io/client-go/util/keyutil" + "k8s.io/kubernetes/cmd/kube-apiserver/app/options" + "k8s.io/kubernetes/test/integration/framework" + testutils "k8s.io/kubernetes/test/utils" + "k8s.io/kubernetes/test/utils/ktesting" +) + +func TestAuthnToKAS(t *testing.T) { + tCtx := ktesting.Init(t) + + frontProxyCA, frontProxyClient, frontProxyKey, err := newTestCAWithClient( + pkix.Name{ + CommonName: "test-front-proxy-ca", + }, + pkix.Name{ + CommonName: "test-aggregated-apiserver", + }, + ) + if err != nil { + t.Fatal(err) + } + + modifyOpts := func(setUIDHeaders bool) func(opts *options.ServerRunOptions) { + return func(opts *options.ServerRunOptions) { + opts.GenericServerRunOptions.MaxRequestBodyBytes = 1024 * 1024 + + // rewrite the client + request header CA certs with our own content + frontProxyCAFilename := opts.Authentication.RequestHeader.ClientCAFile + if err := os.WriteFile(frontProxyCAFilename, frontProxyCA, 0644); err != nil { + t.Fatal(err) + } + + opts.Authentication.RequestHeader.AllowedNames = append(opts.Authentication.RequestHeader.AllowedNames, "test-aggregated-apiserver") + if setUIDHeaders { + opts.Authentication.RequestHeader.UIDHeaders = []string{"X-Remote-Uid"} + } + } + } + + for _, tt := range []struct { + name string + setUID bool + }{ + { + name: "KAS without UID config", + setUID: false, + }, + { + name: "KAS with UID config", + setUID: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + _, kubeConfig, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ + ModifyServerRunOptions: modifyOpts(tt.setUID), + }) + defer tearDownFn() + + // Test an aggregated apiserver client (signed by the new front proxy CA) is authorized + extensionApiserverClient, err := kubernetes.NewForConfig(&rest.Config{ + Host: kubeConfig.Host, + TLSClientConfig: rest.TLSClientConfig{ + CAData: kubeConfig.TLSClientConfig.CAData, + CAFile: kubeConfig.TLSClientConfig.CAFile, + ServerName: kubeConfig.TLSClientConfig.ServerName, + KeyData: frontProxyKey, + CertData: frontProxyClient, + }, + }) + if err != nil { + t.Fatal(err) + } + + selfInfo := &authnv1.SelfSubjectReview{} + err = extensionApiserverClient.AuthenticationV1().RESTClient(). + Post(). + Resource("selfsubjectreviews"). + VersionedParams(&metav1.CreateOptions{}, scheme.ParameterCodec). + Body(&authnv1.SelfSubjectReview{}). + SetHeader("X-Remote-Uid", "test-uid"). + SetHeader("X-Remote-User", "testuser"). + SetHeader("X-Remote-Groups", "group1", "group2"). + Do(tCtx). + Into(selfInfo) + if err != nil { + t.Fatalf("failed to retrieve self-info: %v", err) + } + + if selfUID := selfInfo.Status.UserInfo.UID; (len(selfUID) != 0) != tt.setUID { + t.Errorf("UID should be set: %v, but we got %v", tt.setUID, selfUID) + } + }) + } + +} + +func newTestCAWithClient(caSubject pkix.Name, clientSubject pkix.Name) (caPEMBytes, clientCertPEMBytes, clientKeyPEMBytes []byte, err error) { + caPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, nil, err + } + + newCA, err := certutil.NewSelfSignedCACert(certutil.Config{ + CommonName: caSubject.CommonName, + Organization: caSubject.Organization, + NotBefore: time.Now().Add(-time.Minute), + }, caPrivateKey) + + if err != nil { + return nil, nil, nil, err + } + + clientCertPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, nil, err + } + + clientCertPrivateKeyPEM, err := keyutil.MarshalPrivateKeyToPEM(clientCertPrivateKey) + if err != nil { + return nil, nil, nil, err + } + + clientCert, err := testutils.NewSignedCert(&certutil.Config{ + CommonName: clientSubject.CommonName, + Organization: clientSubject.Organization, + NotBefore: time.Now().Add(-time.Minute), + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }, clientCertPrivateKey, newCA, caPrivateKey) + + if err != nil { + return nil, nil, nil, err + } + + caPEMBytes = testutils.EncodeCertPEM(newCA) + clientCertPEMBytes = testutils.EncodeCertPEM(clientCert) + return caPEMBytes, + clientCertPEMBytes, + clientCertPrivateKeyPEM, + nil +} diff --git a/test/integration/examples/apiserver_test.go b/test/integration/examples/apiserver_test.go index 2bbc7666a33..e5caa54dd4d 100644 --- a/test/integration/examples/apiserver_test.go +++ b/test/integration/examples/apiserver_test.go @@ -29,16 +29,22 @@ import ( "reflect" "sort" "strings" + "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" + v1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/server/dynamiccertificates" genericapiserveroptions "k8s.io/apiserver/pkg/server/options" utilversion "k8s.io/apiserver/pkg/util/version" @@ -46,6 +52,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" "k8s.io/client-go/util/cert" "k8s.io/component-base/featuregate" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" @@ -248,6 +255,143 @@ func TestAggregatedAPIServer(t *testing.T) { }) } +func TestFrontProxyConfig(t *testing.T) { + t.Run("WithoutUID", func(t *testing.T) { + testFrontProxyConfig(t, false) + }) + t.Run("WithUID", func(t *testing.T) { + testFrontProxyConfig(t, true) + }) +} + +// TestFrontProxyConfig tests that the RequestHeader configuration is consumed +// correctly by the aggregated API servers. +func testFrontProxyConfig(t *testing.T, withUID bool) { + const testNamespace = "integration-test-front-proxy-config" + const wardleBinaryVersion = "1.1" + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + var extraKASFlags []string + if withUID { + extraKASFlags = []string{"--requestheader-uid-headers=x-remote-uid"} + } + + // each wardle binary is bundled with a specific kube binary. + kubeBinaryVersion := sampleserver.WardleVersionToKubeVersion(version.MustParse(wardleBinaryVersion)).String() + + // start up the KAS and prepare the options for the wardle API server + testKAS, wardleOptions, wardlePort := prepareAggregatedWardleAPIServer(ctx, t, testNamespace, kubeBinaryVersion, wardleBinaryVersion, extraKASFlags) + kubeConfig := getKubeConfig(testKAS) + + // create the SA that we will use to query the aggregated API + kubeClient := client.NewForConfigOrDie(kubeConfig) + expectedSA, err := kubeClient.CoreV1().ServiceAccounts(testNamespace).Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wardle-client-sa", + }, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + saTokenReq, err := kubeClient.CoreV1().ServiceAccounts(testNamespace).CreateToken(ctx, "wardle-client-sa", &v1.TokenRequest{}, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + saToken := saTokenReq.Status.Token + if len(saToken) == 0 { + t.Fatal("empty SA token in token request response") + } + + saClientConfig := rest.AnonymousClientConfig(kubeConfig) + saClientConfig.BearerToken = saToken + + saKubeClient := client.NewForConfigOrDie(saClientConfig) + saDetails, err := saKubeClient.AuthenticationV1().SelfSubjectReviews().Create(ctx, &v1.SelfSubjectReview{}, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to retrieve details about the SA: %v", err) + } + + saUserInfo := serviceaccount.UserInfo(expectedSA.Namespace, expectedSA.Name, string(expectedSA.UID)) + expectedSAUserInfo := user.DefaultInfo{ + Name: saUserInfo.GetName(), + Groups: append(saUserInfo.GetGroups(), user.AllAuthenticated), + Extra: saUserInfo.GetExtra(), + } + if withUID { + expectedSAUserInfo.UID = saUserInfo.GetUID() + } + + if expectedSAUserInfo.Extra == nil { + expectedSAUserInfo.Extra = map[string][]string{} + } + expectedSAUserInfo.Extra[user.CredentialIDKey] = saDetails.Status.UserInfo.Extra[user.CredentialIDKey] + + var checksProcessed atomic.Uint32 + + // wrap the authz round tripper to catch the request for our SA SAR to the KAS + wardleOptions.RecommendedOptions.Authorization.WithCustomRoundTripper( + // adding a round tripper wrapper to test default RequestHeader configuration + transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper { + return roundTripperFunc(func(req *http.Request) (*http.Response, error) { + gotUser, ok := genericapirequest.UserFrom(req.Context()) + if !ok || gotUser.GetName() == "system:anonymous" { + return nil, fmt.Errorf("got an unauthenticated request") + } + + // this is likely the KAS checking the OpenAPI endpoints + if gotUser.GetName() == "system:anonymous" || gotUser.GetName() == "system:aggregator" { + return rt.RoundTrip(req) + } + + if got, expected := gotUser.GetUID(), expectedSAUserInfo.GetUID(); expected != got { + t.Errorf("expected UID: %q, got: %q", expected, got) + } + if got, expected := gotUser.GetName(), expectedSAUserInfo.GetName(); expected != got { + t.Errorf("expected name: %q, got: %q", expected, got) + } + if got, expected := gotUser.GetGroups(), expectedSAUserInfo.GetGroups(); !reflect.DeepEqual(expected, got) { + t.Errorf("expected groups: %v, got: %v", expected, got) + } + if got, expected := gotUser.GetExtra(), expectedSAUserInfo.GetExtra(); !apiequality.Semantic.DeepEqual(expected, got) { + t.Errorf("expected extra to be %v, but got %v", expected, got) + } + + checksProcessed.Add(1) + return rt.RoundTrip(req) + }) + }), + ) + + wardleCertDir, _ := os.MkdirTemp("", "test-integration-wardle-server") + defer os.RemoveAll(wardleCertDir) + + runPreparedWardleServer(ctx, t, wardleOptions, wardleCertDir, wardlePort, false, true, wardleBinaryVersion, kubeConfig) + waitForWardleAPIServiceReady(ctx, t, kubeConfig, wardleCertDir, testNamespace) + + // get the wardle API client using our SA token + wardleClientConfig := rest.AnonymousClientConfig(kubeConfig) + wardleClientConfig.BearerToken = saToken + wardleClient := wardlev1alpha1client.NewForConfigOrDie(wardleClientConfig) + + _, err = wardleClient.Flunders(metav1.NamespaceSystem).List(ctx, metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + + if checksProcessed.Load() != 1 { + t.Errorf("the request is in fact not being tested") + } +} + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + func testAggregatedAPIServer(t *testing.T, setWardleFeatureGate, banFlunder bool, wardleBinaryVersion, wardleEmulationVersion string) { const testNamespace = "kube-wardle" @@ -257,7 +401,7 @@ func testAggregatedAPIServer(t *testing.T, setWardleFeatureGate, banFlunder bool // each wardle binary is bundled with a specific kube binary. kubeBinaryVersion := sampleserver.WardleVersionToKubeVersion(version.MustParse(wardleBinaryVersion)).String() - testKAS, wardleOptions, wardlePort := prepareAggregatedWardleAPIServer(ctx, t, testNamespace, kubeBinaryVersion, wardleBinaryVersion) + testKAS, wardleOptions, wardlePort := prepareAggregatedWardleAPIServer(ctx, t, testNamespace, kubeBinaryVersion, wardleBinaryVersion, nil) kubeClientConfig := getKubeConfig(testKAS) wardleCertDir, _ := os.MkdirTemp("", "test-integration-wardle-server") @@ -537,7 +681,7 @@ func TestAggregatedAPIServerRejectRedirectResponse(t *testing.T) { } } -func prepareAggregatedWardleAPIServer(ctx context.Context, t *testing.T, namespace, kubebinaryVersion, wardleBinaryVersion string) (*kastesting.TestServer, *sampleserver.WardleServerOptions, int) { +func prepareAggregatedWardleAPIServer(ctx context.Context, t *testing.T, namespace, kubebinaryVersion, wardleBinaryVersion string, kubeAPIServerFlags []string) (*kastesting.TestServer, *sampleserver.WardleServerOptions, int) { // makes the kube-apiserver very responsive. it's normally a minute dynamiccertificates.FileRefreshDuration = 1 * time.Second @@ -549,7 +693,13 @@ func prepareAggregatedWardleAPIServer(ctx context.Context, t *testing.T, namespa // endpoints cannot have loopback IPs so we need to override the resolver itself t.Cleanup(app.SetServiceResolverForTests(staticURLServiceResolver(fmt.Sprintf("https://127.0.0.1:%d", wardlePort)))) - testServer := kastesting.StartTestServerOrDie(t, &kastesting.TestServerInstanceOptions{EnableCertAuth: true, BinaryVersion: kubebinaryVersion}, nil, framework.SharedEtcd()) + testServer := kastesting.StartTestServerOrDie(t, + &kastesting.TestServerInstanceOptions{ + EnableCertAuth: true, + BinaryVersion: kubebinaryVersion, + }, + kubeAPIServerFlags, + framework.SharedEtcd()) t.Cleanup(func() { testServer.TearDownFn() }) _, _ = utilversion.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister(