k8s.io/apiserver@v0.31.1/pkg/server/options/authentication.go (about) 1 /* 2 Copyright 2016 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 options 18 19 import ( 20 "context" 21 "fmt" 22 "strings" 23 "time" 24 25 "github.com/spf13/pflag" 26 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/util/wait" 29 "k8s.io/apiserver/pkg/apis/apiserver" 30 "k8s.io/apiserver/pkg/authentication/authenticatorfactory" 31 "k8s.io/apiserver/pkg/authentication/request/headerrequest" 32 "k8s.io/apiserver/pkg/server" 33 "k8s.io/apiserver/pkg/server/dynamiccertificates" 34 "k8s.io/client-go/kubernetes" 35 "k8s.io/client-go/rest" 36 "k8s.io/client-go/tools/clientcmd" 37 "k8s.io/client-go/transport" 38 "k8s.io/klog/v2" 39 openapicommon "k8s.io/kube-openapi/pkg/common" 40 ) 41 42 // DefaultAuthWebhookRetryBackoff is the default backoff parameters for 43 // both authentication and authorization webhook used by the apiserver. 44 func DefaultAuthWebhookRetryBackoff() *wait.Backoff { 45 return &wait.Backoff{ 46 Duration: 500 * time.Millisecond, 47 Factor: 1.5, 48 Jitter: 0.2, 49 Steps: 5, 50 } 51 } 52 53 type RequestHeaderAuthenticationOptions struct { 54 // ClientCAFile is the root certificate bundle to verify client certificates on incoming requests 55 // before trusting usernames in headers. 56 ClientCAFile string 57 58 UsernameHeaders []string 59 GroupHeaders []string 60 ExtraHeaderPrefixes []string 61 AllowedNames []string 62 } 63 64 func (s *RequestHeaderAuthenticationOptions) Validate() []error { 65 allErrors := []error{} 66 67 if err := checkForWhiteSpaceOnly("requestheader-username-headers", s.UsernameHeaders...); err != nil { 68 allErrors = append(allErrors, err) 69 } 70 if err := checkForWhiteSpaceOnly("requestheader-group-headers", s.GroupHeaders...); err != nil { 71 allErrors = append(allErrors, err) 72 } 73 if err := checkForWhiteSpaceOnly("requestheader-extra-headers-prefix", s.ExtraHeaderPrefixes...); err != nil { 74 allErrors = append(allErrors, err) 75 } 76 if err := checkForWhiteSpaceOnly("requestheader-allowed-names", s.AllowedNames...); err != nil { 77 allErrors = append(allErrors, err) 78 } 79 80 if len(s.UsernameHeaders) > 0 && !caseInsensitiveHas(s.UsernameHeaders, "X-Remote-User") { 81 klog.Warningf("--requestheader-username-headers is set without specifying the standard X-Remote-User header - API aggregation will not work") 82 } 83 if len(s.GroupHeaders) > 0 && !caseInsensitiveHas(s.GroupHeaders, "X-Remote-Group") { 84 klog.Warningf("--requestheader-group-headers is set without specifying the standard X-Remote-Group header - API aggregation will not work") 85 } 86 if len(s.ExtraHeaderPrefixes) > 0 && !caseInsensitiveHas(s.ExtraHeaderPrefixes, "X-Remote-Extra-") { 87 klog.Warningf("--requestheader-extra-headers-prefix is set without specifying the standard X-Remote-Extra- header prefix - API aggregation will not work") 88 } 89 90 return allErrors 91 } 92 93 func checkForWhiteSpaceOnly(flag string, headerNames ...string) error { 94 for _, headerName := range headerNames { 95 if len(strings.TrimSpace(headerName)) == 0 { 96 return fmt.Errorf("empty value in %q", flag) 97 } 98 } 99 100 return nil 101 } 102 103 func caseInsensitiveHas(headers []string, header string) bool { 104 for _, h := range headers { 105 if strings.EqualFold(h, header) { 106 return true 107 } 108 } 109 return false 110 } 111 112 func (s *RequestHeaderAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { 113 if s == nil { 114 return 115 } 116 117 fs.StringSliceVar(&s.UsernameHeaders, "requestheader-username-headers", s.UsernameHeaders, ""+ 118 "List of request headers to inspect for usernames. X-Remote-User is common.") 119 120 fs.StringSliceVar(&s.GroupHeaders, "requestheader-group-headers", s.GroupHeaders, ""+ 121 "List of request headers to inspect for groups. X-Remote-Group is suggested.") 122 123 fs.StringSliceVar(&s.ExtraHeaderPrefixes, "requestheader-extra-headers-prefix", s.ExtraHeaderPrefixes, ""+ 124 "List of request header prefixes to inspect. X-Remote-Extra- is suggested.") 125 126 fs.StringVar(&s.ClientCAFile, "requestheader-client-ca-file", s.ClientCAFile, ""+ 127 "Root certificate bundle to use to verify client certificates on incoming requests "+ 128 "before trusting usernames in headers specified by --requestheader-username-headers. "+ 129 "WARNING: generally do not depend on authorization being already done for incoming requests.") 130 131 fs.StringSliceVar(&s.AllowedNames, "requestheader-allowed-names", s.AllowedNames, ""+ 132 "List of client certificate common names to allow to provide usernames in headers "+ 133 "specified by --requestheader-username-headers. If empty, any client certificate validated "+ 134 "by the authorities in --requestheader-client-ca-file is allowed.") 135 } 136 137 // ToAuthenticationRequestHeaderConfig returns a RequestHeaderConfig config object for these options 138 // if necessary, nil otherwise. 139 func (s *RequestHeaderAuthenticationOptions) ToAuthenticationRequestHeaderConfig() (*authenticatorfactory.RequestHeaderConfig, error) { 140 if len(s.ClientCAFile) == 0 { 141 return nil, nil 142 } 143 144 caBundleProvider, err := dynamiccertificates.NewDynamicCAContentFromFile("request-header", s.ClientCAFile) 145 if err != nil { 146 return nil, err 147 } 148 149 return &authenticatorfactory.RequestHeaderConfig{ 150 UsernameHeaders: headerrequest.StaticStringSlice(s.UsernameHeaders), 151 GroupHeaders: headerrequest.StaticStringSlice(s.GroupHeaders), 152 ExtraHeaderPrefixes: headerrequest.StaticStringSlice(s.ExtraHeaderPrefixes), 153 CAContentProvider: caBundleProvider, 154 AllowedClientNames: headerrequest.StaticStringSlice(s.AllowedNames), 155 }, nil 156 } 157 158 // ClientCertAuthenticationOptions provides different options for client cert auth. You should use `GetClientVerifyOptionFn` to 159 // get the verify options for your authenticator. 160 type ClientCertAuthenticationOptions struct { 161 // ClientCA is the certificate bundle for all the signers that you'll recognize for incoming client certificates 162 ClientCA string 163 164 // CAContentProvider are the options for verifying incoming connections using mTLS and directly assigning to users. 165 // Generally this is the CA bundle file used to authenticate client certificates 166 // If non-nil, this takes priority over the ClientCA file. 167 CAContentProvider dynamiccertificates.CAContentProvider 168 } 169 170 // GetClientVerifyOptionFn provides verify options for your authenticator while respecting the preferred order of verifiers. 171 func (s *ClientCertAuthenticationOptions) GetClientCAContentProvider() (dynamiccertificates.CAContentProvider, error) { 172 if s.CAContentProvider != nil { 173 return s.CAContentProvider, nil 174 } 175 176 if len(s.ClientCA) == 0 { 177 return nil, nil 178 } 179 180 return dynamiccertificates.NewDynamicCAContentFromFile("client-ca-bundle", s.ClientCA) 181 } 182 183 func (s *ClientCertAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { 184 fs.StringVar(&s.ClientCA, "client-ca-file", s.ClientCA, ""+ 185 "If set, any request presenting a client certificate signed by one of "+ 186 "the authorities in the client-ca-file is authenticated with an identity "+ 187 "corresponding to the CommonName of the client certificate.") 188 } 189 190 // DelegatingAuthenticationOptions provides an easy way for composing API servers to delegate their authentication to 191 // the root kube API server. The API federator will act as 192 // a front proxy and direction connections will be able to delegate to the core kube API server 193 type DelegatingAuthenticationOptions struct { 194 // RemoteKubeConfigFile is the file to use to connect to a "normal" kube API server which hosts the 195 // TokenAccessReview.authentication.k8s.io endpoint for checking tokens. 196 RemoteKubeConfigFile string 197 // RemoteKubeConfigFileOptional is specifying whether not specifying the kubeconfig or 198 // a missing in-cluster config will be fatal. 199 RemoteKubeConfigFileOptional bool 200 201 // CacheTTL is the length of time that a token authentication answer will be cached. 202 CacheTTL time.Duration 203 204 ClientCert ClientCertAuthenticationOptions 205 RequestHeader RequestHeaderAuthenticationOptions 206 207 // SkipInClusterLookup indicates missing authentication configuration should not be retrieved from the cluster configmap 208 SkipInClusterLookup bool 209 210 // TolerateInClusterLookupFailure indicates failures to look up authentication configuration from the cluster configmap should not be fatal. 211 // Setting this can result in an authenticator that will reject all requests. 212 TolerateInClusterLookupFailure bool 213 214 // WebhookRetryBackoff specifies the backoff parameters for the authentication webhook retry logic. 215 // This allows us to configure the sleep time at each iteration and the maximum number of retries allowed 216 // before we fail the webhook call in order to limit the fan out that ensues when the system is degraded. 217 WebhookRetryBackoff *wait.Backoff 218 219 // TokenRequestTimeout specifies a time limit for requests made by the authorization webhook client. 220 // The default value is set to 10 seconds. 221 TokenRequestTimeout time.Duration 222 223 // CustomRoundTripperFn allows for specifying a middleware function for custom HTTP behaviour for the authentication webhook client. 224 CustomRoundTripperFn transport.WrapperFunc 225 226 // Anonymous gives user an option to enable/disable Anonymous authentication. 227 Anonymous *apiserver.AnonymousAuthConfig 228 } 229 230 func NewDelegatingAuthenticationOptions() *DelegatingAuthenticationOptions { 231 return &DelegatingAuthenticationOptions{ 232 // very low for responsiveness, but high enough to handle storms 233 CacheTTL: 10 * time.Second, 234 ClientCert: ClientCertAuthenticationOptions{}, 235 RequestHeader: RequestHeaderAuthenticationOptions{ 236 UsernameHeaders: []string{"x-remote-user"}, 237 GroupHeaders: []string{"x-remote-group"}, 238 ExtraHeaderPrefixes: []string{"x-remote-extra-"}, 239 }, 240 WebhookRetryBackoff: DefaultAuthWebhookRetryBackoff(), 241 TokenRequestTimeout: 10 * time.Second, 242 Anonymous: &apiserver.AnonymousAuthConfig{Enabled: true}, 243 } 244 } 245 246 // WithCustomRetryBackoff sets the custom backoff parameters for the authentication webhook retry logic. 247 func (s *DelegatingAuthenticationOptions) WithCustomRetryBackoff(backoff wait.Backoff) { 248 s.WebhookRetryBackoff = &backoff 249 } 250 251 // WithRequestTimeout sets the given timeout for requests made by the authentication webhook client. 252 func (s *DelegatingAuthenticationOptions) WithRequestTimeout(timeout time.Duration) { 253 s.TokenRequestTimeout = timeout 254 } 255 256 // WithCustomRoundTripper allows for specifying a middleware function for custom HTTP behaviour for the authentication webhook client. 257 func (s *DelegatingAuthenticationOptions) WithCustomRoundTripper(rt transport.WrapperFunc) { 258 s.CustomRoundTripperFn = rt 259 } 260 261 func (s *DelegatingAuthenticationOptions) Validate() []error { 262 if s == nil { 263 return nil 264 } 265 266 allErrors := []error{} 267 allErrors = append(allErrors, s.RequestHeader.Validate()...) 268 269 if s.WebhookRetryBackoff != nil && s.WebhookRetryBackoff.Steps <= 0 { 270 allErrors = append(allErrors, fmt.Errorf("number of webhook retry attempts must be greater than 1, but is: %d", s.WebhookRetryBackoff.Steps)) 271 } 272 273 return allErrors 274 } 275 276 func (s *DelegatingAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { 277 if s == nil { 278 return 279 } 280 281 var optionalKubeConfigSentence string 282 if s.RemoteKubeConfigFileOptional { 283 optionalKubeConfigSentence = " This is optional. If empty, all token requests are considered to be anonymous and no client CA is looked up in the cluster." 284 } 285 fs.StringVar(&s.RemoteKubeConfigFile, "authentication-kubeconfig", s.RemoteKubeConfigFile, ""+ 286 "kubeconfig file pointing at the 'core' kubernetes server with enough rights to create "+ 287 "tokenreviews.authentication.k8s.io."+optionalKubeConfigSentence) 288 289 fs.DurationVar(&s.CacheTTL, "authentication-token-webhook-cache-ttl", s.CacheTTL, 290 "The duration to cache responses from the webhook token authenticator.") 291 292 s.ClientCert.AddFlags(fs) 293 s.RequestHeader.AddFlags(fs) 294 295 fs.BoolVar(&s.SkipInClusterLookup, "authentication-skip-lookup", s.SkipInClusterLookup, ""+ 296 "If false, the authentication-kubeconfig will be used to lookup missing authentication "+ 297 "configuration from the cluster.") 298 fs.BoolVar(&s.TolerateInClusterLookupFailure, "authentication-tolerate-lookup-failure", s.TolerateInClusterLookupFailure, ""+ 299 "If true, failures to look up missing authentication configuration from the cluster are not considered fatal. "+ 300 "Note that this can result in authentication that treats all requests as anonymous.") 301 } 302 303 func (s *DelegatingAuthenticationOptions) ApplyTo(authenticationInfo *server.AuthenticationInfo, servingInfo *server.SecureServingInfo, openAPIConfig *openapicommon.Config) error { 304 if s == nil { 305 authenticationInfo.Authenticator = nil 306 return nil 307 } 308 309 cfg := authenticatorfactory.DelegatingAuthenticatorConfig{ 310 Anonymous: &apiserver.AnonymousAuthConfig{Enabled: true}, 311 CacheTTL: s.CacheTTL, 312 WebhookRetryBackoff: s.WebhookRetryBackoff, 313 TokenAccessReviewTimeout: s.TokenRequestTimeout, 314 } 315 316 client, err := s.getClient() 317 if err != nil { 318 return fmt.Errorf("failed to get delegated authentication kubeconfig: %v", err) 319 } 320 321 // configure token review 322 if client != nil { 323 cfg.TokenAccessReviewClient = client.AuthenticationV1() 324 } 325 326 // get the clientCA information 327 clientCASpecified := s.ClientCert != ClientCertAuthenticationOptions{} 328 var clientCAProvider dynamiccertificates.CAContentProvider 329 if clientCASpecified { 330 clientCAProvider, err = s.ClientCert.GetClientCAContentProvider() 331 if err != nil { 332 return fmt.Errorf("unable to load client CA provider: %v", err) 333 } 334 cfg.ClientCertificateCAContentProvider = clientCAProvider 335 if err = authenticationInfo.ApplyClientCert(cfg.ClientCertificateCAContentProvider, servingInfo); err != nil { 336 return fmt.Errorf("unable to assign client CA provider: %v", err) 337 } 338 339 } else if !s.SkipInClusterLookup { 340 if client == nil { 341 klog.Warningf("No authentication-kubeconfig provided in order to lookup client-ca-file in configmap/%s in %s, so client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace) 342 } else { 343 clientCAProvider, err = dynamiccertificates.NewDynamicCAFromConfigMapController("client-ca", authenticationConfigMapNamespace, authenticationConfigMapName, "client-ca-file", client) 344 if err != nil { 345 return fmt.Errorf("unable to load configmap based client CA file: %v", err) 346 } 347 cfg.ClientCertificateCAContentProvider = clientCAProvider 348 if err = authenticationInfo.ApplyClientCert(cfg.ClientCertificateCAContentProvider, servingInfo); err != nil { 349 return fmt.Errorf("unable to assign configmap based client CA file: %v", err) 350 } 351 352 } 353 } 354 355 requestHeaderCAFileSpecified := len(s.RequestHeader.ClientCAFile) > 0 356 var requestHeaderConfig *authenticatorfactory.RequestHeaderConfig 357 if requestHeaderCAFileSpecified { 358 requestHeaderConfig, err = s.RequestHeader.ToAuthenticationRequestHeaderConfig() 359 if err != nil { 360 return fmt.Errorf("unable to create request header authentication config: %v", err) 361 } 362 363 } else if !s.SkipInClusterLookup { 364 if client == nil { 365 klog.Warningf("No authentication-kubeconfig provided in order to lookup requestheader-client-ca-file in configmap/%s in %s, so request-header client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace) 366 } else { 367 requestHeaderConfig, err = s.createRequestHeaderConfig(client) 368 if err != nil { 369 if s.TolerateInClusterLookupFailure { 370 klog.Warningf("Error looking up in-cluster authentication configuration: %v", err) 371 klog.Warning("Continuing without authentication configuration. This may treat all requests as anonymous.") 372 klog.Warning("To require authentication configuration lookup to succeed, set --authentication-tolerate-lookup-failure=false") 373 } else { 374 return fmt.Errorf("unable to load configmap based request-header-client-ca-file: %v", err) 375 } 376 } 377 } 378 } 379 if requestHeaderConfig != nil { 380 cfg.RequestHeaderConfig = requestHeaderConfig 381 authenticationInfo.RequestHeaderConfig = requestHeaderConfig 382 if err = authenticationInfo.ApplyClientCert(cfg.RequestHeaderConfig.CAContentProvider, servingInfo); err != nil { 383 return fmt.Errorf("unable to load request-header-client-ca-file: %v", err) 384 } 385 } 386 387 // create authenticator 388 authenticator, securityDefinitions, err := cfg.New() 389 if err != nil { 390 return err 391 } 392 authenticationInfo.Authenticator = authenticator 393 if openAPIConfig != nil { 394 openAPIConfig.SecurityDefinitions = securityDefinitions 395 } 396 397 return nil 398 } 399 400 const ( 401 authenticationConfigMapNamespace = metav1.NamespaceSystem 402 // authenticationConfigMapName is the name of ConfigMap in the kube-system namespace holding the root certificate 403 // bundle to use to verify client certificates on incoming requests before trusting usernames in headers specified 404 // by --requestheader-username-headers. This is created in the cluster by the kube-apiserver. 405 // "WARNING: generally do not depend on authorization being already done for incoming requests.") 406 authenticationConfigMapName = "extension-apiserver-authentication" 407 ) 408 409 func (s *DelegatingAuthenticationOptions) createRequestHeaderConfig(client kubernetes.Interface) (*authenticatorfactory.RequestHeaderConfig, error) { 410 dynamicRequestHeaderProvider, err := newDynamicRequestHeaderController(client) 411 if err != nil { 412 return nil, fmt.Errorf("unable to create request header authentication config: %v", err) 413 } 414 415 // look up authentication configuration in the cluster and in case of an err defer to authentication-tolerate-lookup-failure flag 416 // We are passing the context to ProxyCerts.RunOnce as it needs to implement RunOnce(ctx) however the 417 // context is not used at all. So passing a empty context shouldn't be a problem 418 ctx := context.TODO() 419 if err := dynamicRequestHeaderProvider.RunOnce(ctx); err != nil { 420 return nil, err 421 } 422 423 return &authenticatorfactory.RequestHeaderConfig{ 424 CAContentProvider: dynamicRequestHeaderProvider, 425 UsernameHeaders: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.UsernameHeaders)), 426 GroupHeaders: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.GroupHeaders)), 427 ExtraHeaderPrefixes: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.ExtraHeaderPrefixes)), 428 AllowedClientNames: headerrequest.StringSliceProvider(headerrequest.StringSliceProviderFunc(dynamicRequestHeaderProvider.AllowedClientNames)), 429 }, nil 430 } 431 432 // getClient returns a Kubernetes clientset. If s.RemoteKubeConfigFileOptional is true, nil will be returned 433 // if no kubeconfig is specified by the user and the in-cluster config is not found. 434 func (s *DelegatingAuthenticationOptions) getClient() (kubernetes.Interface, error) { 435 var clientConfig *rest.Config 436 var err error 437 if len(s.RemoteKubeConfigFile) > 0 { 438 loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: s.RemoteKubeConfigFile} 439 loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) 440 441 clientConfig, err = loader.ClientConfig() 442 } else { 443 // without the remote kubeconfig file, try to use the in-cluster config. Most addon API servers will 444 // use this path. If it is optional, ignore errors. 445 clientConfig, err = rest.InClusterConfig() 446 if err != nil && s.RemoteKubeConfigFileOptional { 447 if err != rest.ErrNotInCluster { 448 klog.Warningf("failed to read in-cluster kubeconfig for delegated authentication: %v", err) 449 } 450 return nil, nil 451 } 452 } 453 if err != nil { 454 return nil, fmt.Errorf("failed to get delegated authentication kubeconfig: %v", err) 455 } 456 457 // set high qps/burst limits since this will effectively limit API server responsiveness 458 clientConfig.QPS = 200 459 clientConfig.Burst = 400 460 // do not set a timeout on the http client, instead use context for cancellation 461 // if multiple timeouts were set, the request will pick the smaller timeout to be applied, leaving other useless. 462 // 463 // see https://github.com/golang/go/blob/a937729c2c2f6950a32bc5cd0f5b88700882f078/src/net/http/client.go#L364 464 if s.CustomRoundTripperFn != nil { 465 clientConfig.Wrap(s.CustomRoundTripperFn) 466 } 467 468 return kubernetes.NewForConfig(clientConfig) 469 }