diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 2a7b0364d09..bb4118c26fe 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -201,6 +201,7 @@ func Run(s *options.APIServer) error { } apiAuthenticator, err := authenticator.New(authenticator.AuthenticatorConfig{ + Anonymous: s.AnonymousAuth, BasicAuthFile: s.BasicAuthFile, ClientCAFile: s.ClientCAFile, TokenAuthFile: s.TokenAuthFile, diff --git a/federation/cmd/federation-apiserver/app/server.go b/federation/cmd/federation-apiserver/app/server.go index 44d92d60d7d..ff7eaca86ea 100644 --- a/federation/cmd/federation-apiserver/app/server.go +++ b/federation/cmd/federation-apiserver/app/server.go @@ -115,6 +115,7 @@ func Run(s *options.ServerRunOptions) error { } apiAuthenticator, err := authenticator.New(authenticator.AuthenticatorConfig{ + Anonymous: s.AnonymousAuth, BasicAuthFile: s.BasicAuthFile, ClientCAFile: s.ClientCAFile, TokenAuthFile: s.TokenAuthFile, diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index fc1aa5872db..2e78575917f 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -9,6 +9,7 @@ all-namespaces allocate-node-cidrs allow-privileged allowed-not-ready-nodes +anonymous-auth api-advertise-addresses api-external-dns-names api-burst diff --git a/pkg/apiserver/authenticator/authn.go b/pkg/apiserver/authenticator/authn.go index 1a738308873..6978eb60839 100644 --- a/pkg/apiserver/authenticator/authn.go +++ b/pkg/apiserver/authenticator/authn.go @@ -22,11 +22,13 @@ import ( "k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/authenticator/bearertoken" + "k8s.io/kubernetes/pkg/auth/group" "k8s.io/kubernetes/pkg/auth/user" "k8s.io/kubernetes/pkg/serviceaccount" certutil "k8s.io/kubernetes/pkg/util/cert" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/keystone" "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/union" "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509" @@ -36,6 +38,7 @@ import ( ) type AuthenticatorConfig struct { + Anonymous bool BasicAuthFile string ClientCAFile string TokenAuthFile string @@ -57,6 +60,7 @@ type AuthenticatorConfig struct { func New(config AuthenticatorConfig) (authenticator.Request, error) { var authenticators []authenticator.Request + // BasicAuth methods, local first, then remote if len(config.BasicAuthFile) > 0 { basicAuth, err := newAuthenticatorFromBasicAuthFile(config.BasicAuthFile) if err != nil { @@ -64,7 +68,15 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { } authenticators = append(authenticators, basicAuth) } + if len(config.KeystoneURL) > 0 { + keystoneAuth, err := newAuthenticatorFromKeystoneURL(config.KeystoneURL) + if err != nil { + return nil, err + } + authenticators = append(authenticators, keystoneAuth) + } + // X509 methods if len(config.ClientCAFile) > 0 { certAuth, err := newAuthenticatorFromClientCAFile(config.ClientCAFile) if err != nil { @@ -73,6 +85,7 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { authenticators = append(authenticators, certAuth) } + // Bearer token methods, local first, then remote if len(config.TokenAuthFile) > 0 { tokenAuth, err := newAuthenticatorFromTokenFile(config.TokenAuthFile) if err != nil { @@ -80,7 +93,6 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { } authenticators = append(authenticators, tokenAuth) } - if len(config.ServiceAccountKeyFile) > 0 { serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountKeyFile, config.ServiceAccountLookup, config.ServiceAccountTokenGetter) if err != nil { @@ -88,7 +100,6 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { } authenticators = append(authenticators, serviceAccountAuth) } - // NOTE(ericchiang): Keep the OpenID Connect after Service Accounts. // // Because both plugins verify JWTs whichever comes first in the union experiences @@ -102,15 +113,6 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { } authenticators = append(authenticators, oidcAuth) } - - if len(config.KeystoneURL) > 0 { - keystoneAuth, err := newAuthenticatorFromKeystoneURL(config.KeystoneURL) - if err != nil { - return nil, err - } - authenticators = append(authenticators, keystoneAuth) - } - if len(config.WebhookTokenAuthnConfigFile) > 0 { webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL) if err != nil { @@ -119,14 +121,23 @@ func New(config AuthenticatorConfig) (authenticator.Request, error) { authenticators = append(authenticators, webhookTokenAuth) } - switch len(authenticators) { - case 0: + if len(authenticators) == 0 { + if config.Anonymous { + return anonymous.NewAuthenticator(), nil + } return nil, nil - case 1: - return authenticators[0], nil - default: - return union.New(authenticators...), nil } + + authenticator := union.New(authenticators...) + + authenticator = group.NewGroupAdder(authenticator, []string{"system:authenticated"}) + + if config.Anonymous { + // If the authenticator chain returns an error, return an error (don't consider a bad bearer token anonymous). + authenticator = union.NewFailOnError(authenticator, anonymous.NewAuthenticator()) + } + + return authenticator, nil } // IsValidServiceAccountKeyFile returns true if a valid public RSA key can be read from the given file diff --git a/pkg/auth/group/group_adder.go b/pkg/auth/group/group_adder.go new file mode 100644 index 00000000000..7a15941a94d --- /dev/null +++ b/pkg/auth/group/group_adder.go @@ -0,0 +1,50 @@ +/* +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 group + +import ( + "net/http" + + "k8s.io/kubernetes/pkg/auth/authenticator" + "k8s.io/kubernetes/pkg/auth/user" +) + +// GroupAdder adds groups to an authenticated user.Info +type GroupAdder struct { + // Authenticator is delegated to make the authentication decision + Authenticator authenticator.Request + // Groups are additional groups to add to the user.Info from a successful authentication + Groups []string +} + +// NewGroupAdder wraps a request authenticator, and adds the specified groups to the returned user when authentication succeeds +func NewGroupAdder(auth authenticator.Request, groups []string) authenticator.Request { + return &GroupAdder{auth, groups} +} + +func (g *GroupAdder) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + u, ok, err := g.Authenticator.AuthenticateRequest(req) + if err != nil || !ok { + return nil, ok, err + } + return &user.DefaultInfo{ + Name: u.GetName(), + UID: u.GetUID(), + Groups: append(u.GetGroups(), g.Groups...), + Extra: u.GetExtra(), + }, true, nil +} diff --git a/pkg/auth/group/group_adder_test.go b/pkg/auth/group/group_adder_test.go new file mode 100644 index 00000000000..4eec6c0b3bf --- /dev/null +++ b/pkg/auth/group/group_adder_test.go @@ -0,0 +1,42 @@ +/* +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 group + +import ( + "net/http" + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/auth/authenticator" + "k8s.io/kubernetes/pkg/auth/user" +) + +func TestGroupAdder(t *testing.T) { + adder := authenticator.Request( + NewGroupAdder( + authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + return &user.DefaultInfo{Name: "user", Groups: []string{"original"}}, true, nil + }), + []string{"added"}, + ), + ) + + user, _, _ := adder.AuthenticateRequest(nil) + if !reflect.DeepEqual(user.GetGroups(), []string{"original", "added"}) { + t.Errorf("Expected original,added groups, got %#v", user.GetGroups()) + } +} diff --git a/pkg/genericapiserver/options/server_run_options.go b/pkg/genericapiserver/options/server_run_options.go index ca5e3852abe..68d9f1396f8 100644 --- a/pkg/genericapiserver/options/server_run_options.go +++ b/pkg/genericapiserver/options/server_run_options.go @@ -71,6 +71,7 @@ type ServerRunOptions struct { AuthorizationWebhookCacheUnauthorizedTTL time.Duration AuthorizationRBACSuperUser string + AnonymousAuth bool BasicAuthFile string BindAddress net.IP CertDirectory string @@ -127,6 +128,7 @@ func NewServerRunOptions() *ServerRunOptions { APIGroupPrefix: "/apis", APIPrefix: "/api", AdmissionControl: "AlwaysAdmit", + AnonymousAuth: true, AuthorizationMode: "AlwaysAllow", AuthorizationWebhookCacheAuthorizedTTL: 5 * time.Minute, AuthorizationWebhookCacheUnauthorizedTTL: 30 * time.Second, @@ -269,6 +271,11 @@ func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) { "If specified, a username which avoids RBAC authorization checks and role binding "+ "privilege escalation checks, to be used with --authorization-mode=RBAC.") + fs.BoolVar(&s.AnonymousAuth, "anonymous-auth", s.AnonymousAuth, ""+ + "Enables anonymous requests to the secure port of the API server. "+ + "Requests that are not rejected by another authentication method are treated as anonymous requests. "+ + "Anonymous requests have a username of system:anonymous, and a group name of system:unauthenticated.") + fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, ""+ "If set, the file that will be used to admit requests to the secure port of the API server "+ "via http basic authentication.") diff --git a/plugin/pkg/auth/authenticator/request/anonymous/anonymous.go b/plugin/pkg/auth/authenticator/request/anonymous/anonymous.go new file mode 100644 index 00000000000..9864722ec7c --- /dev/null +++ b/plugin/pkg/auth/authenticator/request/anonymous/anonymous.go @@ -0,0 +1,36 @@ +/* +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 anonymous + +import ( + "net/http" + + "k8s.io/kubernetes/pkg/auth/authenticator" + "k8s.io/kubernetes/pkg/auth/user" +) + +const ( + anonymousUser = "system:anonymous" + + unauthenticatedGroup = "system:unauthenticated" +) + +func NewAuthenticator() authenticator.Request { + return authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + return &user.DefaultInfo{Name: anonymousUser, Groups: []string{unauthenticatedGroup}}, true, nil + }) +} diff --git a/plugin/pkg/auth/authenticator/request/anonymous/anonymous_test.go b/plugin/pkg/auth/authenticator/request/anonymous/anonymous_test.go new file mode 100644 index 00000000000..ca22fb7bfc0 --- /dev/null +++ b/plugin/pkg/auth/authenticator/request/anonymous/anonymous_test.go @@ -0,0 +1,41 @@ +/* +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 anonymous + +import ( + "testing" + + "k8s.io/kubernetes/pkg/auth/authenticator" + "k8s.io/kubernetes/pkg/util/sets" +) + +func TestAnonymous(t *testing.T) { + var a authenticator.Request = NewAuthenticator() + u, ok, err := a.AuthenticateRequest(nil) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if !ok { + t.Fatalf("Unexpectedly unauthenticated") + } + if u.GetName() != "system:anonymous" { + t.Fatalf("Expected username %s, got %s", "system:anonymous", u.GetName()) + } + if !sets.NewString(u.GetGroups()...).Equal(sets.NewString("system:unauthenticated")) { + t.Fatalf("Expected group %s, got %v", "system:unauthenticated", u.GetGroups()) + } +} diff --git a/plugin/pkg/auth/authenticator/request/union/union.go b/plugin/pkg/auth/authenticator/request/union/union.go index 5c34b4d5fcd..020f3558c66 100644 --- a/plugin/pkg/auth/authenticator/request/union/union.go +++ b/plugin/pkg/auth/authenticator/request/union/union.go @@ -25,26 +25,46 @@ import ( ) // unionAuthRequestHandler authenticates requests using a chain of authenticator.Requests -type unionAuthRequestHandler []authenticator.Request - -// New returns a request authenticator that validates credentials using a chain of authenticator.Request objects -func New(authRequestHandlers ...authenticator.Request) authenticator.Request { - return unionAuthRequestHandler(authRequestHandlers) +type unionAuthRequestHandler struct { + // Handlers is a chain of request authenticators to delegate to + Handlers []authenticator.Request + // FailOnError determines whether an error returns short-circuits the chain + FailOnError bool } -// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects. The first -// success returns that identity. Errors are only returned if no matches are found. -func (authHandler unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { +// New returns a request authenticator that validates credentials using a chain of authenticator.Request objects. +// The entire chain is tried until one succeeds. If all fail, an aggregate error is returned. +func New(authRequestHandlers ...authenticator.Request) authenticator.Request { + if len(authRequestHandlers) == 1 { + return authRequestHandlers[0] + } + return &unionAuthRequestHandler{Handlers: authRequestHandlers, FailOnError: false} +} + +// NewFailOnError returns a request authenticator that validates credentials using a chain of authenticator.Request objects. +// The first error short-circuits the chain. +func NewFailOnError(authRequestHandlers ...authenticator.Request) authenticator.Request { + if len(authRequestHandlers) == 1 { + return authRequestHandlers[0] + } + return &unionAuthRequestHandler{Handlers: authRequestHandlers, FailOnError: true} +} + +// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects. +func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { var errlist []error - for _, currAuthRequestHandler := range authHandler { + for _, currAuthRequestHandler := range authHandler.Handlers { info, ok, err := currAuthRequestHandler.AuthenticateRequest(req) if err != nil { + if authHandler.FailOnError { + return info, ok, err + } errlist = append(errlist, err) continue } if ok { - return info, true, nil + return info, ok, err } } diff --git a/plugin/pkg/auth/authenticator/request/union/unionauth_test.go b/plugin/pkg/auth/authenticator/request/union/unionauth_test.go index 62fa0c0da59..8792339501f 100644 --- a/plugin/pkg/auth/authenticator/request/union/unionauth_test.go +++ b/plugin/pkg/auth/authenticator/request/union/unionauth_test.go @@ -143,3 +143,24 @@ func TestAuthenticateRequestAdditiveErrors(t *testing.T) { t.Errorf("Unexpectedly authenticated: %v", isAuthenticated) } } + +func TestAuthenticateRequestFailEarly(t *testing.T) { + handler1 := &mockAuthRequestHandler{err: errors.New("first")} + handler2 := &mockAuthRequestHandler{err: errors.New("second")} + authRequestHandler := NewFailOnError(handler1, handler2) + req, _ := http.NewRequest("GET", "http://example.org", nil) + + _, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err == nil { + t.Errorf("Expected an error") + } + if !strings.Contains(err.Error(), "first") { + t.Errorf("Expected error containing %v, got %v", "first", err) + } + if strings.Contains(err.Error(), "second") { + t.Errorf("Did not expect second error, got %v", err) + } + if isAuthenticated { + t.Errorf("Unexpectedly authenticated: %v", isAuthenticated) + } +}