k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controlplane/controller/clusterauthenticationtrust/cluster_authentication_trust_controller.go (about)

     1  /*
     2  Copyright 2019 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 clusterauthenticationtrust
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"encoding/pem"
    25  	"fmt"
    26  	"reflect"
    27  	"strings"
    28  	"time"
    29  
    30  	corev1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/api/equality"
    32  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    35  	"k8s.io/apimachinery/pkg/util/sets"
    36  	"k8s.io/apimachinery/pkg/util/wait"
    37  	"k8s.io/apiserver/pkg/authentication/request/headerrequest"
    38  	"k8s.io/apiserver/pkg/server/dynamiccertificates"
    39  	corev1informers "k8s.io/client-go/informers/core/v1"
    40  	"k8s.io/client-go/kubernetes"
    41  	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
    42  	corev1listers "k8s.io/client-go/listers/core/v1"
    43  	"k8s.io/client-go/tools/cache"
    44  	"k8s.io/client-go/util/cert"
    45  	"k8s.io/client-go/util/workqueue"
    46  	"k8s.io/klog/v2"
    47  )
    48  
    49  const (
    50  	configMapNamespace = "kube-system"
    51  	configMapName      = "extension-apiserver-authentication"
    52  )
    53  
    54  // Controller holds the running state for the controller
    55  type Controller struct {
    56  	requiredAuthenticationData ClusterAuthenticationInfo
    57  
    58  	configMapLister corev1listers.ConfigMapLister
    59  	configMapClient corev1client.ConfigMapsGetter
    60  	namespaceClient corev1client.NamespacesGetter
    61  
    62  	// queue is where incoming work is placed to de-dup and to allow "easy" rate limited requeues on errors.
    63  	// we only ever place one entry in here, but it is keyed as usual: namespace/name
    64  	queue workqueue.TypedRateLimitingInterface[string]
    65  
    66  	// kubeSystemConfigMapInformer is tracked so that we can start these on Run
    67  	kubeSystemConfigMapInformer cache.SharedIndexInformer
    68  
    69  	// preRunCaches are the caches to sync before starting the work of this control loop
    70  	preRunCaches []cache.InformerSynced
    71  }
    72  
    73  // ClusterAuthenticationInfo holds the information that will included in public configmap.
    74  type ClusterAuthenticationInfo struct {
    75  	// ClientCA is the CA that can be used to verify the identity of normal clients
    76  	ClientCA dynamiccertificates.CAContentProvider
    77  
    78  	// RequestHeaderUsernameHeaders are the headers used by this kube-apiserver to determine username
    79  	RequestHeaderUsernameHeaders headerrequest.StringSliceProvider
    80  	// RequestHeaderGroupHeaders are the headers used by this kube-apiserver to determine groups
    81  	RequestHeaderGroupHeaders headerrequest.StringSliceProvider
    82  	// RequestHeaderExtraHeaderPrefixes are the headers used by this kube-apiserver to determine user.extra
    83  	RequestHeaderExtraHeaderPrefixes headerrequest.StringSliceProvider
    84  	// RequestHeaderAllowedNames are the sujbects allowed to act as a front proxy
    85  	RequestHeaderAllowedNames headerrequest.StringSliceProvider
    86  	// RequestHeaderCA is the CA that can be used to verify the front proxy
    87  	RequestHeaderCA dynamiccertificates.CAContentProvider
    88  }
    89  
    90  // NewClusterAuthenticationTrustController returns a controller that will maintain the kube-system configmap/extension-apiserver-authentication
    91  // that holds information about how to aggregated apiservers are recommended (but not required) to configure themselves.
    92  func NewClusterAuthenticationTrustController(requiredAuthenticationData ClusterAuthenticationInfo, kubeClient kubernetes.Interface) *Controller {
    93  	// we construct our own informer because we need such a small subset of the information available.  Just one namespace.
    94  	kubeSystemConfigMapInformer := corev1informers.NewConfigMapInformer(kubeClient, configMapNamespace, 12*time.Hour, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
    95  
    96  	c := &Controller{
    97  		requiredAuthenticationData: requiredAuthenticationData,
    98  		configMapLister:            corev1listers.NewConfigMapLister(kubeSystemConfigMapInformer.GetIndexer()),
    99  		configMapClient:            kubeClient.CoreV1(),
   100  		namespaceClient:            kubeClient.CoreV1(),
   101  		queue: workqueue.NewTypedRateLimitingQueueWithConfig(
   102  			workqueue.DefaultTypedControllerRateLimiter[string](),
   103  			workqueue.TypedRateLimitingQueueConfig[string]{Name: "cluster_authentication_trust_controller"},
   104  		),
   105  		preRunCaches:                []cache.InformerSynced{kubeSystemConfigMapInformer.HasSynced},
   106  		kubeSystemConfigMapInformer: kubeSystemConfigMapInformer,
   107  	}
   108  
   109  	kubeSystemConfigMapInformer.AddEventHandler(cache.FilteringResourceEventHandler{
   110  		FilterFunc: func(obj interface{}) bool {
   111  			if cast, ok := obj.(*corev1.ConfigMap); ok {
   112  				return cast.Namespace == configMapNamespace && cast.Name == configMapName
   113  			}
   114  			if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
   115  				if cast, ok := tombstone.Obj.(*corev1.ConfigMap); ok {
   116  					return cast.Namespace == configMapNamespace && cast.Name == configMapName
   117  				}
   118  			}
   119  			return true // always return true just in case.  The checks are fairly cheap
   120  		},
   121  		Handler: cache.ResourceEventHandlerFuncs{
   122  			// we have a filter, so any time we're called, we may as well queue. We only ever check one configmap
   123  			// so we don't have to be choosy about our key.
   124  			AddFunc: func(obj interface{}) {
   125  				c.queue.Add(keyFn())
   126  			},
   127  			UpdateFunc: func(oldObj, newObj interface{}) {
   128  				c.queue.Add(keyFn())
   129  			},
   130  			DeleteFunc: func(obj interface{}) {
   131  				c.queue.Add(keyFn())
   132  			},
   133  		},
   134  	})
   135  
   136  	return c
   137  }
   138  
   139  func (c *Controller) syncConfigMap() error {
   140  	originalAuthConfigMap, err := c.configMapLister.ConfigMaps(configMapNamespace).Get(configMapName)
   141  	if apierrors.IsNotFound(err) {
   142  		originalAuthConfigMap = &corev1.ConfigMap{
   143  			ObjectMeta: metav1.ObjectMeta{Namespace: configMapNamespace, Name: configMapName},
   144  		}
   145  	} else if err != nil {
   146  		return err
   147  	}
   148  	// keep the original to diff against later before updating
   149  	authConfigMap := originalAuthConfigMap.DeepCopy()
   150  
   151  	existingAuthenticationInfo, err := getClusterAuthenticationInfoFor(originalAuthConfigMap.Data)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	combinedInfo, err := combinedClusterAuthenticationInfo(existingAuthenticationInfo, c.requiredAuthenticationData)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	authConfigMap.Data, err = getConfigMapDataFor(combinedInfo)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	if equality.Semantic.DeepEqual(authConfigMap, originalAuthConfigMap) {
   165  		klog.V(5).Info("no changes to configmap")
   166  		return nil
   167  	}
   168  	klog.V(2).Infof("writing updated authentication info to  %s configmaps/%s", configMapNamespace, configMapName)
   169  
   170  	if err := createNamespaceIfNeeded(c.namespaceClient, authConfigMap.Namespace); err != nil {
   171  		return err
   172  	}
   173  	if err := writeConfigMap(c.configMapClient, authConfigMap); err != nil {
   174  		return err
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  func createNamespaceIfNeeded(nsClient corev1client.NamespacesGetter, ns string) error {
   181  	if _, err := nsClient.Namespaces().Get(context.TODO(), ns, metav1.GetOptions{}); err == nil {
   182  		// the namespace already exists
   183  		return nil
   184  	}
   185  	newNs := &corev1.Namespace{
   186  		ObjectMeta: metav1.ObjectMeta{
   187  			Name:      ns,
   188  			Namespace: "",
   189  		},
   190  	}
   191  	_, err := nsClient.Namespaces().Create(context.TODO(), newNs, metav1.CreateOptions{})
   192  	if err != nil && apierrors.IsAlreadyExists(err) {
   193  		err = nil
   194  	}
   195  	return err
   196  }
   197  
   198  func writeConfigMap(configMapClient corev1client.ConfigMapsGetter, required *corev1.ConfigMap) error {
   199  	_, err := configMapClient.ConfigMaps(required.Namespace).Update(context.TODO(), required, metav1.UpdateOptions{})
   200  	if apierrors.IsNotFound(err) {
   201  		_, err := configMapClient.ConfigMaps(required.Namespace).Create(context.TODO(), required, metav1.CreateOptions{})
   202  		return err
   203  	}
   204  
   205  	// If the configmap is too big, clear the entire thing and count on this controller (or another one) to add the correct data back.
   206  	// We return the original error which causes the controller to re-queue.
   207  	// Too big means
   208  	//   1. request is so big the generic request catcher finds it
   209  	//   2. the content is so large that that the server sends a validation error "Too long: must have at most 1048576 characters"
   210  	if apierrors.IsRequestEntityTooLargeError(err) || (apierrors.IsInvalid(err) && strings.Contains(err.Error(), "Too long")) {
   211  		if deleteErr := configMapClient.ConfigMaps(required.Namespace).Delete(context.TODO(), required.Name, metav1.DeleteOptions{}); deleteErr != nil {
   212  			return deleteErr
   213  		}
   214  		return err
   215  	}
   216  
   217  	return err
   218  }
   219  
   220  // combinedClusterAuthenticationInfo combines two sets of authentication information into a new one
   221  func combinedClusterAuthenticationInfo(lhs, rhs ClusterAuthenticationInfo) (ClusterAuthenticationInfo, error) {
   222  	ret := ClusterAuthenticationInfo{
   223  		RequestHeaderAllowedNames:        combineUniqueStringSlices(lhs.RequestHeaderAllowedNames, rhs.RequestHeaderAllowedNames),
   224  		RequestHeaderExtraHeaderPrefixes: combineUniqueStringSlices(lhs.RequestHeaderExtraHeaderPrefixes, rhs.RequestHeaderExtraHeaderPrefixes),
   225  		RequestHeaderGroupHeaders:        combineUniqueStringSlices(lhs.RequestHeaderGroupHeaders, rhs.RequestHeaderGroupHeaders),
   226  		RequestHeaderUsernameHeaders:     combineUniqueStringSlices(lhs.RequestHeaderUsernameHeaders, rhs.RequestHeaderUsernameHeaders),
   227  	}
   228  
   229  	var err error
   230  	ret.ClientCA, err = combineCertLists(lhs.ClientCA, rhs.ClientCA)
   231  	if err != nil {
   232  		return ClusterAuthenticationInfo{}, err
   233  	}
   234  	ret.RequestHeaderCA, err = combineCertLists(lhs.RequestHeaderCA, rhs.RequestHeaderCA)
   235  	if err != nil {
   236  		return ClusterAuthenticationInfo{}, err
   237  	}
   238  
   239  	return ret, nil
   240  }
   241  
   242  func getConfigMapDataFor(authenticationInfo ClusterAuthenticationInfo) (map[string]string, error) {
   243  	data := map[string]string{}
   244  	if authenticationInfo.ClientCA != nil {
   245  		if caBytes := authenticationInfo.ClientCA.CurrentCABundleContent(); len(caBytes) > 0 {
   246  			data["client-ca-file"] = string(caBytes)
   247  		}
   248  	}
   249  
   250  	if authenticationInfo.RequestHeaderCA == nil {
   251  		return data, nil
   252  	}
   253  
   254  	if caBytes := authenticationInfo.RequestHeaderCA.CurrentCABundleContent(); len(caBytes) > 0 {
   255  		var err error
   256  
   257  		// encoding errors aren't going to get better, so just fail on them.
   258  		data["requestheader-username-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderUsernameHeaders.Value())
   259  		if err != nil {
   260  			return nil, err
   261  		}
   262  		data["requestheader-group-headers"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderGroupHeaders.Value())
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  		data["requestheader-extra-headers-prefix"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderExtraHeaderPrefixes.Value())
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  
   271  		data["requestheader-client-ca-file"] = string(caBytes)
   272  		data["requestheader-allowed-names"], err = jsonSerializeStringSlice(authenticationInfo.RequestHeaderAllowedNames.Value())
   273  		if err != nil {
   274  			return nil, err
   275  		}
   276  	}
   277  
   278  	return data, nil
   279  }
   280  
   281  func getClusterAuthenticationInfoFor(data map[string]string) (ClusterAuthenticationInfo, error) {
   282  	ret := ClusterAuthenticationInfo{}
   283  
   284  	var err error
   285  	ret.RequestHeaderGroupHeaders, err = jsonDeserializeStringSlice(data["requestheader-group-headers"])
   286  	if err != nil {
   287  		return ClusterAuthenticationInfo{}, err
   288  	}
   289  	ret.RequestHeaderExtraHeaderPrefixes, err = jsonDeserializeStringSlice(data["requestheader-extra-headers-prefix"])
   290  	if err != nil {
   291  		return ClusterAuthenticationInfo{}, err
   292  	}
   293  	ret.RequestHeaderAllowedNames, err = jsonDeserializeStringSlice(data["requestheader-allowed-names"])
   294  	if err != nil {
   295  		return ClusterAuthenticationInfo{}, err
   296  	}
   297  	ret.RequestHeaderUsernameHeaders, err = jsonDeserializeStringSlice(data["requestheader-username-headers"])
   298  	if err != nil {
   299  		return ClusterAuthenticationInfo{}, err
   300  	}
   301  
   302  	if caBundle := data["requestheader-client-ca-file"]; len(caBundle) > 0 {
   303  		ret.RequestHeaderCA, err = dynamiccertificates.NewStaticCAContent("existing", []byte(caBundle))
   304  		if err != nil {
   305  			return ClusterAuthenticationInfo{}, err
   306  		}
   307  	}
   308  
   309  	if caBundle := data["client-ca-file"]; len(caBundle) > 0 {
   310  		ret.ClientCA, err = dynamiccertificates.NewStaticCAContent("existing", []byte(caBundle))
   311  		if err != nil {
   312  			return ClusterAuthenticationInfo{}, err
   313  		}
   314  	}
   315  
   316  	return ret, nil
   317  }
   318  
   319  func jsonSerializeStringSlice(in []string) (string, error) {
   320  	out, err := json.Marshal(in)
   321  	if err != nil {
   322  		return "", err
   323  	}
   324  	return string(out), err
   325  }
   326  
   327  func jsonDeserializeStringSlice(in string) (headerrequest.StringSliceProvider, error) {
   328  	if len(in) == 0 {
   329  		return nil, nil
   330  	}
   331  
   332  	out := []string{}
   333  	if err := json.Unmarshal([]byte(in), &out); err != nil {
   334  		return nil, err
   335  	}
   336  	return headerrequest.StaticStringSlice(out), nil
   337  }
   338  
   339  func combineUniqueStringSlices(lhs, rhs headerrequest.StringSliceProvider) headerrequest.StringSliceProvider {
   340  	ret := []string{}
   341  	present := sets.String{}
   342  
   343  	if lhs != nil {
   344  		for _, curr := range lhs.Value() {
   345  			if present.Has(curr) {
   346  				continue
   347  			}
   348  			ret = append(ret, curr)
   349  			present.Insert(curr)
   350  		}
   351  	}
   352  
   353  	if rhs != nil {
   354  		for _, curr := range rhs.Value() {
   355  			if present.Has(curr) {
   356  				continue
   357  			}
   358  			ret = append(ret, curr)
   359  			present.Insert(curr)
   360  		}
   361  	}
   362  
   363  	return headerrequest.StaticStringSlice(ret)
   364  }
   365  
   366  func combineCertLists(lhs, rhs dynamiccertificates.CAContentProvider) (dynamiccertificates.CAContentProvider, error) {
   367  	certificates := []*x509.Certificate{}
   368  
   369  	if lhs != nil {
   370  		lhsCABytes := lhs.CurrentCABundleContent()
   371  		lhsCAs, err := cert.ParseCertsPEM(lhsCABytes)
   372  		if err != nil {
   373  			return nil, err
   374  		}
   375  		certificates = append(certificates, lhsCAs...)
   376  	}
   377  	if rhs != nil {
   378  		rhsCABytes := rhs.CurrentCABundleContent()
   379  		rhsCAs, err := cert.ParseCertsPEM(rhsCABytes)
   380  		if err != nil {
   381  			return nil, err
   382  		}
   383  		certificates = append(certificates, rhsCAs...)
   384  	}
   385  
   386  	certificates = filterExpiredCerts(certificates...)
   387  
   388  	finalCertificates := []*x509.Certificate{}
   389  	// now check for duplicates. n^2, but super simple
   390  	for i := range certificates {
   391  		found := false
   392  		for j := range finalCertificates {
   393  			if reflect.DeepEqual(certificates[i].Raw, finalCertificates[j].Raw) {
   394  				found = true
   395  				break
   396  			}
   397  		}
   398  		if !found {
   399  			finalCertificates = append(finalCertificates, certificates[i])
   400  		}
   401  	}
   402  
   403  	finalCABytes, err := encodeCertificates(finalCertificates...)
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  
   408  	if len(finalCABytes) == 0 {
   409  		return nil, nil
   410  	}
   411  	// it makes sense for this list to be static because the combination of sources is only used just before writing and
   412  	// is recalculated
   413  	return dynamiccertificates.NewStaticCAContent("combined", finalCABytes)
   414  }
   415  
   416  // filterExpiredCerts checks are all certificates in the bundle valid, i.e. they have not expired.
   417  // The function returns new bundle with only valid certificates or error if no valid certificate is found.
   418  // We allow five minutes of slack for NotAfter comparisons
   419  func filterExpiredCerts(certs ...*x509.Certificate) []*x509.Certificate {
   420  	fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
   421  
   422  	var validCerts []*x509.Certificate
   423  	for _, c := range certs {
   424  		if c.NotAfter.After(fiveMinutesAgo) {
   425  			validCerts = append(validCerts, c)
   426  		}
   427  	}
   428  
   429  	return validCerts
   430  }
   431  
   432  // Enqueue a method to allow separate control loops to cause the controller to trigger and reconcile content.
   433  func (c *Controller) Enqueue() {
   434  	c.queue.Add(keyFn())
   435  }
   436  
   437  // Run the controller until stopped.
   438  func (c *Controller) Run(ctx context.Context, workers int) {
   439  	defer utilruntime.HandleCrash()
   440  	// make sure the work queue is shutdown which will trigger workers to end
   441  	defer c.queue.ShutDown()
   442  
   443  	klog.Infof("Starting cluster_authentication_trust_controller controller")
   444  	defer klog.Infof("Shutting down cluster_authentication_trust_controller controller")
   445  
   446  	// we have a personal informer that is narrowly scoped, start it.
   447  	go c.kubeSystemConfigMapInformer.Run(ctx.Done())
   448  
   449  	// wait for your secondary caches to fill before starting your work
   450  	if !cache.WaitForNamedCacheSync("cluster_authentication_trust_controller", ctx.Done(), c.preRunCaches...) {
   451  		return
   452  	}
   453  
   454  	// only run one worker
   455  	go wait.Until(c.runWorker, time.Second, ctx.Done())
   456  
   457  	// checks are cheap.  run once a minute just to be sure we stay in sync in case fsnotify fails again
   458  	// start timer that rechecks every minute, just in case.  this also serves to prime the controller quickly.
   459  	_ = wait.PollImmediateUntil(1*time.Minute, func() (bool, error) {
   460  		c.queue.Add(keyFn())
   461  		return false, nil
   462  	}, ctx.Done())
   463  
   464  	// wait until we're told to stop
   465  	<-ctx.Done()
   466  }
   467  
   468  func (c *Controller) runWorker() {
   469  	// hot loop until we're told to stop.  processNextWorkItem will automatically wait until there's work
   470  	// available, so we don't worry about secondary waits
   471  	for c.processNextWorkItem() {
   472  	}
   473  }
   474  
   475  // processNextWorkItem deals with one key off the queue.  It returns false when it's time to quit.
   476  func (c *Controller) processNextWorkItem() bool {
   477  	// pull the next work item from queue.  It should be a key we use to lookup something in a cache
   478  	key, quit := c.queue.Get()
   479  	if quit {
   480  		return false
   481  	}
   482  	// you always have to indicate to the queue that you've completed a piece of work
   483  	defer c.queue.Done(key)
   484  
   485  	// do your work on the key.  This method will contains your "do stuff" logic
   486  	err := c.syncConfigMap()
   487  	if err == nil {
   488  		// if you had no error, tell the queue to stop tracking history for your key.  This will
   489  		// reset things like failure counts for per-item rate limiting
   490  		c.queue.Forget(key)
   491  		return true
   492  	}
   493  
   494  	// there was a failure so be sure to report it.  This method allows for pluggable error handling
   495  	// which can be used for things like cluster-monitoring
   496  	utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err))
   497  	// since we failed, we should requeue the item to work on later.  This method will add a backoff
   498  	// to avoid hotlooping on particular items (they're probably still not going to work right away)
   499  	// and overall controller protection (everything I've done is broken, this controller needs to
   500  	// calm down or it can starve other useful work) cases.
   501  	c.queue.AddRateLimited(key)
   502  
   503  	return true
   504  }
   505  
   506  func keyFn() string {
   507  	// this format matches DeletionHandlingMetaNamespaceKeyFunc for our single key
   508  	return configMapNamespace + "/" + configMapName
   509  }
   510  
   511  func encodeCertificates(certs ...*x509.Certificate) ([]byte, error) {
   512  	b := bytes.Buffer{}
   513  	for _, cert := range certs {
   514  		if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
   515  			return []byte{}, err
   516  		}
   517  	}
   518  	return b.Bytes(), nil
   519  }