Merge pull request #32386 from liggitt/anonymous-authenticated-groups

Automatic merge from submit-queue

Allow anonymous API server access, decorate authenticated users with system:authenticated group

When writing authorization policy, it is often necessary to allow certain actions to any authenticated user. For example, creating a service or configmap, and granting read access to all users

It is also frequently necessary to allow actions to any unauthenticated user. For example, fetching discovery APIs might be part of an authentication process, and therefore need to be able to be read without access to authentication credentials.

This PR:
* Adds an option to allow anonymous requests to the secured API port. If enabled, requests to the secure port that are not rejected by other configured authentication methods are treated as anonymous requests, and given a username of `system:anonymous` and a group of `system:unauthenticated`. Note: this should only be used with an `--authorization-mode` other than `AlwaysAllow`
* Decorates user.Info returned from configured authenticators with the group `system:authenticated`.

This is related to defining a default set of roles and bindings for RBAC (https://github.com/kubernetes/features/issues/2). The bootstrap policy should allow all users (anonymous or authenticated) to request the discovery APIs.

```release-note
kube-apiserver learned the '--anonymous-auth' flag, which defaults to true. When enabled, requests to the secure port that are not rejected by other configured authentication methods are treated as anonymous requests, and given a username of 'system:anonymous' and a group of 'system:unauthenticated'. 

Authenticated users are decorated with a 'system:authenticated' group.

NOTE: anonymous access is enabled by default. If you rely on authentication alone to authorize access, change to use an authorization mode other than AlwaysAllow, or or set '--anonymous-auth=false'.
```

c.f. https://github.com/kubernetes/kubernetes/issues/29177#issuecomment-244191596
This commit is contained in:
Kubernetes Submit Queue 2016-09-29 10:47:49 -07:00 committed by GitHub
commit d187997c94
11 changed files with 258 additions and 27 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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())
}
}

View File

@ -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.")

View File

@ -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
})
}

View File

@ -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())
}
}

View File

@ -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
}
}

View File

@ -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)
}
}