mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
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:
commit
d187997c94
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
50
pkg/auth/group/group_adder.go
Normal file
50
pkg/auth/group/group_adder.go
Normal 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
|
||||
}
|
42
pkg/auth/group/group_adder_test.go
Normal file
42
pkg/auth/group/group_adder_test.go
Normal 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())
|
||||
}
|
||||
}
|
@ -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.")
|
||||
|
36
plugin/pkg/auth/authenticator/request/anonymous/anonymous.go
Normal file
36
plugin/pkg/auth/authenticator/request/anonymous/anonymous.go
Normal 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
|
||||
})
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user