...

Source file src/k8s.io/kubernetes/test/integration/serviceaccount/legacy_service_account_token_clean_up_test.go

Documentation: k8s.io/kubernetes/test/integration/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  // This file tests the legacy service account token cleaning-up.
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"testing"
    26  	"time"
    27  
    28  	v1 "k8s.io/api/core/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	applyv1 "k8s.io/client-go/applyconfigurations/core/v1"
    34  	clientinformers "k8s.io/client-go/informers"
    35  	clientset "k8s.io/client-go/kubernetes"
    36  	listersv1 "k8s.io/client-go/listers/core/v1"
    37  	serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
    38  	"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
    39  	"k8s.io/kubernetes/pkg/serviceaccount"
    40  	"k8s.io/utils/clock"
    41  	testingclock "k8s.io/utils/clock/testing"
    42  )
    43  
    44  const (
    45  	dateFormat    = "2006-01-02"
    46  	cleanUpPeriod = 24 * time.Hour
    47  	syncInterval  = 5 * time.Second
    48  	pollTimeout   = 15 * time.Second
    49  	pollInterval  = time.Second
    50  )
    51  
    52  func TestLegacyServiceAccountTokenCleanUp(t *testing.T) {
    53  	ctx, cancel := context.WithCancel(context.Background())
    54  	defer cancel()
    55  	c, config, stopFunc, informers, err := startServiceAccountTestServerAndWaitForCaches(ctx, t)
    56  	defer stopFunc()
    57  	if err != nil {
    58  		t.Fatalf("failed to setup ServiceAccounts server: %v", err)
    59  	}
    60  
    61  	// wait configmap to be labeled with tracking date
    62  	waitConfigmapToBeLabeled(ctx, t, c)
    63  
    64  	tests := []struct {
    65  		name               string
    66  		secretName         string
    67  		secretTokenData    string
    68  		namespace          string
    69  		expectCleanedUp    bool
    70  		expectInvalidLabel bool
    71  		lastUsedLabel      bool
    72  		isPodMounted       bool
    73  		isManual           bool
    74  	}{
    75  		{
    76  			name:               "auto created legacy token without pod binding",
    77  			secretName:         "auto-token-without-pod-mounting-a",
    78  			namespace:          "clean-ns-1",
    79  			lastUsedLabel:      true,
    80  			isManual:           false,
    81  			isPodMounted:       false,
    82  			expectCleanedUp:    true,
    83  			expectInvalidLabel: true,
    84  		},
    85  		{
    86  			name:               "manually created legacy token",
    87  			secretName:         "manual-token",
    88  			namespace:          "clean-ns-2",
    89  			lastUsedLabel:      true,
    90  			isManual:           true,
    91  			isPodMounted:       false,
    92  			expectCleanedUp:    false,
    93  			expectInvalidLabel: false,
    94  		},
    95  		{
    96  			name:               "auto created legacy token with pod binding",
    97  			secretName:         "auto-token-with-pod-mounting",
    98  			namespace:          "clean-ns-3",
    99  			lastUsedLabel:      true,
   100  			isManual:           false,
   101  			isPodMounted:       true,
   102  			expectCleanedUp:    false,
   103  			expectInvalidLabel: false,
   104  		},
   105  		{
   106  			name:               "auto created legacy token without pod binding, secret has not been used after tracking",
   107  			secretName:         "auto-token-without-pod-mounting-b",
   108  			namespace:          "clean-ns-4",
   109  			lastUsedLabel:      false,
   110  			isManual:           false,
   111  			isPodMounted:       false,
   112  			expectCleanedUp:    true,
   113  			expectInvalidLabel: true,
   114  		},
   115  	}
   116  	for _, test := range tests {
   117  		t.Run(test.name, func(t *testing.T) {
   118  
   119  			fakeClock := testingclock.NewFakeClock(time.Now().UTC())
   120  
   121  			// start legacy service account token cleaner
   122  			ctxForCleaner, cancelFunc := context.WithCancel(context.Background())
   123  			startLegacyServiceAccountTokenCleaner(ctxForCleaner, c, fakeClock, informers)
   124  			informers.Start(ctx.Done())
   125  			defer cancelFunc()
   126  
   127  			// create service account
   128  			_, err = c.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: test.namespace}}, metav1.CreateOptions{})
   129  			if err != nil && !apierrors.IsAlreadyExists(err) {
   130  				t.Fatalf("could not create namespace: %v", err)
   131  			}
   132  			mysa, err := c.CoreV1().ServiceAccounts(test.namespace).Create(context.TODO(), &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: readOnlyServiceAccountName}}, metav1.CreateOptions{})
   133  			if err != nil {
   134  				t.Fatalf("Service Account not created: %v", err)
   135  			}
   136  
   137  			// create secret
   138  			secret, err := createServiceAccountToken(c, mysa, test.namespace, test.secretName)
   139  			if err != nil {
   140  				t.Fatalf("Secret not created: %v", err)
   141  			}
   142  			if !test.isManual {
   143  				if err := addReferencedServiceAccountToken(c, test.namespace, readOnlyServiceAccountName, secret); err != nil {
   144  					t.Fatal(err)
   145  				}
   146  			}
   147  			podLister := informers.Core().V1().Pods().Lister()
   148  			if test.isPodMounted {
   149  				createAutotokenMountedPod(ctx, t, c, test.namespace, test.secretName, podLister)
   150  			}
   151  
   152  			myConfig := *config
   153  			wh := &warningHandler{}
   154  			myConfig.WarningHandler = wh
   155  			myConfig.BearerToken = string(string(secret.Data[v1.ServiceAccountTokenKey]))
   156  			roClient := clientset.NewForConfigOrDie(&myConfig)
   157  
   158  			// the secret should not be labeled with LastUsedLabelKey.
   159  			checkLastUsedLabel(ctx, t, c, secret, false)
   160  
   161  			if test.lastUsedLabel {
   162  				doServiceAccountAPIReadRequest(ctx, t, roClient, test.namespace, true)
   163  
   164  				// all service account tokens should be labeled with LastUsedLabelKey.
   165  				checkLastUsedLabel(ctx, t, c, secret, true)
   166  			}
   167  
   168  			// Test invalid labels
   169  			fakeClock.Step(cleanUpPeriod + 24*time.Hour)
   170  			checkInvalidSinceLabel(ctx, t, c, secret, fakeClock, test.expectInvalidLabel)
   171  
   172  			// Test invalid secret cannot be used
   173  			if test.expectInvalidLabel {
   174  				t.Logf("Check the invalid token cannot authenticate request.")
   175  				doServiceAccountAPIReadRequest(ctx, t, roClient, test.namespace, false)
   176  
   177  				// Check the secret has been labelded with the LastUsedLabelKey.
   178  				if !test.lastUsedLabel {
   179  					checkLastUsedLabel(ctx, t, c, secret, true)
   180  				}
   181  
   182  				// Update secret by removing the invalid since label
   183  				removeInvalidLabel(ctx, c, t, secret)
   184  
   185  				t.Logf("Check the token can authenticate request after patching the secret by removing the invalid label.")
   186  				doServiceAccountAPIReadRequest(ctx, t, roClient, test.namespace, true)
   187  
   188  				// Update the lastUsed label date to the fakeClock date (as the validation function uses the real time to label the lastUsed date)
   189  				patchSecret(ctx, c, t, fakeClock.Now().UTC().Format(dateFormat), secret)
   190  
   191  				// The secret will be marked as invalid again after time period duration cleanUpPeriod + 24*time.Hour
   192  				fakeClock.Step(cleanUpPeriod + 24*time.Hour)
   193  				checkInvalidSinceLabel(ctx, t, c, secret, fakeClock, true)
   194  			}
   195  
   196  			fakeClock.Step(cleanUpPeriod + 24*time.Hour)
   197  			checkSecretCleanUp(ctx, t, c, secret, test.expectCleanedUp)
   198  		})
   199  	}
   200  }
   201  
   202  func waitConfigmapToBeLabeled(ctx context.Context, t *testing.T, c clientset.Interface) {
   203  	if err := wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   204  		configMap, err := c.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, legacytokentracking.ConfigMapName, metav1.GetOptions{})
   205  		if err != nil {
   206  			return false, err
   207  		}
   208  		_, exist := configMap.Data[legacytokentracking.ConfigMapDataKey]
   209  		if !exist {
   210  			return false, fmt.Errorf("configMap does not have since label")
   211  		}
   212  		return true, nil
   213  	}); err != nil {
   214  		t.Fatalf("failed to wait configmap starts to track: %v", err)
   215  	}
   216  }
   217  
   218  func checkSecretCleanUp(ctx context.Context, t *testing.T, c clientset.Interface, secret *v1.Secret, shouldCleanUp bool) {
   219  	err := wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   220  		_, err := c.CoreV1().Secrets(secret.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{})
   221  		if shouldCleanUp {
   222  			if err == nil {
   223  				return false, nil
   224  			} else if !apierrors.IsNotFound(err) {
   225  				t.Fatalf("Failed to get secret %s, err: %v", secret.Name, err)
   226  			}
   227  			return true, nil
   228  		}
   229  		if err != nil {
   230  			if apierrors.IsNotFound(err) {
   231  				t.Fatalf("The secret %s should not be cleaned up, err: %v", secret.Name, err)
   232  			} else {
   233  				t.Fatalf("Failed to get secret %s, err: %v", secret.Name, err)
   234  			}
   235  		}
   236  		return true, nil
   237  	})
   238  	if err != nil {
   239  		t.Fatalf("Failed to check the existence for secret: %s, shouldCleanUp: %v, error: %v", secret.Name, shouldCleanUp, err)
   240  	}
   241  }
   242  
   243  func checkInvalidSinceLabel(ctx context.Context, t *testing.T, c clientset.Interface, secret *v1.Secret, fakeClock *testingclock.FakeClock, shouldLabel bool) {
   244  	err := wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   245  		liveSecret, err := c.CoreV1().Secrets(secret.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{})
   246  		if err != nil {
   247  			t.Fatalf("Failed to get secret: %s, err: %v", secret.Name, err)
   248  		}
   249  		invalidSince, ok := liveSecret.GetLabels()[serviceaccount.InvalidSinceLabelKey]
   250  		if shouldLabel {
   251  			if !ok || invalidSince != fakeClock.Now().UTC().Format(dateFormat) {
   252  				return false, nil
   253  			}
   254  			return true, nil
   255  		}
   256  		if invalidSince != "" {
   257  			return false, nil
   258  		}
   259  		return true, nil
   260  	})
   261  
   262  	if err != nil {
   263  		t.Fatalf("Failed to check secret invalid since label for secret: %s, shouldLabel: %v, error: %v", secret.Name, shouldLabel, err)
   264  	}
   265  }
   266  
   267  func checkLastUsedLabel(ctx context.Context, t *testing.T, c clientset.Interface, secret *v1.Secret, shouldLabel bool) {
   268  	err := wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   269  		liveSecret, err := c.CoreV1().Secrets(secret.Namespace).Get(ctx, secret.Name, metav1.GetOptions{})
   270  		if err != nil {
   271  			t.Fatalf("Failed to get secret: %s, err: %v", secret.Name, err)
   272  		}
   273  		lastUsed, ok := liveSecret.GetLabels()[serviceaccount.LastUsedLabelKey]
   274  		if shouldLabel {
   275  			if !ok || lastUsed != time.Now().UTC().Format(dateFormat) {
   276  				return false, nil
   277  			}
   278  			t.Logf("The secret %s has been labeled with %s", secret.Name, lastUsed)
   279  			return true, nil
   280  		}
   281  		if ok {
   282  			t.Fatalf("Secret %s should not have the lastUsed label", secret.Name)
   283  		}
   284  		return true, nil
   285  	})
   286  	if err != nil {
   287  		t.Fatalf("Failed to check secret last used label for secret: %s, shouldLabel: %v, error: %v", secret.Name, shouldLabel, err)
   288  	}
   289  }
   290  
   291  func removeInvalidLabel(ctx context.Context, c clientset.Interface, t *testing.T, secret *v1.Secret) {
   292  	lastUsed := secret.GetLabels()[serviceaccount.LastUsedLabelKey]
   293  	patchContent, err := json.Marshal(applyv1.Secret(secret.Name, secret.Namespace).WithLabels(map[string]string{serviceaccount.InvalidSinceLabelKey: "", serviceaccount.LastUsedLabelKey: lastUsed}))
   294  	if err != nil {
   295  		t.Fatalf("Failed to marshal invalid since label, err: %v", err)
   296  	}
   297  	t.Logf("Patch the secret by removing the invalid label.")
   298  	if _, err := c.CoreV1().Secrets(secret.Namespace).Patch(ctx, secret.Name, types.MergePatchType, patchContent, metav1.PatchOptions{}); err != nil {
   299  		t.Fatalf("Failed to remove invalid since label, err: %v", err)
   300  	}
   301  	err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   302  		secret, err = c.CoreV1().Secrets(secret.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{})
   303  		if err != nil {
   304  			t.Fatalf("Failed to get secret: %s, err: %v", secret.Name, err)
   305  		}
   306  		invalidSince := secret.GetLabels()[serviceaccount.InvalidSinceLabelKey]
   307  		if invalidSince != "" {
   308  			t.Log("Patch has not completed.")
   309  			return false, nil
   310  		}
   311  		return true, nil
   312  	})
   313  	if err != nil {
   314  		t.Fatalf("Failed to patch secret: %s, err: %v", secret.Name, err)
   315  	}
   316  }
   317  
   318  func patchSecret(ctx context.Context, c clientset.Interface, t *testing.T, lastUsed string, secret *v1.Secret) {
   319  	patchContent, err := json.Marshal(applyv1.Secret(secret.Name, secret.Namespace).WithUID(secret.UID).WithLabels(map[string]string{serviceaccount.InvalidSinceLabelKey: "", serviceaccount.LastUsedLabelKey: lastUsed}))
   320  	if err != nil {
   321  		t.Fatalf("Failed to marshal invalid since label, err: %v", err)
   322  	}
   323  	t.Logf("Patch the secret by removing the invalid label.")
   324  	if _, err := c.CoreV1().Secrets(secret.Namespace).Patch(ctx, secret.Name, types.MergePatchType, patchContent, metav1.PatchOptions{}); err != nil {
   325  		t.Fatalf("Failed to remove invalid since label, err: %v", err)
   326  	}
   327  	err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   328  		secret, err = c.CoreV1().Secrets(secret.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{})
   329  		if err != nil {
   330  			t.Fatalf("Failed to get secret: %s, err: %v", secret.Name, err)
   331  		}
   332  		lastUsedString := secret.GetLabels()[serviceaccount.LastUsedLabelKey]
   333  		if lastUsedString != lastUsed {
   334  			t.Log("Patch has not completed.")
   335  			return false, nil
   336  		}
   337  		return true, nil
   338  	})
   339  	if err != nil {
   340  		t.Fatalf("Failed to patch secret: %s, err: %v", secret.Name, err)
   341  	}
   342  }
   343  
   344  func startLegacyServiceAccountTokenCleaner(ctx context.Context, client clientset.Interface, fakeClock clock.Clock, informers clientinformers.SharedInformerFactory) {
   345  	legacySATokenCleaner, _ := serviceaccountcontroller.NewLegacySATokenCleaner(
   346  		informers.Core().V1().ServiceAccounts(),
   347  		informers.Core().V1().Secrets(),
   348  		informers.Core().V1().Pods(),
   349  		client,
   350  		fakeClock,
   351  		serviceaccountcontroller.LegacySATokenCleanerOptions{
   352  			SyncInterval:  syncInterval,
   353  			CleanUpPeriod: cleanUpPeriod,
   354  		})
   355  	go legacySATokenCleaner.Run(ctx)
   356  }
   357  
   358  func doServiceAccountAPIReadRequest(ctx context.Context, t *testing.T, c clientset.Interface, ns string, authenticated bool) {
   359  	readOps := []testOperation{
   360  		func() error {
   361  			_, err := c.CoreV1().Secrets(ns).List(context.TODO(), metav1.ListOptions{})
   362  			return err
   363  		},
   364  		func() error {
   365  			_, err := c.CoreV1().Pods(ns).List(context.TODO(), metav1.ListOptions{})
   366  			return err
   367  		},
   368  	}
   369  
   370  	for _, op := range readOps {
   371  		err := wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   372  			err := op()
   373  			if authenticated && err != nil || !authenticated && err == nil {
   374  				return false, nil
   375  			}
   376  			return true, nil
   377  		})
   378  		if err != nil {
   379  			t.Fatalf("Failed to check secret token authentication: error: %v", err)
   380  		}
   381  	}
   382  }
   383  
   384  func createAutotokenMountedPod(ctx context.Context, t *testing.T, c clientset.Interface, ns, secretName string, podLister listersv1.PodLister) *v1.Pod {
   385  	pod := &v1.Pod{
   386  		ObjectMeta: metav1.ObjectMeta{
   387  			Name:      "token-bound-pod",
   388  			Namespace: ns,
   389  		},
   390  		Spec: v1.PodSpec{
   391  			Containers: []v1.Container{
   392  				{Name: "name", Image: "image"},
   393  			},
   394  			Volumes: []v1.Volume{{Name: "foo", VolumeSource: v1.VolumeSource{Secret: &v1.SecretVolumeSource{SecretName: secretName}}}},
   395  		},
   396  	}
   397  	pod, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
   398  	if err != nil {
   399  		t.Fatalf("Failed to create pod with token (%s:%s) bound, err: %v", ns, secretName, err)
   400  	}
   401  	err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, func(ctx context.Context) (bool, error) {
   402  		pod, err = podLister.Pods(ns).Get("token-bound-pod")
   403  		if err != nil {
   404  			return false, nil
   405  		}
   406  		return true, nil
   407  	})
   408  	if err != nil {
   409  		t.Fatalf("Failed to wait auto-token mounted pod: err: %v", err)
   410  	}
   411  	return pod
   412  }
   413  

View as plain text