k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/serviceaccount/claims.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 "errors" 22 "fmt" 23 "time" 24 25 "github.com/google/uuid" 26 "gopkg.in/square/go-jose.v2/jwt" 27 "k8s.io/klog/v2" 28 29 "k8s.io/apiserver/pkg/audit" 30 apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount" 31 utilfeature "k8s.io/apiserver/pkg/util/feature" 32 "k8s.io/kubernetes/pkg/apis/core" 33 "k8s.io/kubernetes/pkg/features" 34 ) 35 36 const ( 37 // Injected bound service account token expiration which triggers monitoring of its time-bound feature. 38 WarnOnlyBoundTokenExpirationSeconds = 60*60 + 7 39 40 // Extended expiration for those modified tokens involved in safe rollout if time-bound feature. 41 ExpirationExtensionSeconds = 24 * 365 * 60 * 60 42 ) 43 44 var ( 45 // time.Now stubbed out to allow testing 46 now = time.Now 47 // uuid.New stubbed out to allow testing 48 newUUID = uuid.NewString 49 ) 50 51 type privateClaims struct { 52 Kubernetes kubernetes `json:"kubernetes.io,omitempty"` 53 } 54 55 type kubernetes struct { 56 Namespace string `json:"namespace,omitempty"` 57 Svcacct ref `json:"serviceaccount,omitempty"` 58 Pod *ref `json:"pod,omitempty"` 59 Secret *ref `json:"secret,omitempty"` 60 Node *ref `json:"node,omitempty"` 61 WarnAfter *jwt.NumericDate `json:"warnafter,omitempty"` 62 } 63 64 type ref struct { 65 Name string `json:"name,omitempty"` 66 UID string `json:"uid,omitempty"` 67 } 68 69 func Claims(sa core.ServiceAccount, pod *core.Pod, secret *core.Secret, node *core.Node, expirationSeconds, warnafter int64, audience []string) (*jwt.Claims, interface{}, error) { 70 now := now() 71 sc := &jwt.Claims{ 72 Subject: apiserverserviceaccount.MakeUsername(sa.Namespace, sa.Name), 73 Audience: jwt.Audience(audience), 74 IssuedAt: jwt.NewNumericDate(now), 75 NotBefore: jwt.NewNumericDate(now), 76 Expiry: jwt.NewNumericDate(now.Add(time.Duration(expirationSeconds) * time.Second)), 77 } 78 if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) { 79 sc.ID = newUUID() 80 } 81 pc := &privateClaims{ 82 Kubernetes: kubernetes{ 83 Namespace: sa.Namespace, 84 Svcacct: ref{ 85 Name: sa.Name, 86 UID: string(sa.UID), 87 }, 88 }, 89 } 90 91 if secret != nil && (node != nil || pod != nil) { 92 return nil, nil, fmt.Errorf("internal error, token can only be bound to one object type") 93 } 94 switch { 95 case pod != nil: 96 pc.Kubernetes.Pod = &ref{ 97 Name: pod.Name, 98 UID: string(pod.UID), 99 } 100 if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) { 101 // if this is bound to a pod and the node information is available, persist that too 102 if node != nil { 103 pc.Kubernetes.Node = &ref{ 104 Name: node.Name, 105 UID: string(node.UID), 106 } 107 } 108 } 109 case secret != nil: 110 pc.Kubernetes.Secret = &ref{ 111 Name: secret.Name, 112 UID: string(secret.UID), 113 } 114 case node != nil: 115 if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBinding) { 116 return nil, nil, fmt.Errorf("token bound to Node object requested, but %q feature gate is disabled", features.ServiceAccountTokenNodeBinding) 117 } 118 pc.Kubernetes.Node = &ref{ 119 Name: node.Name, 120 UID: string(node.UID), 121 } 122 } 123 124 if warnafter != 0 { 125 pc.Kubernetes.WarnAfter = jwt.NewNumericDate(now.Add(time.Duration(warnafter) * time.Second)) 126 } 127 128 return sc, pc, nil 129 } 130 131 func NewValidator(getter ServiceAccountTokenGetter) Validator { 132 return &validator{ 133 getter: getter, 134 } 135 } 136 137 type validator struct { 138 getter ServiceAccountTokenGetter 139 } 140 141 var _ = Validator(&validator{}) 142 143 func (v *validator) Validate(ctx context.Context, _ string, public *jwt.Claims, privateObj interface{}) (*apiserverserviceaccount.ServiceAccountInfo, error) { 144 private, ok := privateObj.(*privateClaims) 145 if !ok { 146 klog.Errorf("service account jwt validator expected private claim of type *privateClaims but got: %T", privateObj) 147 return nil, errors.New("service account token claims could not be validated due to unexpected private claim") 148 } 149 nowTime := now() 150 err := public.Validate(jwt.Expected{ 151 Time: nowTime, 152 }) 153 switch err { 154 case nil: 155 // successful validation 156 157 case jwt.ErrExpired: 158 return nil, errors.New("service account token has expired") 159 160 case jwt.ErrNotValidYet: 161 return nil, errors.New("service account token is not valid yet") 162 163 case jwt.ErrIssuedInTheFuture: 164 return nil, errors.New("service account token is issued in the future") 165 166 // our current use of jwt.Expected above should make these cases impossible to hit 167 case jwt.ErrInvalidAudience, jwt.ErrInvalidID, jwt.ErrInvalidIssuer, jwt.ErrInvalidSubject: 168 klog.Errorf("service account token claim validation got unexpected validation failure: %v", err) 169 return nil, fmt.Errorf("service account token claims could not be validated: %w", err) // safe to pass these errors back to the user 170 171 default: 172 klog.Errorf("service account token claim validation got unexpected error type: %T", err) // avoid leaking unexpected information into the logs 173 return nil, errors.New("service account token claims could not be validated due to unexpected validation error") // return an opaque error 174 } 175 176 // consider things deleted prior to now()-leeway to be invalid 177 invalidIfDeletedBefore := nowTime.Add(-jwt.DefaultLeeway) 178 namespace := private.Kubernetes.Namespace 179 saref := private.Kubernetes.Svcacct 180 podref := private.Kubernetes.Pod 181 noderef := private.Kubernetes.Node 182 secref := private.Kubernetes.Secret 183 // Make sure service account still exists (name and UID) 184 serviceAccount, err := v.getter.GetServiceAccount(namespace, saref.Name) 185 if err != nil { 186 klog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, saref.Name, err) 187 return nil, err 188 } 189 190 if string(serviceAccount.UID) != saref.UID { 191 klog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, saref.Name, string(serviceAccount.UID), saref.UID) 192 return nil, fmt.Errorf("service account UID (%s) does not match claim (%s)", serviceAccount.UID, saref.UID) 193 } 194 if serviceAccount.DeletionTimestamp != nil && serviceAccount.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) { 195 klog.V(4).Infof("Service account has been deleted %s/%s", namespace, saref.Name) 196 return nil, fmt.Errorf("service account %s/%s has been deleted", namespace, saref.Name) 197 } 198 199 if secref != nil { 200 // Make sure token hasn't been invalidated by deletion of the secret 201 secret, err := v.getter.GetSecret(namespace, secref.Name) 202 if err != nil { 203 klog.V(4).Infof("Could not retrieve bound secret %s/%s for service account %s/%s: %v", namespace, secref.Name, namespace, saref.Name, err) 204 return nil, errors.New("service account token has been invalidated") 205 } 206 if secref.UID != string(secret.UID) { 207 klog.V(4).Infof("Secret UID no longer matches %s/%s: %q != %q", namespace, secref.Name, string(secret.UID), secref.UID) 208 return nil, fmt.Errorf("secret UID (%s) does not match service account secret ref claim (%s)", secret.UID, secref.UID) 209 } 210 if secret.DeletionTimestamp != nil && secret.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) { 211 klog.V(4).Infof("Bound secret is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, secref.Name, namespace, saref.Name) 212 return nil, errors.New("service account token has been invalidated") 213 } 214 } 215 216 var podName, podUID string 217 if podref != nil { 218 // Make sure token hasn't been invalidated by deletion of the pod 219 pod, err := v.getter.GetPod(namespace, podref.Name) 220 if err != nil { 221 klog.V(4).Infof("Could not retrieve bound pod %s/%s for service account %s/%s: %v", namespace, podref.Name, namespace, saref.Name, err) 222 return nil, errors.New("service account token has been invalidated") 223 } 224 if podref.UID != string(pod.UID) { 225 klog.V(4).Infof("Pod UID no longer matches %s/%s: %q != %q", namespace, podref.Name, string(pod.UID), podref.UID) 226 return nil, fmt.Errorf("pod UID (%s) does not match service account pod ref claim (%s)", pod.UID, podref.UID) 227 } 228 if pod.DeletionTimestamp != nil && pod.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) { 229 klog.V(4).Infof("Bound pod is deleted and awaiting removal: %s/%s for service account %s/%s", namespace, podref.Name, namespace, saref.Name) 230 return nil, errors.New("service account token has been invalidated") 231 } 232 podName = podref.Name 233 podUID = podref.UID 234 } 235 236 var nodeName, nodeUID string 237 if noderef != nil { 238 switch { 239 case podref != nil: 240 if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) { 241 // for pod-bound tokens, just extract the node claims 242 nodeName = noderef.Name 243 nodeUID = noderef.UID 244 } 245 case podref == nil: 246 if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBindingValidation) { 247 klog.V(4).Infof("ServiceAccount token is bound to a Node object, but the node bound token validation feature is disabled") 248 return nil, fmt.Errorf("token is bound to a Node object but the %s feature gate is disabled", features.ServiceAccountTokenNodeBindingValidation) 249 } 250 251 node, err := v.getter.GetNode(noderef.Name) 252 if err != nil { 253 klog.V(4).Infof("Could not retrieve node object %q for service account %s/%s: %v", noderef.Name, namespace, saref.Name, err) 254 return nil, errors.New("service account token has been invalidated") 255 } 256 if noderef.UID != string(node.UID) { 257 klog.V(4).Infof("Node UID no longer matches %s: %q != %q", noderef.Name, string(node.UID), noderef.UID) 258 return nil, fmt.Errorf("node UID (%s) does not match service account node ref claim (%s)", node.UID, noderef.UID) 259 } 260 if node.DeletionTimestamp != nil && node.DeletionTimestamp.Time.Before(invalidIfDeletedBefore) { 261 klog.V(4).Infof("Node %q is deleted and awaiting removal for service account %s/%s", node.Name, namespace, saref.Name) 262 return nil, errors.New("service account token has been invalidated") 263 } 264 nodeName = noderef.Name 265 nodeUID = noderef.UID 266 } 267 } 268 269 // Check special 'warnafter' field for projected service account token transition. 270 warnafter := private.Kubernetes.WarnAfter 271 if warnafter != nil && *warnafter != 0 { 272 if nowTime.After(warnafter.Time()) { 273 secondsAfterWarn := nowTime.Unix() - warnafter.Time().Unix() 274 auditInfo := fmt.Sprintf("subject: %s, seconds after warning threshold: %d", public.Subject, secondsAfterWarn) 275 audit.AddAuditAnnotation(ctx, "authentication.k8s.io/stale-token", auditInfo) 276 staleTokensTotal.WithContext(ctx).Inc() 277 } else { 278 validTokensTotal.WithContext(ctx).Inc() 279 } 280 } 281 282 var jti string 283 if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) { 284 jti = public.ID 285 } 286 return &apiserverserviceaccount.ServiceAccountInfo{ 287 Namespace: private.Kubernetes.Namespace, 288 Name: private.Kubernetes.Svcacct.Name, 289 UID: private.Kubernetes.Svcacct.UID, 290 PodName: podName, 291 PodUID: podUID, 292 NodeName: nodeName, 293 NodeUID: nodeUID, 294 CredentialID: apiserverserviceaccount.CredentialIDForJTI(jti), 295 }, nil 296 } 297 298 func (v *validator) NewPrivateClaims() interface{} { 299 return &privateClaims{} 300 }