diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index b0ab7d718b..33d1fdc6f9 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -232,6 +232,7 @@ func Run(s *options.APIServer) error { KeystoneURL: s.KeystoneURL, WebhookTokenAuthnConfigFile: s.WebhookTokenAuthnConfigFile, WebhookTokenAuthnCacheTTL: s.WebhookTokenAuthnCacheTTL, + RequestHeaderConfig: s.AuthenticationRequestHeaderConfig(), }) if err != nil { diff --git a/federation/cmd/federation-apiserver/app/server.go b/federation/cmd/federation-apiserver/app/server.go index 9c287ff405..1b8806b9dd 100644 --- a/federation/cmd/federation-apiserver/app/server.go +++ b/federation/cmd/federation-apiserver/app/server.go @@ -117,17 +117,18 @@ func Run(s *options.ServerRunOptions) error { } apiAuthenticator, securityDefinitions, err := authenticator.New(authenticator.AuthenticatorConfig{ - Anonymous: s.AnonymousAuth, - AnyToken: s.EnableAnyToken, - BasicAuthFile: s.BasicAuthFile, - ClientCAFile: s.ClientCAFile, - TokenAuthFile: s.TokenAuthFile, - OIDCIssuerURL: s.OIDCIssuerURL, - OIDCClientID: s.OIDCClientID, - OIDCCAFile: s.OIDCCAFile, - OIDCUsernameClaim: s.OIDCUsernameClaim, - OIDCGroupsClaim: s.OIDCGroupsClaim, - KeystoneURL: s.KeystoneURL, + Anonymous: s.AnonymousAuth, + AnyToken: s.EnableAnyToken, + BasicAuthFile: s.BasicAuthFile, + ClientCAFile: s.ClientCAFile, + TokenAuthFile: s.TokenAuthFile, + OIDCIssuerURL: s.OIDCIssuerURL, + OIDCClientID: s.OIDCClientID, + OIDCCAFile: s.OIDCCAFile, + OIDCUsernameClaim: s.OIDCUsernameClaim, + OIDCGroupsClaim: s.OIDCGroupsClaim, + KeystoneURL: s.KeystoneURL, + RequestHeaderConfig: s.AuthenticationRequestHeaderConfig(), }) if err != nil { glog.Fatalf("Invalid Authentication Config: %v", err) diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index f1a63c9bb1..e73dec1d75 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -464,6 +464,9 @@ replication-controller-lookup-cache-size repo-root report-dir report-prefix +requestheader-allowed-names +requestheader-client-ca-file +requestheader-username-headers require-kubeconfig required-contexts resolv-conf diff --git a/pkg/apiserver/authenticator/BUILD b/pkg/apiserver/authenticator/BUILD index c8b90f65c3..79517cdd26 100644 --- a/pkg/apiserver/authenticator/BUILD +++ b/pkg/apiserver/authenticator/BUILD @@ -25,6 +25,7 @@ go_library( "//plugin/pkg/auth/authenticator/password/passwordfile:go_default_library", "//plugin/pkg/auth/authenticator/request/anonymous:go_default_library", "//plugin/pkg/auth/authenticator/request/basicauth:go_default_library", + "//plugin/pkg/auth/authenticator/request/headerrequest:go_default_library", "//plugin/pkg/auth/authenticator/request/union:go_default_library", "//plugin/pkg/auth/authenticator/request/x509:go_default_library", "//plugin/pkg/auth/authenticator/token/anytoken:go_default_library", diff --git a/pkg/apiserver/authenticator/authn.go b/pkg/apiserver/authenticator/authn.go index 2dfe01acd7..6d5720077e 100644 --- a/pkg/apiserver/authenticator/authn.go +++ b/pkg/apiserver/authenticator/authn.go @@ -31,6 +31,7 @@ import ( "k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/anonymous" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/basicauth" + "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/headerrequest" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/union" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/anytoken" @@ -39,6 +40,15 @@ import ( "k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/webhook" ) +type RequestHeaderConfig struct { + // UsernameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins. + UsernameHeaders []string + // ClientCA points to CA bundle file which is used verify the identity of the front proxy + ClientCA string + // AllowedClientNames is a list of common names that may be presented by the authenticating front proxy. Empty means: accept any. + AllowedClientNames []string +} + type AuthenticatorConfig struct { Anonymous bool AnyToken bool @@ -56,6 +66,8 @@ type AuthenticatorConfig struct { KeystoneURL string WebhookTokenAuthnConfigFile string WebhookTokenAuthnCacheTTL time.Duration + + RequestHeaderConfig *RequestHeaderConfig } // New returns an authenticator.Request or an error that supports the standard @@ -66,7 +78,20 @@ func New(config AuthenticatorConfig) (authenticator.Request, *spec.SecurityDefin hasBasicAuth := false hasTokenAuth := false - // BasicAuth methods, local first, then remote + // front-proxy, BasicAuth methods, local first, then remote + // Add the front proxy authenticator if requested + if config.RequestHeaderConfig != nil { + requestHeaderAuthenticator, err := headerrequest.NewSecure( + config.RequestHeaderConfig.ClientCA, + config.RequestHeaderConfig.AllowedClientNames, + config.RequestHeaderConfig.UsernameHeaders, + ) + if err != nil { + return nil, nil, err + } + authenticators = append(authenticators, requestHeaderAuthenticator) + } + if len(config.BasicAuthFile) > 0 { basicAuth, err := newAuthenticatorFromBasicAuthFile(config.BasicAuthFile) if err != nil { diff --git a/pkg/genericapiserver/options/BUILD b/pkg/genericapiserver/options/BUILD index eebe8f8b90..ee766a9e78 100644 --- a/pkg/genericapiserver/options/BUILD +++ b/pkg/genericapiserver/options/BUILD @@ -13,6 +13,7 @@ load( go_library( name = "go_default_library", srcs = [ + "authenticator.go", "doc.go", "etcd_options.go", "server_run_options.go", @@ -23,6 +24,7 @@ go_library( "//pkg/api:go_default_library", "//pkg/api/unversioned:go_default_library", "//pkg/apimachinery/registered:go_default_library", + "//pkg/apiserver/authenticator:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library", "//pkg/client/restclient:go_default_library", "//pkg/storage/storagebackend:go_default_library", diff --git a/pkg/genericapiserver/options/authenticator.go b/pkg/genericapiserver/options/authenticator.go new file mode 100644 index 0000000000..58f943050e --- /dev/null +++ b/pkg/genericapiserver/options/authenticator.go @@ -0,0 +1,35 @@ +/* +Copyright 2016 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 options + +import ( + "k8s.io/kubernetes/pkg/apiserver/authenticator" +) + +// AuthenticationRequestHeaderConfig returns an authenticator config object for these options +// if necessary. nil otherwise. +func (s *ServerRunOptions) AuthenticationRequestHeaderConfig() *authenticator.RequestHeaderConfig { + if len(s.RequestHeaderUsernameHeaders) == 0 { + return nil + } + + return &authenticator.RequestHeaderConfig{ + UsernameHeaders: s.RequestHeaderUsernameHeaders, + ClientCA: s.RequestHeaderClientCAFile, + AllowedClientNames: s.RequestHeaderAllowedNames, + } +} diff --git a/pkg/genericapiserver/options/server_run_options.go b/pkg/genericapiserver/options/server_run_options.go index d36516a3ba..69de738403 100644 --- a/pkg/genericapiserver/options/server_run_options.go +++ b/pkg/genericapiserver/options/server_run_options.go @@ -67,46 +67,49 @@ type ServerRunOptions struct { AuthorizationWebhookCacheUnauthorizedTTL time.Duration AuthorizationRBACSuperUser string - AnonymousAuth bool - BasicAuthFile string - BindAddress net.IP - CertDirectory string - ClientCAFile string - CloudConfigFile string - CloudProvider string - CorsAllowedOriginList []string - DefaultStorageMediaType string - DeleteCollectionWorkers int - AuditLogPath string - AuditLogMaxAge int - AuditLogMaxBackups int - AuditLogMaxSize int - EnableGarbageCollection bool - EnableProfiling bool - EnableSwaggerUI bool - EnableWatchCache bool - EtcdServersOverrides []string - StorageConfig storagebackend.Config - ExternalHost string - InsecureBindAddress net.IP - InsecurePort int - KeystoneURL string - KubernetesServiceNodePort int - LongRunningRequestRE string - MasterCount int - MasterServiceNamespace string - MaxRequestsInFlight int - MinRequestTimeout int - OIDCCAFile string - OIDCClientID string - OIDCIssuerURL string - OIDCUsernameClaim string - OIDCGroupsClaim string - RuntimeConfig config.ConfigurationMap - SecurePort int - ServiceClusterIPRange net.IPNet // TODO: make this a list - ServiceNodePortRange utilnet.PortRange - StorageVersions string + AnonymousAuth bool + BasicAuthFile string + BindAddress net.IP + CertDirectory string + ClientCAFile string + CloudConfigFile string + CloudProvider string + CorsAllowedOriginList []string + DefaultStorageMediaType string + DeleteCollectionWorkers int + AuditLogPath string + AuditLogMaxAge int + AuditLogMaxBackups int + AuditLogMaxSize int + EnableGarbageCollection bool + EnableProfiling bool + EnableSwaggerUI bool + EnableWatchCache bool + EtcdServersOverrides []string + StorageConfig storagebackend.Config + ExternalHost string + InsecureBindAddress net.IP + InsecurePort int + KeystoneURL string + KubernetesServiceNodePort int + LongRunningRequestRE string + MasterCount int + MasterServiceNamespace string + MaxRequestsInFlight int + MinRequestTimeout int + OIDCCAFile string + OIDCClientID string + OIDCIssuerURL string + OIDCUsernameClaim string + OIDCGroupsClaim string + RequestHeaderUsernameHeaders []string + RequestHeaderClientCAFile string + RequestHeaderAllowedNames []string + RuntimeConfig config.ConfigurationMap + SecurePort int + ServiceClusterIPRange net.IPNet // TODO: make this a list + ServiceNodePortRange utilnet.PortRange + StorageVersions string // The default values for StorageVersions. StorageVersions overrides // these; you can change this if you want to change the defaults (e.g., // for testing). This is not actually exposed as a flag. @@ -423,6 +426,18 @@ func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) { "The claim value is expected to be a string or array of strings. This flag is experimental, "+ "please see the authentication documentation for further details.") + fs.StringSliceVar(&s.RequestHeaderUsernameHeaders, "requestheader-username-headers", s.RequestHeaderUsernameHeaders, ""+ + "List of request headers to inspect for usernames. X-Remote-User is common.") + + fs.StringVar(&s.RequestHeaderClientCAFile, "requestheader-client-ca-file", s.RequestHeaderClientCAFile, ""+ + "Root certificate bundle to use to verify client certificates on incoming requests "+ + "before trusting usernames in headers specified by --requestheader-username-headers") + + fs.StringSliceVar(&s.RequestHeaderAllowedNames, "requestheader-allowed-names", s.RequestHeaderAllowedNames, ""+ + "List of client certificate common names to allow to provide usernames in headers "+ + "specified by --requestheader-username-headers. If empty, any client certificate validated "+ + "by the authorities in --requestheader-client-ca-file is allowed.") + fs.Var(&s.RuntimeConfig, "runtime-config", ""+ "A set of key=value pairs that describe runtime configuration that may be passed "+ "to apiserver. apis/ key can be used to turn on/off specific api versions. "+ diff --git a/plugin/pkg/auth/authenticator/request/headerrequest/BUILD b/plugin/pkg/auth/authenticator/request/headerrequest/BUILD new file mode 100644 index 0000000000..0d7dfd2b31 --- /dev/null +++ b/plugin/pkg/auth/authenticator/request/headerrequest/BUILD @@ -0,0 +1,32 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_binary", + "go_library", + "go_test", + "cgo_library", +) + +go_library( + name = "go_default_library", + srcs = ["requestheader.go"], + tags = ["automanaged"], + deps = [ + "//pkg/auth/authenticator:go_default_library", + "//pkg/auth/user:go_default_library", + "//pkg/util/cert:go_default_library", + "//pkg/util/sets:go_default_library", + "//plugin/pkg/auth/authenticator/request/x509:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["requestheader_test.go"], + library = "go_default_library", + tags = ["automanaged"], + deps = ["//pkg/auth/user:go_default_library"], +) diff --git a/plugin/pkg/auth/authenticator/request/headerrequest/requestheader.go b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader.go new file mode 100644 index 0000000000..cd704fbdfe --- /dev/null +++ b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader.go @@ -0,0 +1,96 @@ +/* +Copyright 2016 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 headerrequest + +import ( + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "k8s.io/kubernetes/pkg/auth/authenticator" + "k8s.io/kubernetes/pkg/auth/user" + utilcert "k8s.io/kubernetes/pkg/util/cert" + "k8s.io/kubernetes/pkg/util/sets" + x509request "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509" +) + +type requestHeaderAuthRequestHandler struct { + // nameHeaders are the headers to check (in order, case-insensitively) for an identity. The first header with a value wins. + nameHeaders []string +} + +func New(nameHeaders []string) (authenticator.Request, error) { + headers := []string{} + for _, headerName := range nameHeaders { + trimmedHeader := strings.TrimSpace(headerName) + if len(trimmedHeader) == 0 { + return nil, fmt.Errorf("empty header %q", headerName) + } + headers = append(headers, trimmedHeader) + } + + return &requestHeaderAuthRequestHandler{nameHeaders: headers}, nil +} + +func NewSecure(clientCA string, proxyClientNames []string, nameHeaders []string) (authenticator.Request, error) { + headerAuthenticator, err := New(nameHeaders) + if err != nil { + return nil, err + } + + if len(clientCA) == 0 { + return nil, fmt.Errorf("missing clientCA file") + } + + // Wrap with an x509 verifier + caData, err := ioutil.ReadFile(clientCA) + if err != nil { + return nil, fmt.Errorf("error reading %s: %v", clientCA, err) + } + opts := x509request.DefaultVerifyOptions() + opts.Roots = x509.NewCertPool() + certs, err := utilcert.ParseCertsPEM(caData) + if err != nil { + return nil, fmt.Errorf("error loading certs from %s: %v", clientCA, err) + } + for _, cert := range certs { + opts.Roots.AddCert(cert) + } + + return x509request.NewVerifier(opts, headerAuthenticator, sets.NewString(proxyClientNames...)), nil +} + +func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + name := headerValue(req.Header, a.nameHeaders) + if len(name) == 0 { + return nil, false, nil + } + + return &user.DefaultInfo{Name: name}, true, nil +} + +func headerValue(h http.Header, headerNames []string) string { + for _, headerName := range headerNames { + headerValue := h.Get(headerName) + if len(headerValue) > 0 { + return headerValue + } + } + return "" +} diff --git a/plugin/pkg/auth/authenticator/request/headerrequest/requestheader_test.go b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader_test.go new file mode 100644 index 0000000000..75402ab64e --- /dev/null +++ b/plugin/pkg/auth/authenticator/request/headerrequest/requestheader_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2016 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 headerrequest + +import ( + "net/http" + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/auth/user" +) + +func TestRequestHeader(t *testing.T) { + testcases := map[string]struct { + nameHeaders []string + requestHeaders http.Header + + expectedUser user.Info + expectedOk bool + }{ + "empty": {}, + "no match": { + nameHeaders: []string{"X-Remote-User"}, + }, + "match": { + nameHeaders: []string{"X-Remote-User"}, + requestHeaders: http.Header{"X-Remote-User": {"Bob"}}, + expectedUser: &user.DefaultInfo{Name: "Bob"}, + expectedOk: true, + }, + "exact match": { + nameHeaders: []string{"X-Remote-User"}, + requestHeaders: http.Header{ + "Prefixed-X-Remote-User-With-Suffix": {"Bob"}, + "X-Remote-User-With-Suffix": {"Bob"}, + }, + }, + "first match": { + nameHeaders: []string{ + "X-Remote-User", + "A-Second-X-Remote-User", + "Another-X-Remote-User", + }, + requestHeaders: http.Header{ + "X-Remote-User": {"", "First header, second value"}, + "A-Second-X-Remote-User": {"Second header, first value", "Second header, second value"}, + "Another-X-Remote-User": {"Third header, first value"}}, + expectedUser: &user.DefaultInfo{Name: "Second header, first value"}, + expectedOk: true, + }, + "case-insensitive": { + nameHeaders: []string{"x-REMOTE-user"}, // configured headers can be case-insensitive + requestHeaders: http.Header{"X-Remote-User": {"Bob"}}, // the parsed headers are normalized by the http package + expectedUser: &user.DefaultInfo{Name: "Bob"}, + expectedOk: true, + }, + } + + for k, testcase := range testcases { + auth, err := New(testcase.nameHeaders) + if err != nil { + t.Fatal(err) + } + req := &http.Request{Header: testcase.requestHeaders} + + user, ok, _ := auth.AuthenticateRequest(req) + if testcase.expectedOk != ok { + t.Errorf("%v: expected %v, got %v", k, testcase.expectedOk, ok) + } + if e, a := testcase.expectedUser, user; !reflect.DeepEqual(e, a) { + t.Errorf("%v: expected %#v, got %#v", k, e, a) + + } + } +} diff --git a/plugin/pkg/auth/authenticator/request/x509/BUILD b/plugin/pkg/auth/authenticator/request/x509/BUILD index c78af13cb8..dad4d99367 100644 --- a/plugin/pkg/auth/authenticator/request/x509/BUILD +++ b/plugin/pkg/auth/authenticator/request/x509/BUILD @@ -18,8 +18,11 @@ go_library( ], tags = ["automanaged"], deps = [ + "//pkg/auth/authenticator:go_default_library", "//pkg/auth/user:go_default_library", "//pkg/util/errors:go_default_library", + "//pkg/util/sets:go_default_library", + "//vendor:github.com/golang/glog", ], ) @@ -29,5 +32,9 @@ go_test( data = glob(["testdata/*"]), library = "go_default_library", tags = ["automanaged"], - deps = ["//pkg/auth/user:go_default_library"], + deps = [ + "//pkg/auth/authenticator:go_default_library", + "//pkg/auth/user:go_default_library", + "//pkg/util/sets:go_default_library", + ], ) diff --git a/plugin/pkg/auth/authenticator/request/x509/x509.go b/plugin/pkg/auth/authenticator/request/x509/x509.go index 42526fd2c5..223799cc31 100644 --- a/plugin/pkg/auth/authenticator/request/x509/x509.go +++ b/plugin/pkg/auth/authenticator/request/x509/x509.go @@ -18,11 +18,17 @@ package x509 import ( "crypto/x509" + "crypto/x509/pkix" "encoding/asn1" + "fmt" "net/http" + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/user" utilerrors "k8s.io/kubernetes/pkg/util/errors" + "k8s.io/kubernetes/pkg/util/sets" ) // UserConversion defines an interface for extracting user info from a client certificate chain @@ -85,6 +91,58 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, return nil, false, utilerrors.NewAggregate(errlist) } +// Verifier implements request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth +type Verifier struct { + opts x509.VerifyOptions + auth authenticator.Request + + // allowedCommonNames contains the common names which a verified certificate is allowed to have. + // If empty, all verified certificates are allowed. + allowedCommonNames sets.String +} + +// NewVerifier create a request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth +func NewVerifier(opts x509.VerifyOptions, auth authenticator.Request, allowedCommonNames sets.String) authenticator.Request { + return &Verifier{opts, auth, allowedCommonNames} +} + +// AuthenticateRequest verifies the presented client certificate, then delegates to the wrapped auth +func (a *Verifier) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 { + return nil, false, nil + } + + // Use intermediates, if provided + optsCopy := a.opts + if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 { + optsCopy.Intermediates = x509.NewCertPool() + for _, intermediate := range req.TLS.PeerCertificates[1:] { + optsCopy.Intermediates.AddCert(intermediate) + } + } + + if _, err := req.TLS.PeerCertificates[0].Verify(optsCopy); err != nil { + return nil, false, err + } + if err := a.verifySubject(req.TLS.PeerCertificates[0].Subject); err != nil { + return nil, false, err + } + return a.auth.AuthenticateRequest(req) +} + +func (a *Verifier) verifySubject(subject pkix.Name) error { + // No CN restrictions + if len(a.allowedCommonNames) == 0 { + return nil + } + // Enforce CN restrictions + if a.allowedCommonNames.Has(subject.CommonName) { + return nil + } + glog.Warningf("x509: subject with cn=%s is not in the allowed list: %v", subject.CommonName, a.allowedCommonNames.List()) + return fmt.Errorf("x509: subject with cn=%s is not allowed", subject.CommonName) +} + // DefaultVerifyOptions returns VerifyOptions that use the system root certificates, current time, // and requires certificates to be valid for client auth (x509.ExtKeyUsageClientAuth) func DefaultVerifyOptions() x509.VerifyOptions { diff --git a/plugin/pkg/auth/authenticator/request/x509/x509_test.go b/plugin/pkg/auth/authenticator/request/x509/x509_test.go index fe143bce15..12a8e99e0d 100644 --- a/plugin/pkg/auth/authenticator/request/x509/x509_test.go +++ b/plugin/pkg/auth/authenticator/request/x509/x509_test.go @@ -28,7 +28,9 @@ import ( "testing" "time" + "k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/util/sets" ) const ( @@ -373,12 +375,12 @@ mFlG6tStAWz3TmydciZNdiEbeqHw5uaIYWj1zC5AdvFXBFue0ojIrJ5JtbTWccH9 ` /* - openssl genrsa -out ca.key 4096 - openssl req -new -x509 -days 36500 \ - -sha256 -key ca.key -extensions v3_ca \ - -out ca.crt \ - -subj "/C=US/ST=My State/L=My City/O=My Org/O=My Org 1/O=My Org 2/CN=ROOT CA WITH GROUPS" - openssl x509 -in ca.crt -text + openssl genrsa -out ca.key 4096 + openssl req -new -x509 -days 36500 \ + -sha256 -key ca.key -extensions v3_ca \ + -out ca.crt \ + -subj "/C=US/ST=My State/L=My City/O=My Org/O=My Org 1/O=My Org 2/CN=ROOT CA WITH GROUPS" + openssl x509 -in ca.crt -text */ // A certificate with multiple organizations. @@ -723,6 +725,164 @@ func TestX509(t *testing.T) { } } +func TestX509Verifier(t *testing.T) { + multilevelOpts := DefaultVerifyOptions() + multilevelOpts.Roots = x509.NewCertPool() + multilevelOpts.Roots.AddCert(getCertsFromFile(t, "root")[0]) + + testCases := map[string]struct { + Insecure bool + Certs []*x509.Certificate + + Opts x509.VerifyOptions + + AllowedCNs sets.String + + ExpectOK bool + ExpectErr bool + }{ + "non-tls": { + Insecure: true, + + ExpectOK: false, + ExpectErr: false, + }, + + "tls, no certs": { + ExpectOK: false, + ExpectErr: false, + }, + + "self signed": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, selfSignedCert), + + ExpectErr: true, + }, + + "server cert disallowed": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, serverCert), + + ExpectErr: true, + }, + "server cert allowing non-client cert usages": { + Opts: x509.VerifyOptions{Roots: getRootCertPool(t)}, + Certs: getCerts(t, serverCert), + + ExpectOK: true, + ExpectErr: false, + }, + + "valid client cert": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + + ExpectOK: true, + ExpectErr: false, + }, + "valid client cert with wrong CN": { + Opts: getDefaultVerifyOptions(t), + AllowedCNs: sets.NewString("foo", "bar"), + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + "valid client cert with right CN": { + Opts: getDefaultVerifyOptions(t), + AllowedCNs: sets.NewString("client_cn"), + Certs: getCerts(t, clientCNCert), + + ExpectOK: true, + ExpectErr: false, + }, + + "future cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(-100 * time.Hour * 24 * 365), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + "expired cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(100 * time.Hour * 24 * 365), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + + "multi-level, valid": { + Opts: multilevelOpts, + Certs: getCertsFromFile(t, "client-valid", "intermediate"), + + ExpectOK: true, + ExpectErr: false, + }, + "multi-level, expired": { + Opts: multilevelOpts, + Certs: getCertsFromFile(t, "client-expired", "intermediate"), + + ExpectOK: false, + ExpectErr: true, + }, + } + + for k, testCase := range testCases { + req, _ := http.NewRequest("GET", "/", nil) + if !testCase.Insecure { + req.TLS = &tls.ConnectionState{PeerCertificates: testCase.Certs} + } + + authCall := false + auth := authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + authCall = true + return &user.DefaultInfo{Name: "innerauth"}, true, nil + }) + + a := NewVerifier(testCase.Opts, auth, testCase.AllowedCNs) + + user, ok, err := a.AuthenticateRequest(req) + + if testCase.ExpectErr && err == nil { + t.Errorf("%s: Expected error, got none", k) + continue + } + if !testCase.ExpectErr && err != nil { + t.Errorf("%s: Got unexpected error: %v", k, err) + continue + } + + if testCase.ExpectOK != ok { + t.Errorf("%s: Expected ok=%v, got %v", k, testCase.ExpectOK, ok) + continue + } + + if testCase.ExpectOK { + if !authCall { + t.Errorf("%s: Expected inner auth called, wasn't", k) + continue + } + if "innerauth" != user.GetName() { + t.Errorf("%s: Expected user.name=%v, got %v", k, "innerauth", user.GetName()) + continue + } + } else { + if authCall { + t.Errorf("%s: Expected inner auth not to be called, was", k) + continue + } + } + } +} + func getDefaultVerifyOptions(t *testing.T) x509.VerifyOptions { options := DefaultVerifyOptions() options.Roots = getRootCertPool(t) diff --git a/test/test_owners.csv b/test/test_owners.csv index 3439773c5c..c14e20d90a 100644 --- a/test/test_owners.csv +++ b/test/test_owners.csv @@ -844,6 +844,7 @@ k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/allow,liggitt,0 k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile,liggitt,0 k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/anonymous,justinsb,1 k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/basicauth,liggitt,0 +k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/headerrequest,deads2k,0 k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/union,liggitt,0 k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509,liggitt,0 k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/anytoken,krousey,1