k8s.io/kubernetes@v1.29.3/pkg/serviceaccount/legacy.go (about) 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 }