...

Source file src/k8s.io/kubernetes/pkg/controller/serviceaccount/legacy_serviceaccount_token_cleaner.go

Documentation: k8s.io/kubernetes/pkg/controller/serviceaccount

     1  /*
     2  Copyright 2023 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 serviceaccount
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"time"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/labels"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	applyv1 "k8s.io/client-go/applyconfigurations/core/v1"
    34  	coreinformers "k8s.io/client-go/informers/core/v1"
    35  	clientset "k8s.io/client-go/kubernetes"
    36  	listersv1 "k8s.io/client-go/listers/core/v1"
    37  	"k8s.io/client-go/tools/cache"
    38  	"k8s.io/klog/v2"
    39  	podutil "k8s.io/kubernetes/pkg/api/v1/pod"
    40  	"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
    41  	"k8s.io/kubernetes/pkg/serviceaccount"
    42  	"k8s.io/utils/clock"
    43  )
    44  
    45  const (
    46  	dateFormat                 = "2006-01-02"
    47  	DefaultCleanerSyncInterval = 24 * time.Hour
    48  )
    49  
    50  // TokenCleanerOptions contains options for the LegacySATokenCleaner
    51  type LegacySATokenCleanerOptions struct {
    52  	// CleanUpPeriod is the period of time since the last usage of an legacy token before it can be deleted.
    53  	CleanUpPeriod time.Duration
    54  	SyncInterval  time.Duration
    55  }
    56  
    57  // LegacySATokenCleaner is a controller that deletes legacy serviceaccount tokens that are not in use for a specified period of time.
    58  type LegacySATokenCleaner struct {
    59  	client           clientset.Interface
    60  	clock            clock.Clock
    61  	saLister         listersv1.ServiceAccountLister
    62  	saInformerSynced cache.InformerSynced
    63  
    64  	secretLister         listersv1.SecretLister
    65  	secretInformerSynced cache.InformerSynced
    66  
    67  	podLister         listersv1.PodLister
    68  	podInformerSynced cache.InformerSynced
    69  
    70  	syncInterval         time.Duration
    71  	minimumSinceLastUsed time.Duration
    72  }
    73  
    74  // NewLegacySATokenCleaner returns a new *NewLegacySATokenCleaner.
    75  func NewLegacySATokenCleaner(saInformer coreinformers.ServiceAccountInformer, secretInformer coreinformers.SecretInformer, podInformer coreinformers.PodInformer, client clientset.Interface, cl clock.Clock, options LegacySATokenCleanerOptions) (*LegacySATokenCleaner, error) {
    76  	if !(options.CleanUpPeriod > 0) {
    77  		return nil, fmt.Errorf("invalid CleanUpPeriod: %v", options.CleanUpPeriod)
    78  	}
    79  	if !(options.SyncInterval > 0) {
    80  		return nil, fmt.Errorf("invalid SyncInterval: %v", options.SyncInterval)
    81  	}
    82  
    83  	tc := &LegacySATokenCleaner{
    84  		client:               client,
    85  		clock:                cl,
    86  		saLister:             saInformer.Lister(),
    87  		saInformerSynced:     saInformer.Informer().HasSynced,
    88  		secretLister:         secretInformer.Lister(),
    89  		secretInformerSynced: secretInformer.Informer().HasSynced,
    90  		podLister:            podInformer.Lister(),
    91  		podInformerSynced:    podInformer.Informer().HasSynced,
    92  		minimumSinceLastUsed: options.CleanUpPeriod,
    93  		syncInterval:         options.SyncInterval,
    94  	}
    95  
    96  	return tc, nil
    97  }
    98  
    99  func (tc *LegacySATokenCleaner) Run(ctx context.Context) {
   100  	defer utilruntime.HandleCrash()
   101  
   102  	logger := klog.FromContext(ctx)
   103  	logger.Info("Starting legacy service account token cleaner controller")
   104  	defer logger.Info("Shutting down legacy service account token cleaner controller")
   105  
   106  	if !cache.WaitForNamedCacheSync("legacy-service-account-token-cleaner", ctx.Done(), tc.saInformerSynced, tc.secretInformerSynced, tc.podInformerSynced) {
   107  		return
   108  	}
   109  
   110  	go wait.UntilWithContext(ctx, tc.evaluateSATokens, tc.syncInterval)
   111  
   112  	<-ctx.Done()
   113  }
   114  
   115  func (tc *LegacySATokenCleaner) evaluateSATokens(ctx context.Context) {
   116  	logger := klog.FromContext(ctx)
   117  
   118  	now := tc.clock.Now().UTC()
   119  	trackedSince, err := tc.latestPossibleTrackedSinceTime(ctx)
   120  	if err != nil {
   121  		logger.Error(err, "Getting lastest possible tracked_since time")
   122  		return
   123  	}
   124  
   125  	if now.Before(trackedSince.Add(tc.minimumSinceLastUsed)) {
   126  		// we haven't been tracking long enough
   127  		return
   128  	}
   129  
   130  	preserveCreatedOnOrAfter := now.Add(-tc.minimumSinceLastUsed)
   131  	preserveUsedOnOrAfter := now.Add(-tc.minimumSinceLastUsed).Format(dateFormat)
   132  
   133  	secretList, err := tc.secretLister.Secrets(metav1.NamespaceAll).List(labels.Everything())
   134  	if err != nil {
   135  		logger.Error(err, "Getting cached secret list")
   136  		return
   137  	}
   138  
   139  	namespaceToUsedSecretNames := make(map[string]sets.String)
   140  	for _, secret := range secretList {
   141  		if secret.Type != v1.SecretTypeServiceAccountToken {
   142  			continue
   143  		}
   144  		if !secret.CreationTimestamp.Time.Before(preserveCreatedOnOrAfter) {
   145  			continue
   146  		}
   147  
   148  		if secret.DeletionTimestamp != nil {
   149  			continue
   150  		}
   151  
   152  		// if LastUsedLabelKey does not exist, we think the secret has not been used
   153  		// since the legacy token starts to track.
   154  		lastUsed, ok := secret.Labels[serviceaccount.LastUsedLabelKey]
   155  		if ok {
   156  			_, err := time.Parse(dateFormat, lastUsed)
   157  			if err != nil {
   158  				// the lastUsed value is not well-formed thus we cannot determine it
   159  				logger.Error(err, "Parsing lastUsed time", "secret", klog.KRef(secret.Namespace, secret.Name))
   160  				continue
   161  			}
   162  			if lastUsed >= preserveUsedOnOrAfter {
   163  				continue
   164  			}
   165  		}
   166  
   167  		sa, saErr := tc.getServiceAccount(secret)
   168  
   169  		if saErr != nil {
   170  			logger.Error(saErr, "Getting service account", "secret", klog.KRef(secret.Namespace, secret.Name))
   171  			continue
   172  		}
   173  		if sa == nil || !hasSecretReference(sa, secret.Name) {
   174  			// can't determine if this is an auto-generated token
   175  			continue
   176  		}
   177  
   178  		mountedSecretNames, err := tc.getMountedSecretNames(secret.Namespace, namespaceToUsedSecretNames)
   179  		if err != nil {
   180  			logger.Error(err, "Resolving mounted secrets", "secret", klog.KRef(secret.Namespace, secret.Name))
   181  			continue
   182  		}
   183  		if mountedSecretNames.Has(secret.Name) {
   184  			// still used by pods
   185  			continue
   186  		}
   187  
   188  		invalidSince := secret.Labels[serviceaccount.InvalidSinceLabelKey]
   189  		// If the secret has not been labeled with invalid since date or the label value has invalid format, update the invalidSince label with the current date value.
   190  		_, err = time.Parse(dateFormat, invalidSince)
   191  		if err != nil {
   192  			invalidSince = now.Format(dateFormat)
   193  			logger.Info("Mark the auto-generated service account token as invalid", "invalidSince", invalidSince, "secret", klog.KRef(secret.Namespace, secret.Name))
   194  			patchContent, err := json.Marshal(applyv1.Secret(secret.Name, secret.Namespace).WithUID(secret.UID).WithLabels(map[string]string{serviceaccount.InvalidSinceLabelKey: invalidSince}))
   195  			if err != nil {
   196  				logger.Error(err, "Failed to marshal invalid since label")
   197  			} else {
   198  				if _, err := tc.client.CoreV1().Secrets(secret.Namespace).Patch(ctx, secret.Name, types.MergePatchType, patchContent, metav1.PatchOptions{}); err != nil {
   199  					logger.Error(err, "Failed to label legacy service account token secret with invalid since date")
   200  				}
   201  			}
   202  			continue
   203  		}
   204  
   205  		if invalidSince >= preserveUsedOnOrAfter {
   206  			continue
   207  		}
   208  
   209  		logger.Info("Delete auto-generated service account token", "secret", klog.KRef(secret.Namespace, secret.Name), "creationTime", secret.CreationTimestamp, "lastUsed", lastUsed, "invalidSince", invalidSince)
   210  		if err := tc.client.CoreV1().Secrets(secret.Namespace).Delete(ctx, secret.Name, metav1.DeleteOptions{Preconditions: &metav1.Preconditions{ResourceVersion: &secret.ResourceVersion}}); err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) {
   211  			logger.Error(err, "Deleting legacy service account token", "secret", klog.KRef(secret.Namespace, secret.Name), "serviceaccount", sa.Name)
   212  		}
   213  	}
   214  }
   215  
   216  func (tc *LegacySATokenCleaner) getMountedSecretNames(secretNamespace string, namespaceToUsedSecretNames map[string]sets.String) (sets.String, error) {
   217  	if secrets, ok := namespaceToUsedSecretNames[secretNamespace]; ok {
   218  		return secrets, nil
   219  	}
   220  
   221  	podList, err := tc.podLister.Pods(secretNamespace).List(labels.Everything())
   222  	if err != nil {
   223  		return nil, fmt.Errorf("failed to get pod list from pod cache: %v", err)
   224  	}
   225  
   226  	var secrets sets.String
   227  	for _, pod := range podList {
   228  		podutil.VisitPodSecretNames(pod, func(secretName string) bool {
   229  			if secrets == nil {
   230  				secrets = sets.NewString()
   231  			}
   232  			secrets.Insert(secretName)
   233  			return true
   234  		})
   235  	}
   236  	if secrets != nil {
   237  		namespaceToUsedSecretNames[secretNamespace] = secrets
   238  	}
   239  	return secrets, nil
   240  }
   241  
   242  func (tc *LegacySATokenCleaner) getServiceAccount(secret *v1.Secret) (*v1.ServiceAccount, error) {
   243  	saName := secret.Annotations[v1.ServiceAccountNameKey]
   244  	if len(saName) == 0 {
   245  		return nil, nil
   246  	}
   247  	saUID := types.UID(secret.Annotations[v1.ServiceAccountUIDKey])
   248  	sa, err := tc.saLister.ServiceAccounts(secret.Namespace).Get(saName)
   249  	if apierrors.IsNotFound(err) {
   250  		return nil, nil
   251  	}
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	// Ensure UID matches if given
   257  	if len(saUID) == 0 || saUID == sa.UID {
   258  		return sa, nil
   259  	}
   260  
   261  	return nil, nil
   262  }
   263  
   264  // get the latest possible TrackedSince time information from the configMap label.
   265  func (tc *LegacySATokenCleaner) latestPossibleTrackedSinceTime(ctx context.Context) (time.Time, error) {
   266  	configMap, err := tc.client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, legacytokentracking.ConfigMapName, metav1.GetOptions{})
   267  	if err != nil {
   268  		return time.Time{}, err
   269  	}
   270  	trackedSince, exist := configMap.Data[legacytokentracking.ConfigMapDataKey]
   271  	if !exist {
   272  		return time.Time{}, fmt.Errorf("configMap does not have since label")
   273  	}
   274  	trackedSinceTime, err := time.Parse(dateFormat, trackedSince)
   275  	if err != nil {
   276  		return time.Time{}, fmt.Errorf("error parsing trackedSince time: %v", err)
   277  	}
   278  	// make sure the time to be 00:00 on the day just after the date starts to track
   279  	return trackedSinceTime.AddDate(0, 0, 1), nil
   280  }
   281  
   282  func hasSecretReference(serviceAccount *v1.ServiceAccount, secretName string) bool {
   283  	for _, secret := range serviceAccount.Secrets {
   284  		if secret.Name == secretName {
   285  			return true
   286  		}
   287  	}
   288  	return false
   289  }
   290  

View as plain text