267 lines
8.8 KiB
Go
267 lines
8.8 KiB
Go
/*
|
|
Copyright 2023 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package serviceaccount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
coreinformers "k8s.io/client-go/informers/core/v1"
|
|
clientset "k8s.io/client-go/kubernetes"
|
|
listersv1 "k8s.io/client-go/listers/core/v1"
|
|
"k8s.io/client-go/tools/cache"
|
|
"k8s.io/klog/v2"
|
|
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
|
"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
|
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
|
"k8s.io/utils/clock"
|
|
)
|
|
|
|
const (
|
|
dateFormat = "2006-01-02"
|
|
DefaultCleanerSyncInterval = 24 * time.Hour
|
|
)
|
|
|
|
// TokenCleanerOptions contains options for the LegacySATokenCleaner
|
|
type LegacySATokenCleanerOptions struct {
|
|
// CleanUpPeriod is the period of time since the last usage of an legacy token before it can be deleted.
|
|
CleanUpPeriod time.Duration
|
|
SyncInterval time.Duration
|
|
}
|
|
|
|
// LegacySATokenCleaner is a controller that deletes legacy serviceaccount tokens that are not in use for a specified period of time.
|
|
type LegacySATokenCleaner struct {
|
|
client clientset.Interface
|
|
clock clock.Clock
|
|
saLister listersv1.ServiceAccountLister
|
|
saInformerSynced cache.InformerSynced
|
|
|
|
secretLister listersv1.SecretLister
|
|
secretInformerSynced cache.InformerSynced
|
|
|
|
podLister listersv1.PodLister
|
|
podInformerSynced cache.InformerSynced
|
|
|
|
syncInterval time.Duration
|
|
minimumSinceLastUsed time.Duration
|
|
}
|
|
|
|
// NewLegacySATokenCleaner returns a new *NewLegacySATokenCleaner.
|
|
func NewLegacySATokenCleaner(saInformer coreinformers.ServiceAccountInformer, secretInformer coreinformers.SecretInformer, podInformer coreinformers.PodInformer, client clientset.Interface, cl clock.Clock, options LegacySATokenCleanerOptions) (*LegacySATokenCleaner, error) {
|
|
if !(options.CleanUpPeriod > 0) {
|
|
return nil, fmt.Errorf("invalid CleanUpPeriod: %v", options.CleanUpPeriod)
|
|
}
|
|
if !(options.SyncInterval > 0) {
|
|
return nil, fmt.Errorf("invalid SyncInterval: %v", options.SyncInterval)
|
|
}
|
|
|
|
tc := &LegacySATokenCleaner{
|
|
client: client,
|
|
clock: cl,
|
|
saLister: saInformer.Lister(),
|
|
saInformerSynced: saInformer.Informer().HasSynced,
|
|
secretLister: secretInformer.Lister(),
|
|
secretInformerSynced: secretInformer.Informer().HasSynced,
|
|
podLister: podInformer.Lister(),
|
|
podInformerSynced: podInformer.Informer().HasSynced,
|
|
minimumSinceLastUsed: options.CleanUpPeriod,
|
|
syncInterval: options.SyncInterval,
|
|
}
|
|
|
|
return tc, nil
|
|
}
|
|
|
|
func (tc *LegacySATokenCleaner) Run(ctx context.Context) {
|
|
defer utilruntime.HandleCrash()
|
|
|
|
logger := klog.FromContext(ctx)
|
|
logger.Info("Starting legacy service account token cleaner controller")
|
|
defer logger.Info("Shutting down legacy service account token cleaner controller")
|
|
|
|
if !cache.WaitForNamedCacheSync("legacy-service-account-token-cleaner", ctx.Done(), tc.saInformerSynced, tc.secretInformerSynced, tc.podInformerSynced) {
|
|
return
|
|
}
|
|
|
|
go wait.UntilWithContext(ctx, tc.evaluateSATokens, tc.syncInterval)
|
|
|
|
<-ctx.Done()
|
|
}
|
|
|
|
func (tc *LegacySATokenCleaner) evaluateSATokens(ctx context.Context) {
|
|
logger := klog.FromContext(ctx)
|
|
|
|
now := tc.clock.Now().UTC()
|
|
trackedSince, err := tc.latestPossibleTrackedSinceTime(ctx)
|
|
if err != nil {
|
|
logger.Error(err, "Getting lastest possible tracked_since time")
|
|
return
|
|
}
|
|
|
|
if now.Before(trackedSince.Add(tc.minimumSinceLastUsed)) {
|
|
// we haven't been tracking long enough
|
|
return
|
|
}
|
|
|
|
preserveCreatedOnOrAfter := now.Add(-tc.minimumSinceLastUsed)
|
|
preserveUsedOnOrAfter := now.Add(-tc.minimumSinceLastUsed).Format(dateFormat)
|
|
|
|
secretList, err := tc.secretLister.Secrets(metav1.NamespaceAll).List(labels.Everything())
|
|
if err != nil {
|
|
logger.Error(err, "Getting cached secret list")
|
|
return
|
|
}
|
|
|
|
namespaceToUsedSecretNames := make(map[string]sets.String)
|
|
for _, secret := range secretList {
|
|
if secret.Type != v1.SecretTypeServiceAccountToken {
|
|
continue
|
|
}
|
|
if !secret.CreationTimestamp.Time.Before(preserveCreatedOnOrAfter) {
|
|
continue
|
|
}
|
|
|
|
if secret.DeletionTimestamp != nil {
|
|
continue
|
|
}
|
|
|
|
// if LastUsedLabelKey does not exist, we think the secret has not been used
|
|
// since the legacy token starts to track.
|
|
lastUsed, ok := secret.Labels[serviceaccount.LastUsedLabelKey]
|
|
if ok {
|
|
_, err := time.Parse(dateFormat, lastUsed)
|
|
if err != nil {
|
|
// the lastUsed value is not well-formed thus we cannot determine it
|
|
logger.Error(err, "Parsing lastUsed time", "secret", klog.KRef(secret.Namespace, secret.Name))
|
|
continue
|
|
}
|
|
if lastUsed >= preserveUsedOnOrAfter {
|
|
continue
|
|
}
|
|
}
|
|
|
|
sa, saErr := tc.getServiceAccount(secret)
|
|
|
|
if saErr != nil {
|
|
logger.Error(saErr, "Getting service account", "secret", klog.KRef(secret.Namespace, secret.Name))
|
|
continue
|
|
}
|
|
if sa == nil || !hasSecretReference(sa, secret.Name) {
|
|
// can't determine if this is an auto-generated token
|
|
continue
|
|
}
|
|
|
|
mountedSecretNames, err := tc.getMountedSecretNames(secret.Namespace, namespaceToUsedSecretNames)
|
|
if err != nil {
|
|
logger.Error(err, "Resolving mounted secrets", "secret", klog.KRef(secret.Namespace, secret.Name))
|
|
continue
|
|
}
|
|
if mountedSecretNames.Has(secret.Name) {
|
|
// still used by pods
|
|
continue
|
|
}
|
|
|
|
logger.Info("Delete auto-generated service account token", "secret", klog.KRef(secret.Namespace, secret.Name), "creationTime", secret.CreationTimestamp, "lastUsed", lastUsed)
|
|
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) {
|
|
logger.Error(err, "Deleting legacy service account token", "secret", klog.KRef(secret.Namespace, secret.Name), "serviceaccount", sa.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tc *LegacySATokenCleaner) getMountedSecretNames(secretNamespace string, namespaceToUsedSecretNames map[string]sets.String) (sets.String, error) {
|
|
if secrets, ok := namespaceToUsedSecretNames[secretNamespace]; ok {
|
|
return secrets, nil
|
|
}
|
|
|
|
podList, err := tc.podLister.Pods(secretNamespace).List(labels.Everything())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get pod list from pod cache: %v", err)
|
|
}
|
|
|
|
var secrets sets.String
|
|
for _, pod := range podList {
|
|
podutil.VisitPodSecretNames(pod, func(secretName string) bool {
|
|
if secrets == nil {
|
|
secrets = sets.NewString()
|
|
}
|
|
secrets.Insert(secretName)
|
|
return true
|
|
})
|
|
}
|
|
if secrets != nil {
|
|
namespaceToUsedSecretNames[secretNamespace] = secrets
|
|
}
|
|
return secrets, nil
|
|
}
|
|
|
|
func (tc *LegacySATokenCleaner) getServiceAccount(secret *v1.Secret) (*v1.ServiceAccount, error) {
|
|
saName := secret.Annotations[v1.ServiceAccountNameKey]
|
|
if len(saName) == 0 {
|
|
return nil, nil
|
|
}
|
|
saUID := types.UID(secret.Annotations[v1.ServiceAccountUIDKey])
|
|
sa, err := tc.saLister.ServiceAccounts(secret.Namespace).Get(saName)
|
|
if apierrors.IsNotFound(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure UID matches if given
|
|
if len(saUID) == 0 || saUID == sa.UID {
|
|
return sa, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// get the latest possible TrackedSince time information from the configMap label.
|
|
func (tc *LegacySATokenCleaner) latestPossibleTrackedSinceTime(ctx context.Context) (time.Time, error) {
|
|
configMap, err := tc.client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, legacytokentracking.ConfigMapName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
trackedSince, exist := configMap.Data[legacytokentracking.ConfigMapDataKey]
|
|
if !exist {
|
|
return time.Time{}, fmt.Errorf("configMap does not have since label")
|
|
}
|
|
trackedSinceTime, err := time.Parse(dateFormat, trackedSince)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("error parsing trackedSince time: %v", err)
|
|
}
|
|
// make sure the time to be 00:00 on the day just after the date starts to track
|
|
return trackedSinceTime.AddDate(0, 0, 1), nil
|
|
}
|
|
|
|
func hasSecretReference(serviceAccount *v1.ServiceAccount, secretName string) bool {
|
|
for _, secret := range serviceAccount.Secrets {
|
|
if secret.Name == secretName {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|