k8s.io/kubernetes@v1.29.3/pkg/registry/core/serviceaccount/storage/token.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 storage 18 19 import ( 20 "context" 21 "fmt" 22 "time" 23 24 authenticationapiv1 "k8s.io/api/authentication/v1" 25 "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/types" 30 "k8s.io/apimachinery/pkg/util/sets" 31 "k8s.io/apimachinery/pkg/util/validation/field" 32 "k8s.io/apiserver/pkg/audit" 33 "k8s.io/apiserver/pkg/authentication/authenticator" 34 "k8s.io/apiserver/pkg/authentication/serviceaccount" 35 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 36 "k8s.io/apiserver/pkg/registry/rest" 37 utilfeature "k8s.io/apiserver/pkg/util/feature" 38 "k8s.io/apiserver/pkg/warning" 39 "k8s.io/klog/v2" 40 authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" 41 authenticationvalidation "k8s.io/kubernetes/pkg/apis/authentication/validation" 42 api "k8s.io/kubernetes/pkg/apis/core" 43 "k8s.io/kubernetes/pkg/features" 44 token "k8s.io/kubernetes/pkg/serviceaccount" 45 ) 46 47 func (r *TokenREST) New() runtime.Object { 48 return &authenticationapi.TokenRequest{} 49 } 50 51 // Destroy cleans up resources on shutdown. 52 func (r *TokenREST) Destroy() { 53 // Given no underlying store, we don't destroy anything 54 // here explicitly. 55 } 56 57 type TokenREST struct { 58 svcaccts rest.Getter 59 pods rest.Getter 60 secrets rest.Getter 61 nodes rest.Getter 62 issuer token.TokenGenerator 63 auds authenticator.Audiences 64 audsSet sets.String 65 maxExpirationSeconds int64 66 extendExpiration bool 67 } 68 69 var _ = rest.NamedCreater(&TokenREST{}) 70 var _ = rest.GroupVersionKindProvider(&TokenREST{}) 71 72 var gvk = schema.GroupVersionKind{ 73 Group: authenticationapiv1.SchemeGroupVersion.Group, 74 Version: authenticationapiv1.SchemeGroupVersion.Version, 75 Kind: "TokenRequest", 76 } 77 78 func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { 79 req := obj.(*authenticationapi.TokenRequest) 80 81 // Get the namespace from the context (populated from the URL). 82 namespace, ok := genericapirequest.NamespaceFrom(ctx) 83 if !ok { 84 return nil, errors.NewBadRequest("namespace is required") 85 } 86 87 // require name/namespace in the body to match URL if specified 88 if len(req.Name) > 0 && req.Name != name { 89 errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("name"), req.Name, "must match the service account name if specified")} 90 return nil, errors.NewInvalid(gvk.GroupKind(), name, errs) 91 } 92 if len(req.Namespace) > 0 && req.Namespace != namespace { 93 errs := field.ErrorList{field.Invalid(field.NewPath("metadata").Child("namespace"), req.Namespace, "must match the service account namespace if specified")} 94 return nil, errors.NewInvalid(gvk.GroupKind(), name, errs) 95 } 96 97 // Lookup service account 98 svcacctObj, err := r.svcaccts.Get(ctx, name, &metav1.GetOptions{}) 99 if err != nil { 100 return nil, err 101 } 102 svcacct := svcacctObj.(*api.ServiceAccount) 103 104 // Default unset spec audiences to API server audiences based on server config 105 if len(req.Spec.Audiences) == 0 { 106 req.Spec.Audiences = r.auds 107 } 108 // Populate metadata fields if not set 109 if len(req.Name) == 0 { 110 req.Name = svcacct.Name 111 } 112 if len(req.Namespace) == 0 { 113 req.Namespace = svcacct.Namespace 114 } 115 116 // Save current time before building the token, to make sure the expiration 117 // returned in TokenRequestStatus would be <= the exp field in token. 118 nowTime := time.Now() 119 req.CreationTimestamp = metav1.NewTime(nowTime) 120 121 // Clear status 122 req.Status = authenticationapi.TokenRequestStatus{} 123 124 // call static validation, then validating admission 125 if errs := authenticationvalidation.ValidateTokenRequest(req); len(errs) != 0 { 126 return nil, errors.NewInvalid(gvk.GroupKind(), "", errs) 127 } 128 if createValidation != nil { 129 if err := createValidation(ctx, obj.DeepCopyObject()); err != nil { 130 return nil, err 131 } 132 } 133 134 var ( 135 pod *api.Pod 136 node *api.Node 137 secret *api.Secret 138 ) 139 140 if ref := req.Spec.BoundObjectRef; ref != nil { 141 var uid types.UID 142 143 gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind) 144 switch { 145 case gvk.Group == "" && gvk.Kind == "Pod": 146 newCtx := newContext(ctx, "pods", ref.Name, namespace, gvk) 147 podObj, err := r.pods.Get(newCtx, ref.Name, &metav1.GetOptions{}) 148 if err != nil { 149 return nil, err 150 } 151 pod = podObj.(*api.Pod) 152 if name != pod.Spec.ServiceAccountName { 153 return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token for serviceaccount %q to pod running with different serviceaccount name.", name)) 154 } 155 uid = pod.UID 156 if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenPodNodeInfo) { 157 if nodeName := pod.Spec.NodeName; nodeName != "" { 158 newCtx := newContext(ctx, "nodes", nodeName, "", api.SchemeGroupVersion.WithKind("Node")) 159 // set ResourceVersion=0 to allow this to be read/served from the apiservers watch cache 160 nodeObj, err := r.nodes.Get(newCtx, nodeName, &metav1.GetOptions{ResourceVersion: "0"}) 161 if err != nil { 162 nodeObj, err = r.nodes.Get(newCtx, nodeName, &metav1.GetOptions{}) // fallback to a live lookup on any error 163 } 164 switch { 165 case errors.IsNotFound(err): 166 // if the referenced Node object does not exist, we still embed just the pod name into the 167 // claims so that clients still have some indication of what node a pod is assigned to when 168 // inspecting a token (even if the UID is not present). 169 klog.V(4).ErrorS(err, "failed fetching node for pod", "pod", klog.KObj(pod), "podUID", pod.UID, "nodeName", nodeName) 170 node = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}} 171 case err != nil: 172 return nil, errors.NewInternalError(err) 173 default: 174 node = nodeObj.(*api.Node) 175 } 176 } 177 } 178 case gvk.Group == "" && gvk.Kind == "Node": 179 if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenNodeBinding) { 180 return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token to a Node object as the %q feature-gate is disabled", features.ServiceAccountTokenNodeBinding)) 181 } 182 newCtx := newContext(ctx, "nodes", ref.Name, "", gvk) 183 nodeObj, err := r.nodes.Get(newCtx, ref.Name, &metav1.GetOptions{}) 184 if err != nil { 185 return nil, err 186 } 187 node = nodeObj.(*api.Node) 188 uid = node.UID 189 case gvk.Group == "" && gvk.Kind == "Secret": 190 newCtx := newContext(ctx, "secrets", ref.Name, namespace, gvk) 191 secretObj, err := r.secrets.Get(newCtx, ref.Name, &metav1.GetOptions{}) 192 if err != nil { 193 return nil, err 194 } 195 secret = secretObj.(*api.Secret) 196 uid = secret.UID 197 default: 198 return nil, errors.NewBadRequest(fmt.Sprintf("cannot bind token to object of type %s", gvk.String())) 199 } 200 if ref.UID != "" && uid != ref.UID { 201 return nil, errors.NewConflict(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, ref.Name, fmt.Errorf("the UID in the bound object reference (%s) does not match the UID in record. The object might have been deleted and then recreated", ref.UID)) 202 } 203 } 204 205 if r.maxExpirationSeconds > 0 && req.Spec.ExpirationSeconds > r.maxExpirationSeconds { 206 //only positive value is valid 207 warning.AddWarning(ctx, "", fmt.Sprintf("requested expiration of %d seconds shortened to %d seconds", req.Spec.ExpirationSeconds, r.maxExpirationSeconds)) 208 req.Spec.ExpirationSeconds = r.maxExpirationSeconds 209 } 210 211 // Tweak expiration for safe transition of projected service account token. 212 // Warn (instead of fail) after requested expiration time. 213 // Fail after hard-coded extended expiration time. 214 // Only perform the extension when token is pod-bound. 215 var warnAfter int64 216 exp := req.Spec.ExpirationSeconds 217 if r.extendExpiration && pod != nil && req.Spec.ExpirationSeconds == token.WarnOnlyBoundTokenExpirationSeconds && r.isKubeAudiences(req.Spec.Audiences) { 218 warnAfter = exp 219 exp = token.ExpirationExtensionSeconds 220 } 221 222 sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences) 223 if err != nil { 224 return nil, err 225 } 226 tokdata, err := r.issuer.GenerateToken(sc, pc) 227 if err != nil { 228 return nil, fmt.Errorf("failed to generate token: %v", err) 229 } 230 231 // populate status 232 out := req.DeepCopy() 233 out.Status = authenticationapi.TokenRequestStatus{ 234 Token: tokdata, 235 ExpirationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(out.Spec.ExpirationSeconds) * time.Second)}, 236 } 237 if utilfeature.DefaultFeatureGate.Enabled(features.ServiceAccountTokenJTI) && len(sc.ID) > 0 { 238 audit.AddAuditAnnotation(ctx, serviceaccount.CredentialIDKey, serviceaccount.CredentialIDForJTI(sc.ID)) 239 } 240 return out, nil 241 } 242 243 func (r *TokenREST) GroupVersionKind(schema.GroupVersion) schema.GroupVersionKind { 244 return gvk 245 } 246 247 // newContext return a copy of ctx in which new RequestInfo is set 248 func newContext(ctx context.Context, resource, name, namespace string, gvk schema.GroupVersionKind) context.Context { 249 newInfo := genericapirequest.RequestInfo{ 250 IsResourceRequest: true, 251 Verb: "get", 252 Namespace: namespace, 253 Resource: resource, 254 Name: name, 255 Parts: []string{resource, name}, 256 APIGroup: gvk.Group, 257 APIVersion: gvk.Version, 258 } 259 return genericapirequest.WithRequestInfo(ctx, &newInfo) 260 } 261 262 // isKubeAudiences returns true if the tokenaudiences is a strict subset of apiserver audiences. 263 func (r *TokenREST) isKubeAudiences(tokenAudience []string) bool { 264 // tokenAudiences must be a strict subset of apiserver audiences 265 return r.audsSet.HasAll(tokenAudience...) 266 }