| /* |
| Copyright 2014 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 authenticator |
| |
| import ( |
| "time" |
| |
| "github.com/go-openapi/spec" |
| |
| "k8s.io/apiserver/pkg/authentication/authenticator" |
| "k8s.io/apiserver/pkg/authentication/authenticatorfactory" |
| "k8s.io/apiserver/pkg/authentication/group" |
| "k8s.io/apiserver/pkg/authentication/request/anonymous" |
| "k8s.io/apiserver/pkg/authentication/request/bearertoken" |
| "k8s.io/apiserver/pkg/authentication/request/headerrequest" |
| "k8s.io/apiserver/pkg/authentication/request/union" |
| "k8s.io/apiserver/pkg/authentication/request/websocket" |
| "k8s.io/apiserver/pkg/authentication/request/x509" |
| tokencache "k8s.io/apiserver/pkg/authentication/token/cache" |
| "k8s.io/apiserver/pkg/authentication/token/tokenfile" |
| tokenunion "k8s.io/apiserver/pkg/authentication/token/union" |
| utilfeature "k8s.io/apiserver/pkg/util/feature" |
| "k8s.io/apiserver/plugin/pkg/authenticator/password/passwordfile" |
| "k8s.io/apiserver/plugin/pkg/authenticator/request/basicauth" |
| "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc" |
| "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" |
| // Initialize all known client auth plugins. |
| _ "k8s.io/client-go/plugin/pkg/client/auth" |
| certutil "k8s.io/client-go/util/cert" |
| "k8s.io/kubernetes/pkg/features" |
| "k8s.io/kubernetes/pkg/serviceaccount" |
| ) |
| |
| // Config contains the data on how to authenticate a request to the Kube API Server |
| type Config struct { |
| Anonymous bool |
| BasicAuthFile string |
| BootstrapToken bool |
| ClientCAFile string |
| TokenAuthFile string |
| OIDCIssuerURL string |
| OIDCClientID string |
| OIDCCAFile string |
| OIDCUsernameClaim string |
| OIDCUsernamePrefix string |
| OIDCGroupsClaim string |
| OIDCGroupsPrefix string |
| OIDCSigningAlgs []string |
| OIDCRequiredClaims map[string]string |
| ServiceAccountKeyFiles []string |
| ServiceAccountLookup bool |
| ServiceAccountIssuer string |
| APIAudiences authenticator.Audiences |
| WebhookTokenAuthnConfigFile string |
| WebhookTokenAuthnCacheTTL time.Duration |
| |
| TokenSuccessCacheTTL time.Duration |
| TokenFailureCacheTTL time.Duration |
| |
| RequestHeaderConfig *authenticatorfactory.RequestHeaderConfig |
| |
| // TODO, this is the only non-serializable part of the entire config. Factor it out into a clientconfig |
| ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter |
| BootstrapTokenAuthenticator authenticator.Token |
| } |
| |
| // New returns an authenticator.Request or an error that supports the standard |
| // Kubernetes authentication mechanisms. |
| func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, error) { |
| var authenticators []authenticator.Request |
| var tokenAuthenticators []authenticator.Token |
| securityDefinitions := spec.SecurityDefinitions{} |
| |
| // 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, |
| config.RequestHeaderConfig.GroupHeaders, |
| config.RequestHeaderConfig.ExtraHeaderPrefixes, |
| ) |
| if err != nil { |
| return nil, nil, err |
| } |
| authenticators = append(authenticators, authenticator.WrapAudienceAgnosticRequest(config.APIAudiences, requestHeaderAuthenticator)) |
| } |
| |
| // basic auth |
| if len(config.BasicAuthFile) > 0 { |
| basicAuth, err := newAuthenticatorFromBasicAuthFile(config.BasicAuthFile) |
| if err != nil { |
| return nil, nil, err |
| } |
| authenticators = append(authenticators, authenticator.WrapAudienceAgnosticRequest(config.APIAudiences, basicAuth)) |
| |
| securityDefinitions["HTTPBasic"] = &spec.SecurityScheme{ |
| SecuritySchemeProps: spec.SecuritySchemeProps{ |
| Type: "basic", |
| Description: "HTTP Basic authentication", |
| }, |
| } |
| } |
| |
| // X509 methods |
| if len(config.ClientCAFile) > 0 { |
| certAuth, err := newAuthenticatorFromClientCAFile(config.ClientCAFile) |
| if err != nil { |
| return nil, nil, err |
| } |
| authenticators = append(authenticators, certAuth) |
| } |
| |
| // Bearer token methods, local first, then remote |
| if len(config.TokenAuthFile) > 0 { |
| tokenAuth, err := newAuthenticatorFromTokenFile(config.TokenAuthFile) |
| if err != nil { |
| return nil, nil, err |
| } |
| tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth)) |
| } |
| if len(config.ServiceAccountKeyFiles) > 0 { |
| serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter) |
| if err != nil { |
| return nil, nil, err |
| } |
| tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth) |
| } |
| if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) && config.ServiceAccountIssuer != "" { |
| serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuer, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter) |
| if err != nil { |
| return nil, nil, err |
| } |
| tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth) |
| } |
| if config.BootstrapToken { |
| if config.BootstrapTokenAuthenticator != nil { |
| // TODO: This can sometimes be nil because of |
| tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, config.BootstrapTokenAuthenticator)) |
| } |
| } |
| // NOTE(ericchiang): Keep the OpenID Connect after Service Accounts. |
| // |
| // Because both plugins verify JWTs whichever comes first in the union experiences |
| // cache misses for all requests using the other. While the service account plugin |
| // simply returns an error, the OpenID Connect plugin may query the provider to |
| // update the keys, causing performance hits. |
| if len(config.OIDCIssuerURL) > 0 && len(config.OIDCClientID) > 0 { |
| oidcAuth, err := newAuthenticatorFromOIDCIssuerURL(oidc.Options{ |
| IssuerURL: config.OIDCIssuerURL, |
| ClientID: config.OIDCClientID, |
| APIAudiences: config.APIAudiences, |
| CAFile: config.OIDCCAFile, |
| UsernameClaim: config.OIDCUsernameClaim, |
| UsernamePrefix: config.OIDCUsernamePrefix, |
| GroupsClaim: config.OIDCGroupsClaim, |
| GroupsPrefix: config.OIDCGroupsPrefix, |
| SupportedSigningAlgs: config.OIDCSigningAlgs, |
| RequiredClaims: config.OIDCRequiredClaims, |
| }) |
| if err != nil { |
| return nil, nil, err |
| } |
| tokenAuthenticators = append(tokenAuthenticators, oidcAuth) |
| } |
| if len(config.WebhookTokenAuthnConfigFile) > 0 { |
| webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL, config.APIAudiences) |
| if err != nil { |
| return nil, nil, err |
| } |
| tokenAuthenticators = append(tokenAuthenticators, webhookTokenAuth) |
| } |
| |
| if len(tokenAuthenticators) > 0 { |
| // Union the token authenticators |
| tokenAuth := tokenunion.New(tokenAuthenticators...) |
| // Optionally cache authentication results |
| if config.TokenSuccessCacheTTL > 0 || config.TokenFailureCacheTTL > 0 { |
| tokenAuth = tokencache.New(tokenAuth, true, config.TokenSuccessCacheTTL, config.TokenFailureCacheTTL) |
| } |
| authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth)) |
| securityDefinitions["BearerToken"] = &spec.SecurityScheme{ |
| SecuritySchemeProps: spec.SecuritySchemeProps{ |
| Type: "apiKey", |
| Name: "authorization", |
| In: "header", |
| Description: "Bearer Token authentication", |
| }, |
| } |
| } |
| |
| if len(authenticators) == 0 { |
| if config.Anonymous { |
| return anonymous.NewAuthenticator(), &securityDefinitions, nil |
| } |
| return nil, &securityDefinitions, nil |
| } |
| |
| authenticator := union.New(authenticators...) |
| |
| authenticator = group.NewAuthenticatedGroupAdder(authenticator) |
| |
| if config.Anonymous { |
| // If the authenticator chain returns an error, return an error (don't consider a bad bearer token |
| // or invalid username/password combination anonymous). |
| authenticator = union.NewFailOnError(authenticator, anonymous.NewAuthenticator()) |
| } |
| |
| return authenticator, &securityDefinitions, nil |
| } |
| |
| // IsValidServiceAccountKeyFile returns true if a valid public RSA key can be read from the given file |
| func IsValidServiceAccountKeyFile(file string) bool { |
| _, err := certutil.PublicKeysFromFile(file) |
| return err == nil |
| } |
| |
| // newAuthenticatorFromBasicAuthFile returns an authenticator.Request or an error |
| func newAuthenticatorFromBasicAuthFile(basicAuthFile string) (authenticator.Request, error) { |
| basicAuthenticator, err := passwordfile.NewCSV(basicAuthFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| return basicauth.New(basicAuthenticator), nil |
| } |
| |
| // newAuthenticatorFromTokenFile returns an authenticator.Token or an error |
| func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, error) { |
| tokenAuthenticator, err := tokenfile.NewCSV(tokenAuthFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| return tokenAuthenticator, nil |
| } |
| |
| // newAuthenticatorFromOIDCIssuerURL returns an authenticator.Token or an error. |
| func newAuthenticatorFromOIDCIssuerURL(opts oidc.Options) (authenticator.Token, error) { |
| const noUsernamePrefix = "-" |
| |
| if opts.UsernamePrefix == "" && opts.UsernameClaim != "email" { |
| // Old behavior. If a usernamePrefix isn't provided, prefix all claims other than "email" |
| // with the issuerURL. |
| // |
| // See https://github.com/kubernetes/kubernetes/issues/31380 |
| opts.UsernamePrefix = opts.IssuerURL + "#" |
| } |
| |
| if opts.UsernamePrefix == noUsernamePrefix { |
| // Special value indicating usernames shouldn't be prefixed. |
| opts.UsernamePrefix = "" |
| } |
| |
| tokenAuthenticator, err := oidc.New(opts) |
| if err != nil { |
| return nil, err |
| } |
| |
| return tokenAuthenticator, nil |
| } |
| |
| // newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error |
| func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) { |
| allPublicKeys := []interface{}{} |
| for _, keyfile := range keyfiles { |
| publicKeys, err := certutil.PublicKeysFromFile(keyfile) |
| if err != nil { |
| return nil, err |
| } |
| allPublicKeys = append(allPublicKeys, publicKeys...) |
| } |
| |
| tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, allPublicKeys, apiAudiences, serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter)) |
| return tokenAuthenticator, nil |
| } |
| |
| // newServiceAccountAuthenticator returns an authenticator.Token or an error |
| func newServiceAccountAuthenticator(iss string, keyfiles []string, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) { |
| allPublicKeys := []interface{}{} |
| for _, keyfile := range keyfiles { |
| publicKeys, err := certutil.PublicKeysFromFile(keyfile) |
| if err != nil { |
| return nil, err |
| } |
| allPublicKeys = append(allPublicKeys, publicKeys...) |
| } |
| |
| tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(iss, allPublicKeys, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter)) |
| return tokenAuthenticator, nil |
| } |
| |
| // newAuthenticatorFromClientCAFile returns an authenticator.Request or an error |
| func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) { |
| roots, err := certutil.NewPool(clientCAFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| opts := x509.DefaultVerifyOptions() |
| opts.Roots = roots |
| |
| return x509.New(opts, x509.CommonNameUserConversion), nil |
| } |
| |
| func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) { |
| webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, implicitAuds) |
| if err != nil { |
| return nil, err |
| } |
| |
| return tokencache.New(webhookTokenAuthenticator, false, ttl, ttl), nil |
| } |