k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/kubeapiserver/authenticator/config.go (about) 1 /* 2 Copyright 2014 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package authenticator 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "sync/atomic" 24 "time" 25 26 utilerrors "k8s.io/apimachinery/pkg/util/errors" 27 utilnet "k8s.io/apimachinery/pkg/util/net" 28 "k8s.io/apimachinery/pkg/util/wait" 29 "k8s.io/apiserver/pkg/apis/apiserver" 30 "k8s.io/apiserver/pkg/authentication/authenticator" 31 "k8s.io/apiserver/pkg/authentication/authenticatorfactory" 32 "k8s.io/apiserver/pkg/authentication/group" 33 "k8s.io/apiserver/pkg/authentication/request/anonymous" 34 "k8s.io/apiserver/pkg/authentication/request/bearertoken" 35 "k8s.io/apiserver/pkg/authentication/request/headerrequest" 36 "k8s.io/apiserver/pkg/authentication/request/union" 37 "k8s.io/apiserver/pkg/authentication/request/websocket" 38 "k8s.io/apiserver/pkg/authentication/request/x509" 39 tokencache "k8s.io/apiserver/pkg/authentication/token/cache" 40 "k8s.io/apiserver/pkg/authentication/token/tokenfile" 41 tokenunion "k8s.io/apiserver/pkg/authentication/token/union" 42 "k8s.io/apiserver/pkg/server/dynamiccertificates" 43 webhookutil "k8s.io/apiserver/pkg/util/webhook" 44 "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc" 45 "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" 46 typedv1core "k8s.io/client-go/kubernetes/typed/core/v1" 47 "k8s.io/kube-openapi/pkg/spec3" 48 "k8s.io/kube-openapi/pkg/validation/spec" 49 50 // Initialize all known client auth plugins. 51 _ "k8s.io/client-go/plugin/pkg/client/auth" 52 "k8s.io/client-go/util/keyutil" 53 "k8s.io/kubernetes/pkg/serviceaccount" 54 ) 55 56 // Config contains the data on how to authenticate a request to the Kube API Server 57 type Config struct { 58 Anonymous bool 59 BootstrapToken bool 60 61 TokenAuthFile string 62 AuthenticationConfig *apiserver.AuthenticationConfiguration 63 AuthenticationConfigData string 64 OIDCSigningAlgs []string 65 ServiceAccountKeyFiles []string 66 ServiceAccountLookup bool 67 ServiceAccountIssuers []string 68 APIAudiences authenticator.Audiences 69 WebhookTokenAuthnConfigFile string 70 WebhookTokenAuthnVersion string 71 WebhookTokenAuthnCacheTTL time.Duration 72 // WebhookRetryBackoff specifies the backoff parameters for the authentication webhook retry logic. 73 // This allows us to configure the sleep time at each iteration and the maximum number of retries allowed 74 // before we fail the webhook call in order to limit the fan out that ensues when the system is degraded. 75 WebhookRetryBackoff *wait.Backoff 76 77 TokenSuccessCacheTTL time.Duration 78 TokenFailureCacheTTL time.Duration 79 80 RequestHeaderConfig *authenticatorfactory.RequestHeaderConfig 81 82 // TODO, this is the only non-serializable part of the entire config. Factor it out into a clientconfig 83 ServiceAccountTokenGetter serviceaccount.ServiceAccountTokenGetter 84 SecretsWriter typedv1core.SecretsGetter 85 BootstrapTokenAuthenticator authenticator.Token 86 // ClientCAContentProvider are the options for verifying incoming connections using mTLS and directly assigning to users. 87 // Generally this is the CA bundle file used to authenticate client certificates 88 // If this value is nil, then mutual TLS is disabled. 89 ClientCAContentProvider dynamiccertificates.CAContentProvider 90 91 // Optional field, custom dial function used to connect to webhook 92 CustomDial utilnet.DialFunc 93 } 94 95 // New returns an authenticator.Request or an error that supports the standard 96 // Kubernetes authentication mechanisms. 97 func (config Config) New(serverLifecycle context.Context) (authenticator.Request, func(context.Context, *apiserver.AuthenticationConfiguration) error, *spec.SecurityDefinitions, spec3.SecuritySchemes, error) { 98 var authenticators []authenticator.Request 99 var tokenAuthenticators []authenticator.Token 100 securityDefinitionsV2 := spec.SecurityDefinitions{} 101 securitySchemesV3 := spec3.SecuritySchemes{} 102 103 // front-proxy, BasicAuth methods, local first, then remote 104 // Add the front proxy authenticator if requested 105 if config.RequestHeaderConfig != nil { 106 requestHeaderAuthenticator := headerrequest.NewDynamicVerifyOptionsSecure( 107 config.RequestHeaderConfig.CAContentProvider.VerifyOptions, 108 config.RequestHeaderConfig.AllowedClientNames, 109 config.RequestHeaderConfig.UsernameHeaders, 110 config.RequestHeaderConfig.GroupHeaders, 111 config.RequestHeaderConfig.ExtraHeaderPrefixes, 112 ) 113 authenticators = append(authenticators, authenticator.WrapAudienceAgnosticRequest(config.APIAudiences, requestHeaderAuthenticator)) 114 } 115 116 // X509 methods 117 if config.ClientCAContentProvider != nil { 118 certAuth := x509.NewDynamic(config.ClientCAContentProvider.VerifyOptions, x509.CommonNameUserConversion) 119 authenticators = append(authenticators, certAuth) 120 } 121 122 // Bearer token methods, local first, then remote 123 if len(config.TokenAuthFile) > 0 { 124 tokenAuth, err := newAuthenticatorFromTokenFile(config.TokenAuthFile) 125 if err != nil { 126 return nil, nil, nil, nil, err 127 } 128 tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth)) 129 } 130 if len(config.ServiceAccountKeyFiles) > 0 { 131 serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter, config.SecretsWriter) 132 if err != nil { 133 return nil, nil, nil, nil, err 134 } 135 tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth) 136 } 137 if len(config.ServiceAccountIssuers) > 0 { 138 serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuers, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter) 139 if err != nil { 140 return nil, nil, nil, nil, err 141 } 142 tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth) 143 } 144 145 if config.BootstrapToken && config.BootstrapTokenAuthenticator != nil { 146 tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, config.BootstrapTokenAuthenticator)) 147 } 148 149 // NOTE(ericchiang): Keep the OpenID Connect after Service Accounts. 150 // 151 // Because both plugins verify JWTs whichever comes first in the union experiences 152 // cache misses for all requests using the other. While the service account plugin 153 // simply returns an error, the OpenID Connect plugin may query the provider to 154 // update the keys, causing performance hits. 155 var updateAuthenticationConfig func(context.Context, *apiserver.AuthenticationConfiguration) error 156 if config.AuthenticationConfig != nil { 157 initialJWTAuthenticator, err := newJWTAuthenticator(serverLifecycle, config.AuthenticationConfig, config.OIDCSigningAlgs, config.APIAudiences, config.ServiceAccountIssuers) 158 if err != nil { 159 return nil, nil, nil, nil, err 160 } 161 162 jwtAuthenticatorPtr := &atomic.Pointer[jwtAuthenticatorWithCancel]{} 163 jwtAuthenticatorPtr.Store(initialJWTAuthenticator) 164 165 updateAuthenticationConfig = (&authenticationConfigUpdater{ 166 serverLifecycle: serverLifecycle, 167 config: config, 168 jwtAuthenticatorPtr: jwtAuthenticatorPtr, 169 }).updateAuthenticationConfig 170 171 tokenAuthenticators = append(tokenAuthenticators, 172 authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { 173 return jwtAuthenticatorPtr.Load().jwtAuthenticator.AuthenticateToken(ctx, token) 174 }), 175 ) 176 } 177 178 if len(config.WebhookTokenAuthnConfigFile) > 0 { 179 webhookTokenAuth, err := newWebhookTokenAuthenticator(config) 180 if err != nil { 181 return nil, nil, nil, nil, err 182 } 183 184 tokenAuthenticators = append(tokenAuthenticators, webhookTokenAuth) 185 } 186 187 if len(tokenAuthenticators) > 0 { 188 // Union the token authenticators 189 tokenAuth := tokenunion.New(tokenAuthenticators...) 190 // Optionally cache authentication results 191 if config.TokenSuccessCacheTTL > 0 || config.TokenFailureCacheTTL > 0 { 192 tokenAuth = tokencache.New(tokenAuth, true, config.TokenSuccessCacheTTL, config.TokenFailureCacheTTL) 193 } 194 authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth)) 195 196 securityDefinitionsV2["BearerToken"] = &spec.SecurityScheme{ 197 SecuritySchemeProps: spec.SecuritySchemeProps{ 198 Type: "apiKey", 199 Name: "authorization", 200 In: "header", 201 Description: "Bearer Token authentication", 202 }, 203 } 204 securitySchemesV3["BearerToken"] = &spec3.SecurityScheme{ 205 SecuritySchemeProps: spec3.SecuritySchemeProps{ 206 Type: "apiKey", 207 Name: "authorization", 208 In: "header", 209 Description: "Bearer Token authentication", 210 }, 211 } 212 } 213 214 if len(authenticators) == 0 { 215 if config.Anonymous { 216 return anonymous.NewAuthenticator(), nil, &securityDefinitionsV2, securitySchemesV3, nil 217 } 218 return nil, nil, &securityDefinitionsV2, securitySchemesV3, nil 219 } 220 221 authenticator := union.New(authenticators...) 222 223 authenticator = group.NewAuthenticatedGroupAdder(authenticator) 224 225 if config.Anonymous { 226 // If the authenticator chain returns an error, return an error (don't consider a bad bearer token 227 // or invalid username/password combination anonymous). 228 authenticator = union.NewFailOnError(authenticator, anonymous.NewAuthenticator()) 229 } 230 231 return authenticator, updateAuthenticationConfig, &securityDefinitionsV2, securitySchemesV3, nil 232 } 233 234 type jwtAuthenticatorWithCancel struct { 235 jwtAuthenticator authenticator.Token 236 healthCheck func() error 237 cancel func() 238 } 239 240 func newJWTAuthenticator(serverLifecycle context.Context, config *apiserver.AuthenticationConfiguration, oidcSigningAlgs []string, apiAudiences authenticator.Audiences, disallowedIssuers []string) (_ *jwtAuthenticatorWithCancel, buildErr error) { 241 ctx, cancel := context.WithCancel(serverLifecycle) 242 243 defer func() { 244 if buildErr != nil { 245 cancel() 246 } 247 }() 248 var jwtAuthenticators []authenticator.Token 249 var healthChecks []func() error 250 for _, jwtAuthenticator := range config.JWT { 251 // TODO remove this CAContentProvider indirection 252 var oidcCAContent oidc.CAContentProvider 253 if len(jwtAuthenticator.Issuer.CertificateAuthority) > 0 { 254 var oidcCAError error 255 oidcCAContent, oidcCAError = dynamiccertificates.NewStaticCAContent("oidc-authenticator", []byte(jwtAuthenticator.Issuer.CertificateAuthority)) 256 if oidcCAError != nil { 257 return nil, oidcCAError 258 } 259 } 260 oidcAuth, err := oidc.New(ctx, oidc.Options{ 261 JWTAuthenticator: jwtAuthenticator, 262 CAContentProvider: oidcCAContent, 263 SupportedSigningAlgs: oidcSigningAlgs, 264 DisallowedIssuers: disallowedIssuers, 265 }) 266 if err != nil { 267 return nil, err 268 } 269 jwtAuthenticators = append(jwtAuthenticators, oidcAuth) 270 healthChecks = append(healthChecks, oidcAuth.HealthCheck) 271 } 272 return &jwtAuthenticatorWithCancel{ 273 jwtAuthenticator: authenticator.WrapAudienceAgnosticToken(apiAudiences, tokenunion.NewFailOnError(jwtAuthenticators...)), // this handles the empty jwtAuthenticators slice case correctly 274 healthCheck: func() error { 275 var errs []error 276 for _, check := range healthChecks { 277 if err := check(); err != nil { 278 errs = append(errs, err) 279 } 280 } 281 return utilerrors.NewAggregate(errs) 282 }, 283 cancel: cancel, 284 }, nil 285 } 286 287 type authenticationConfigUpdater struct { 288 serverLifecycle context.Context 289 config Config 290 jwtAuthenticatorPtr *atomic.Pointer[jwtAuthenticatorWithCancel] 291 } 292 293 // the input ctx controls the timeout for updateAuthenticationConfig to return, not the lifetime of the constructed authenticators. 294 func (c *authenticationConfigUpdater) updateAuthenticationConfig(ctx context.Context, authConfig *apiserver.AuthenticationConfiguration) error { 295 updatedJWTAuthenticator, err := newJWTAuthenticator(c.serverLifecycle, authConfig, c.config.OIDCSigningAlgs, c.config.APIAudiences, c.config.ServiceAccountIssuers) 296 if err != nil { 297 return err 298 } 299 300 var lastErr error 301 if waitErr := wait.PollUntilContextCancel(ctx, 10*time.Second, true, func(_ context.Context) (done bool, err error) { 302 lastErr = updatedJWTAuthenticator.healthCheck() 303 return lastErr == nil, nil 304 }); lastErr != nil || waitErr != nil { 305 updatedJWTAuthenticator.cancel() 306 return utilerrors.NewAggregate([]error{lastErr, waitErr}) // filters out nil errors 307 } 308 309 oldJWTAuthenticator := c.jwtAuthenticatorPtr.Swap(updatedJWTAuthenticator) 310 go func() { 311 t := time.NewTimer(time.Minute) 312 defer t.Stop() 313 select { 314 case <-c.serverLifecycle.Done(): 315 case <-t.C: 316 } 317 // TODO maybe track requests so we know when this is safe to do 318 oldJWTAuthenticator.cancel() 319 }() 320 321 return nil 322 } 323 324 // IsValidServiceAccountKeyFile returns true if a valid public RSA key can be read from the given file 325 func IsValidServiceAccountKeyFile(file string) bool { 326 _, err := keyutil.PublicKeysFromFile(file) 327 return err == nil 328 } 329 330 // newAuthenticatorFromTokenFile returns an authenticator.Token or an error 331 func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, error) { 332 tokenAuthenticator, err := tokenfile.NewCSV(tokenAuthFile) 333 if err != nil { 334 return nil, err 335 } 336 337 return tokenAuthenticator, nil 338 } 339 340 // newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error 341 func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (authenticator.Token, error) { 342 allPublicKeys := []interface{}{} 343 for _, keyfile := range keyfiles { 344 publicKeys, err := keyutil.PublicKeysFromFile(keyfile) 345 if err != nil { 346 return nil, err 347 } 348 allPublicKeys = append(allPublicKeys, publicKeys...) 349 } 350 validator, err := serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter, secretsWriter) 351 if err != nil { 352 return nil, fmt.Errorf("while creating legacy validator, err: %w", err) 353 } 354 355 tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]string{serviceaccount.LegacyIssuer}, allPublicKeys, apiAudiences, validator) 356 return tokenAuthenticator, nil 357 } 358 359 // newServiceAccountAuthenticator returns an authenticator.Token or an error 360 func newServiceAccountAuthenticator(issuers []string, keyfiles []string, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) { 361 allPublicKeys := []interface{}{} 362 for _, keyfile := range keyfiles { 363 publicKeys, err := keyutil.PublicKeysFromFile(keyfile) 364 if err != nil { 365 return nil, err 366 } 367 allPublicKeys = append(allPublicKeys, publicKeys...) 368 } 369 370 tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(issuers, allPublicKeys, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter)) 371 return tokenAuthenticator, nil 372 } 373 374 func newWebhookTokenAuthenticator(config Config) (authenticator.Token, error) { 375 if config.WebhookRetryBackoff == nil { 376 return nil, errors.New("retry backoff parameters for authentication webhook has not been specified") 377 } 378 379 clientConfig, err := webhookutil.LoadKubeconfig(config.WebhookTokenAuthnConfigFile, config.CustomDial) 380 if err != nil { 381 return nil, err 382 } 383 webhookTokenAuthenticator, err := webhook.New(clientConfig, config.WebhookTokenAuthnVersion, config.APIAudiences, *config.WebhookRetryBackoff) 384 if err != nil { 385 return nil, err 386 } 387 388 return tokencache.New(webhookTokenAuthenticator, false, config.WebhookTokenAuthnCacheTTL, config.WebhookTokenAuthnCacheTTL), nil 389 }