...

Source file src/k8s.io/kubernetes/pkg/serviceaccount/legacy.go

Documentation: k8s.io/kubernetes/pkg/serviceaccount

     1  /*
     2  Copyright 2018 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  	"crypto/subtle"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"time"
    26  
    27  	"gopkg.in/square/go-jose.v2/jwt"
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	"k8s.io/apiserver/pkg/audit"
    32  	apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
    33  	"k8s.io/apiserver/pkg/warning"
    34  	applyv1 "k8s.io/client-go/applyconfigurations/core/v1"
    35  	typedv1core "k8s.io/client-go/kubernetes/typed/core/v1"
    36  	"k8s.io/klog/v2"
    37  )
    38  
    39  const InvalidSinceLabelKey = "kubernetes.io/legacy-token-invalid-since"
    40  
    41  func LegacyClaims(serviceAccount v1.ServiceAccount, secret v1.Secret) (*jwt.Claims, interface{}) {
    42  	return &jwt.Claims{
    43  			Subject: apiserverserviceaccount.MakeUsername(serviceAccount.Namespace, serviceAccount.Name),
    44  		}, &legacyPrivateClaims{
    45  			Namespace:          serviceAccount.Namespace,
    46  			ServiceAccountName: serviceAccount.Name,
    47  			ServiceAccountUID:  string(serviceAccount.UID),
    48  			SecretName:         secret.Name,
    49  		}
    50  }
    51  
    52  const (
    53  	LegacyIssuer     = "kubernetes/serviceaccount"
    54  	LastUsedLabelKey = "kubernetes.io/legacy-token-last-used"
    55  )
    56  
    57  type legacyPrivateClaims struct {
    58  	ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"`
    59  	ServiceAccountUID  string `json:"kubernetes.io/serviceaccount/service-account.uid"`
    60  	SecretName         string `json:"kubernetes.io/serviceaccount/secret.name"`
    61  	Namespace          string `json:"kubernetes.io/serviceaccount/namespace"`
    62  }
    63  
    64  func NewLegacyValidator(lookup bool, getter ServiceAccountTokenGetter, secretsWriter typedv1core.SecretsGetter) (Validator, error) {
    65  	if lookup && getter == nil {
    66  		return nil, errors.New("ServiceAccountTokenGetter must be provided")
    67  	}
    68  	if lookup && secretsWriter == nil {
    69  		return nil, errors.New("SecretsWriter must be provided")
    70  	}
    71  	return &legacyValidator{
    72  		lookup:        lookup,
    73  		getter:        getter,
    74  		secretsWriter: secretsWriter,
    75  	}, nil
    76  }
    77  
    78  type legacyValidator struct {
    79  	lookup        bool
    80  	getter        ServiceAccountTokenGetter
    81  	secretsWriter typedv1core.SecretsGetter
    82  }
    83  
    84  var _ = Validator(&legacyValidator{})
    85  
    86  func (v *legacyValidator) Validate(ctx context.Context, tokenData string, public *jwt.Claims, privateObj interface{}) (*apiserverserviceaccount.ServiceAccountInfo, error) {
    87  	private, ok := privateObj.(*legacyPrivateClaims)
    88  	if !ok {
    89  		klog.Errorf("jwt validator expected private claim of type *legacyPrivateClaims but got: %T", privateObj)
    90  		return nil, errors.New("Token could not be validated.")
    91  	}
    92  
    93  	// Make sure the claims we need exist
    94  	if len(public.Subject) == 0 {
    95  		return nil, errors.New("sub claim is missing")
    96  	}
    97  	namespace := private.Namespace
    98  	if len(namespace) == 0 {
    99  		return nil, errors.New("namespace claim is missing")
   100  	}
   101  	secretName := private.SecretName
   102  	if len(secretName) == 0 {
   103  		return nil, errors.New("secretName claim is missing")
   104  	}
   105  	serviceAccountName := private.ServiceAccountName
   106  	if len(serviceAccountName) == 0 {
   107  		return nil, errors.New("serviceAccountName claim is missing")
   108  	}
   109  	serviceAccountUID := private.ServiceAccountUID
   110  	if len(serviceAccountUID) == 0 {
   111  		return nil, errors.New("serviceAccountUID claim is missing")
   112  	}
   113  
   114  	subjectNamespace, subjectName, err := apiserverserviceaccount.SplitUsername(public.Subject)
   115  	if err != nil || subjectNamespace != namespace || subjectName != serviceAccountName {
   116  		return nil, errors.New("sub claim is invalid")
   117  	}
   118  
   119  	if v.lookup {
   120  		// Make sure token hasn't been invalidated by deletion of the secret
   121  		secret, err := v.getter.GetSecret(namespace, secretName)
   122  		if err != nil {
   123  			klog.V(4).Infof("Could not retrieve token %s/%s for service account %s/%s: %v", namespace, secretName, namespace, serviceAccountName, err)
   124  			return nil, errors.New("Token has been invalidated")
   125  		}
   126  		if secret.DeletionTimestamp != nil {
   127  			klog.V(4).Infof("Token is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
   128  			return nil, errors.New("Token has been invalidated")
   129  		}
   130  		if subtle.ConstantTimeCompare(secret.Data[v1.ServiceAccountTokenKey], []byte(tokenData)) == 0 {
   131  			klog.V(4).Infof("Token contents no longer matches %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName)
   132  			return nil, errors.New("Token does not match server's copy")
   133  		}
   134  
   135  		// Make sure service account still exists (name and UID)
   136  		serviceAccount, err := v.getter.GetServiceAccount(namespace, serviceAccountName)
   137  		if err != nil {
   138  			klog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, serviceAccountName, err)
   139  			return nil, err
   140  		}
   141  		if serviceAccount.DeletionTimestamp != nil {
   142  			klog.V(4).Infof("Service account has been deleted %s/%s", namespace, serviceAccountName)
   143  			return nil, fmt.Errorf("ServiceAccount %s/%s has been deleted", namespace, serviceAccountName)
   144  		}
   145  		if string(serviceAccount.UID) != serviceAccountUID {
   146  			klog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, serviceAccountName, string(serviceAccount.UID), serviceAccountUID)
   147  			return nil, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
   148  		}
   149  
   150  		// Track secret-based long-lived service account tokens and add audit annotations and metrics.
   151  		autoGenerated := false
   152  
   153  		// Check if the secret has been marked as invalid
   154  		if invalidSince := secret.Labels[InvalidSinceLabelKey]; invalidSince != "" {
   155  			audit.AddAuditAnnotation(ctx, "authentication.k8s.io/legacy-token-invalidated", secret.Name+"/"+secret.Namespace)
   156  			invalidatedAutoTokensTotal.WithContext(ctx).Inc()
   157  			v.patchSecretWithLastUsedDate(ctx, secret)
   158  			return nil, fmt.Errorf("the token in secret %s/%s for service account %s/%s has been marked invalid. Use tokens from the TokenRequest API or manually created secret-based tokens, or remove the '%s' label from the secret to temporarily allow use of this token", namespace, secretName, namespace, serviceAccountName, InvalidSinceLabelKey)
   159  		}
   160  
   161  		// Check if it is an auto-generated secret-based token
   162  		for _, ref := range serviceAccount.Secrets {
   163  			if ref.Name == secret.Name {
   164  				autoGenerated = true
   165  				warning.AddWarning(ctx, "", "Use tokens from the TokenRequest API or manually created secret-based tokens instead of auto-generated secret-based tokens.")
   166  				audit.AddAuditAnnotation(ctx, "authentication.k8s.io/legacy-token-autogenerated-secret", secret.Name)
   167  				autoGeneratedTokensTotal.WithContext(ctx).Inc()
   168  				break
   169  			}
   170  		}
   171  
   172  		// Check if it's a manually created secret-based token
   173  		if !autoGenerated {
   174  			audit.AddAuditAnnotation(ctx, "authentication.k8s.io/legacy-token-manual-secret", secret.Name)
   175  			manuallyCreatedTokensTotal.WithContext(ctx).Inc()
   176  		}
   177  
   178  		v.patchSecretWithLastUsedDate(ctx, secret)
   179  	}
   180  
   181  	return &apiserverserviceaccount.ServiceAccountInfo{
   182  		Namespace: private.Namespace,
   183  		Name:      private.ServiceAccountName,
   184  		UID:       private.ServiceAccountUID,
   185  	}, nil
   186  }
   187  
   188  func (v *legacyValidator) patchSecretWithLastUsedDate(ctx context.Context, secret *v1.Secret) {
   189  	now := time.Now().UTC()
   190  	today := now.Format("2006-01-02")
   191  	tomorrow := now.AddDate(0, 0, 1).Format("2006-01-02")
   192  	lastUsed := secret.Labels[LastUsedLabelKey]
   193  	if lastUsed != today && lastUsed != tomorrow {
   194  		patchContent, err := json.Marshal(applyv1.Secret(secret.Name, secret.Namespace).WithUID(secret.UID).WithLabels(map[string]string{LastUsedLabelKey: today}))
   195  		if err != nil {
   196  			klog.Errorf("Failed to marshal legacy service account token %s/%s tracking labels, err: %v", secret.Name, secret.Namespace, err)
   197  		} else {
   198  			if _, err := v.secretsWriter.Secrets(secret.Namespace).Patch(ctx, secret.Name, types.MergePatchType, patchContent, metav1.PatchOptions{}); err != nil {
   199  				klog.Errorf("Failed to label legacy service account token %s/%s with last-used date, err: %v", secret.Name, secret.Namespace, err)
   200  			}
   201  		}
   202  	}
   203  }
   204  
   205  func (v *legacyValidator) NewPrivateClaims() interface{} {
   206  	return &legacyPrivateClaims{}
   207  }
   208  

View as plain text