1
16
17 package serviceaccount
18
19 import (
20 "context"
21 "encoding/json"
22 "fmt"
23 "testing"
24 "time"
25
26 "gopkg.in/square/go-jose.v2/jwt"
27
28 v1 "k8s.io/api/core/v1"
29 apierrors "k8s.io/apimachinery/pkg/api/errors"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/runtime/schema"
32 utilfeature "k8s.io/apiserver/pkg/util/feature"
33 featuregatetesting "k8s.io/component-base/featuregate/testing"
34 "k8s.io/kubernetes/pkg/apis/core"
35 "k8s.io/kubernetes/pkg/features"
36 )
37
38 func init() {
39 now = func() time.Time {
40
41 return time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)
42 }
43
44 newUUID = func() string {
45
46 return "fixed"
47 }
48 }
49
50 func TestClaims(t *testing.T) {
51 sa := core.ServiceAccount{
52 ObjectMeta: metav1.ObjectMeta{
53 Namespace: "myns",
54 Name: "mysvcacct",
55 UID: "mysvcacct-uid",
56 },
57 }
58 pod := &core.Pod{
59 ObjectMeta: metav1.ObjectMeta{
60 Namespace: "myns",
61 Name: "mypod",
62 UID: "mypod-uid",
63 },
64 }
65 sec := &core.Secret{
66 ObjectMeta: metav1.ObjectMeta{
67 Namespace: "myns",
68 Name: "mysecret",
69 UID: "mysecret-uid",
70 },
71 }
72 node := &core.Node{
73 ObjectMeta: metav1.ObjectMeta{
74 Name: "mynode",
75 UID: "mynode-uid",
76 },
77 }
78 cs := []struct {
79
80 sa core.ServiceAccount
81 pod *core.Pod
82 sec *core.Secret
83 node *core.Node
84 exp int64
85 warnafter int64
86 aud []string
87 err string
88
89 sc *jwt.Claims
90 pc *privateClaims
91
92 featureJTI, featurePodNodeInfo, featureNodeBinding bool
93 }{
94 {
95
96 sa: sa,
97 pod: pod,
98 sec: sec,
99
100 exp: 0,
101
102 aud: nil,
103 err: "internal error, token can only be bound to one object type",
104 },
105 {
106
107 sa: sa,
108 pod: pod,
109
110 aud: []string{},
111 exp: 100,
112
113 sc: &jwt.Claims{
114 Subject: "system:serviceaccount:myns:mysvcacct",
115 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
116 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
117 Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)),
118 },
119 pc: &privateClaims{
120 Kubernetes: kubernetes{
121 Namespace: "myns",
122 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
123 Pod: &ref{Name: "mypod", UID: "mypod-uid"},
124 },
125 },
126 },
127 {
128
129 sa: sa,
130 sec: sec,
131 exp: 100,
132
133 aud: []string{"1"},
134
135 sc: &jwt.Claims{
136 Subject: "system:serviceaccount:myns:mysvcacct",
137 Audience: []string{"1"},
138 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
139 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
140 Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)),
141 },
142 pc: &privateClaims{
143 Kubernetes: kubernetes{
144 Namespace: "myns",
145 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
146 Secret: &ref{Name: "mysecret", UID: "mysecret-uid"},
147 },
148 },
149 },
150 {
151
152 sa: sa,
153 exp: 100,
154
155 aud: []string{"1", "2"},
156
157 sc: &jwt.Claims{
158 Subject: "system:serviceaccount:myns:mysvcacct",
159 Audience: []string{"1", "2"},
160 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
161 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
162 Expiry: jwt.NewNumericDate(time.Unix(1514764800+100, 0)),
163 },
164 pc: &privateClaims{
165 Kubernetes: kubernetes{
166 Namespace: "myns",
167 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
168 },
169 },
170 },
171 {
172
173 sa: sa,
174 pod: pod,
175 exp: 60 * 60 * 24,
176 warnafter: 60 * 60,
177
178 aud: nil,
179
180 sc: &jwt.Claims{
181 Subject: "system:serviceaccount:myns:mysvcacct",
182 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
183 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
184 Expiry: jwt.NewNumericDate(time.Unix(1514764800+60*60*24, 0)),
185 },
186 pc: &privateClaims{
187 Kubernetes: kubernetes{
188 Namespace: "myns",
189 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
190 Pod: &ref{Name: "mypod", UID: "mypod-uid"},
191 WarnAfter: jwt.NewNumericDate(time.Unix(1514764800+60*60, 0)),
192 },
193 },
194 },
195 {
196
197 sa: sa,
198 node: node,
199
200 exp: 0,
201
202 aud: nil,
203 err: "token bound to Node object requested, but \"ServiceAccountTokenNodeBinding\" feature gate is disabled",
204 },
205 {
206
207 sa: sa,
208 node: node,
209 pod: pod,
210
211 exp: 0,
212
213 aud: nil,
214
215 sc: &jwt.Claims{
216 Subject: "system:serviceaccount:myns:mysvcacct",
217 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
218 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
219 Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)),
220 },
221 pc: &privateClaims{
222 Kubernetes: kubernetes{
223 Namespace: "myns",
224 Pod: &ref{Name: "mypod", UID: "mypod-uid"},
225 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
226 },
227 },
228 },
229 {
230
231 sa: sa,
232 node: node,
233
234 featureNodeBinding: true,
235
236 exp: 0,
237
238 aud: nil,
239
240 sc: &jwt.Claims{
241 Subject: "system:serviceaccount:myns:mysvcacct",
242 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
243 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
244 Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)),
245 },
246 pc: &privateClaims{
247 Kubernetes: kubernetes{
248 Namespace: "myns",
249 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
250 Node: &ref{Name: "mynode", UID: "mynode-uid"},
251 },
252 },
253 },
254 {
255
256 sa: sa,
257 pod: pod,
258 node: node,
259
260 featurePodNodeInfo: true,
261
262 exp: 0,
263
264 aud: nil,
265
266 sc: &jwt.Claims{
267 Subject: "system:serviceaccount:myns:mysvcacct",
268 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
269 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
270 Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)),
271 },
272 pc: &privateClaims{
273 Kubernetes: kubernetes{
274 Namespace: "myns",
275 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
276 Pod: &ref{Name: "mypod", UID: "mypod-uid"},
277 Node: &ref{Name: "mynode", UID: "mynode-uid"},
278 },
279 },
280 },
281 {
282
283 sa: sa,
284 sec: sec,
285 node: node,
286
287 featureNodeBinding: true,
288
289 exp: 0,
290
291 aud: nil,
292 err: "internal error, token can only be bound to one object type",
293 },
294 {
295
296 sa: sa,
297
298 featureJTI: true,
299
300 exp: 0,
301
302 aud: nil,
303
304 sc: &jwt.Claims{
305 Subject: "system:serviceaccount:myns:mysvcacct",
306 IssuedAt: jwt.NewNumericDate(time.Unix(1514764800, 0)),
307 NotBefore: jwt.NewNumericDate(time.Unix(1514764800, 0)),
308 Expiry: jwt.NewNumericDate(time.Unix(1514764800, 0)),
309 ID: "fixed",
310 },
311 pc: &privateClaims{
312 Kubernetes: kubernetes{
313 Namespace: "myns",
314 Svcacct: ref{Name: "mysvcacct", UID: "mysvcacct-uid"},
315 },
316 },
317 },
318 {
319
320 sa: sa,
321 node: node,
322 featureNodeBinding: false,
323
324 exp: 0,
325
326 aud: nil,
327
328 err: "token bound to Node object requested, but \"ServiceAccountTokenNodeBinding\" feature gate is disabled",
329 },
330 }
331 for i, c := range cs {
332 t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
333
334
335
336 spew := func(obj interface{}) string {
337 b, err := json.Marshal(obj)
338 if err != nil {
339 t.Fatalf("err, couldn't marshal claims: %v", err)
340 }
341 return string(b)
342 }
343
344
345 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenJTI, c.featureJTI)()
346 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBinding, c.featureNodeBinding)()
347 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenPodNodeInfo, c.featurePodNodeInfo)()
348
349 sc, pc, err := Claims(c.sa, c.pod, c.sec, c.node, c.exp, c.warnafter, c.aud)
350 if err != nil && err.Error() != c.err {
351 t.Errorf("expected error %q but got: %v", c.err, err)
352 }
353 if err == nil && c.err != "" {
354 t.Errorf("expected an error but got none")
355 }
356 if spew(sc) != spew(c.sc) {
357 t.Errorf("standard claims differed\n\tsaw:\t%s\n\twant:\t%s", spew(sc), spew(c.sc))
358 }
359 if spew(pc) != spew(c.pc) {
360 t.Errorf("private claims differed\n\tsaw: %s\n\twant: %s", spew(pc), spew(c.pc))
361 }
362 })
363 }
364 }
365
366 type deletionTestCase struct {
367 name string
368 time *metav1.Time
369 expectErr bool
370 }
371
372 type claimTestCase struct {
373 name string
374 getter ServiceAccountTokenGetter
375 private *privateClaims
376 expiry jwt.NumericDate
377 notBefore jwt.NumericDate
378 expectErr string
379
380 featureNodeBindingValidation bool
381 }
382
383 func TestValidatePrivateClaims(t *testing.T) {
384 var (
385 nowUnix = int64(1514764800)
386
387 serviceAccount = &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "saname", Namespace: "ns", UID: "sauid"}}
388 secret = &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secretname", Namespace: "ns", UID: "secretuid"}}
389 pod = &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "podname", Namespace: "ns", UID: "poduid"}}
390 node = &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "nodename", UID: "nodeuid"}}
391 )
392
393 deletionTestCases := []deletionTestCase{
394 {
395 name: "valid",
396 time: nil,
397 },
398 {
399 name: "deleted now",
400 time: &metav1.Time{Time: time.Unix(nowUnix, 0)},
401 },
402 {
403 name: "deleted near past",
404 time: &metav1.Time{Time: time.Unix(nowUnix-1, 0)},
405 },
406 {
407 name: "deleted near future",
408 time: &metav1.Time{Time: time.Unix(nowUnix+1, 0)},
409 },
410 {
411 name: "deleted now-leeway",
412 time: &metav1.Time{Time: time.Unix(nowUnix-60, 0)},
413 },
414 {
415 name: "deleted now-leeway-1",
416 time: &metav1.Time{Time: time.Unix(nowUnix-61, 0)},
417 expectErr: true,
418 },
419 }
420
421 testcases := []claimTestCase{
422 {
423 name: "good",
424 getter: fakeGetter{serviceAccount, nil, nil, nil},
425 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
426 expectErr: "",
427 },
428 {
429 name: "expired",
430 getter: fakeGetter{serviceAccount, nil, nil, nil},
431 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
432 expiry: *jwt.NewNumericDate(now().Add(-1_000 * time.Hour)),
433 expectErr: "service account token has expired",
434 },
435 {
436 name: "not yet valid",
437 getter: fakeGetter{serviceAccount, nil, nil, nil},
438 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
439 notBefore: *jwt.NewNumericDate(now().Add(1_000 * time.Hour)),
440 expectErr: "service account token is not valid yet",
441 },
442 {
443 name: "missing serviceaccount",
444 getter: fakeGetter{nil, nil, nil, nil},
445 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
446 expectErr: `serviceaccounts "saname" not found`,
447 },
448 {
449 name: "missing secret",
450 getter: fakeGetter{serviceAccount, nil, nil, nil},
451 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuid"}, Namespace: "ns"}},
452 expectErr: "service account token has been invalidated",
453 },
454 {
455 name: "missing pod",
456 getter: fakeGetter{serviceAccount, nil, nil, nil},
457 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduid"}, Namespace: "ns"}},
458 expectErr: "service account token has been invalidated",
459 },
460 {
461 name: "missing node",
462 getter: fakeGetter{serviceAccount, nil, nil, nil},
463 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Node: &ref{Name: "nodename", UID: "nodeuid"}, Namespace: "ns"}},
464 expectErr: "service account token has been invalidated",
465 featureNodeBindingValidation: true,
466 },
467 {
468 name: "different uid serviceaccount",
469 getter: fakeGetter{serviceAccount, nil, nil, nil},
470 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauidold"}, Namespace: "ns"}},
471 expectErr: "service account UID (sauid) does not match claim (sauidold)",
472 },
473 {
474 name: "different uid secret",
475 getter: fakeGetter{serviceAccount, secret, nil, nil},
476 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuidold"}, Namespace: "ns"}},
477 expectErr: "secret UID (secretuid) does not match service account secret ref claim (secretuidold)",
478 },
479 {
480 name: "different uid pod",
481 getter: fakeGetter{serviceAccount, nil, pod, nil},
482 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduidold"}, Namespace: "ns"}},
483 expectErr: "pod UID (poduid) does not match service account pod ref claim (poduidold)",
484 },
485 }
486
487 for _, deletionTestCase := range deletionTestCases {
488 var (
489 deletedServiceAccount = serviceAccount.DeepCopy()
490 deletedPod = pod.DeepCopy()
491 deletedSecret = secret.DeepCopy()
492 deletedNode = node.DeepCopy()
493 )
494 deletedServiceAccount.DeletionTimestamp = deletionTestCase.time
495 deletedPod.DeletionTimestamp = deletionTestCase.time
496 deletedSecret.DeletionTimestamp = deletionTestCase.time
497 deletedNode.DeletionTimestamp = deletionTestCase.time
498
499 var saDeletedErr, deletedErr string
500 if deletionTestCase.expectErr {
501 saDeletedErr = "service account ns/saname has been deleted"
502 deletedErr = "service account token has been invalidated"
503 }
504
505 testcases = append(testcases,
506 claimTestCase{
507 name: deletionTestCase.name + " serviceaccount",
508 getter: fakeGetter{deletedServiceAccount, nil, nil, nil},
509 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Namespace: "ns"}},
510 expectErr: saDeletedErr,
511 },
512 claimTestCase{
513 name: deletionTestCase.name + " secret",
514 getter: fakeGetter{serviceAccount, deletedSecret, nil, nil},
515 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Secret: &ref{Name: "secretname", UID: "secretuid"}, Namespace: "ns"}},
516 expectErr: deletedErr,
517 },
518 claimTestCase{
519 name: deletionTestCase.name + " pod",
520 getter: fakeGetter{serviceAccount, nil, deletedPod, nil},
521 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Pod: &ref{Name: "podname", UID: "poduid"}, Namespace: "ns"}},
522 expectErr: deletedErr,
523 },
524 claimTestCase{
525 name: deletionTestCase.name + " node",
526 getter: fakeGetter{serviceAccount, nil, nil, deletedNode},
527 private: &privateClaims{Kubernetes: kubernetes{Svcacct: ref{Name: "saname", UID: "sauid"}, Node: &ref{Name: "nodename", UID: "nodeuid"}, Namespace: "ns"}},
528 expectErr: deletedErr,
529 featureNodeBindingValidation: true,
530 },
531 )
532 }
533
534 for _, tc := range testcases {
535 t.Run(tc.name, func(t *testing.T) {
536 v := &validator{getter: tc.getter}
537 expiry := jwt.NumericDate(nowUnix)
538 if tc.expiry != 0 {
539 expiry = tc.expiry
540 }
541
542 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountTokenNodeBindingValidation, tc.featureNodeBindingValidation)()
543
544 _, err := v.Validate(context.Background(), "", &jwt.Claims{Expiry: &expiry, NotBefore: &tc.notBefore}, tc.private)
545 if len(tc.expectErr) > 0 {
546 if errStr := errString(err); tc.expectErr != errStr {
547 t.Fatalf("expected error %q but got %q", tc.expectErr, errStr)
548 }
549 } else if err != nil {
550 t.Fatalf("unexpected error: %v", err)
551 }
552 })
553 }
554 }
555
556 func errString(err error) string {
557 if err == nil {
558 return ""
559 }
560
561 return err.Error()
562 }
563
564 type fakeGetter struct {
565 serviceAccount *v1.ServiceAccount
566 secret *v1.Secret
567 pod *v1.Pod
568 node *v1.Node
569 }
570
571 func (f fakeGetter) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) {
572 if f.serviceAccount == nil {
573 return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "serviceaccounts"}, name)
574 }
575 return f.serviceAccount, nil
576 }
577 func (f fakeGetter) GetPod(namespace, name string) (*v1.Pod, error) {
578 if f.pod == nil {
579 return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, name)
580 }
581 return f.pod, nil
582 }
583 func (f fakeGetter) GetSecret(namespace, name string) (*v1.Secret, error) {
584 if f.secret == nil {
585 return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, name)
586 }
587 return f.secret, nil
588 }
589 func (f fakeGetter) GetNode(name string) (*v1.Node, error) {
590 if f.node == nil {
591 return nil, apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "nodes"}, name)
592 }
593 return f.node, nil
594 }
595
View as plain text