1
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
38 WarnOnlyBoundTokenExpirationSeconds = 60*60 + 7
39
40
41 ExpirationExtensionSeconds = 24 * 365 * 60 * 60
42 )
43
44 var (
45
46 now = time.Now
47
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
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
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
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)
170
171 default:
172 klog.Errorf("service account token claim validation got unexpected error type: %T", err)
173 return nil, errors.New("service account token claims could not be validated due to unexpected validation error")
174 }
175
176
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
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
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
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
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
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 }
301
View as plain text