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  }