k8s.io/kubernetes@v1.29.3/pkg/serviceaccount/claims_test.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 "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 // epoch time: 1514764800 41 return time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC) 42 } 43 44 newUUID = func() string { 45 // always return a fixed/static UUID for testing 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 // input 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 // desired 89 sc *jwt.Claims 90 pc *privateClaims 91 92 featureJTI, featurePodNodeInfo, featureNodeBinding bool 93 }{ 94 { 95 // pod and secret 96 sa: sa, 97 pod: pod, 98 sec: sec, 99 // really fast 100 exp: 0, 101 // nil audience 102 aud: nil, 103 err: "internal error, token can only be bound to one object type", 104 }, 105 { 106 // pod 107 sa: sa, 108 pod: pod, 109 // empty audience 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 // secret 129 sa: sa, 130 sec: sec, 131 exp: 100, 132 // single member audience 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 // no obj binding 152 sa: sa, 153 exp: 100, 154 // multimember audience 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 // warn after provided 173 sa: sa, 174 pod: pod, 175 exp: 60 * 60 * 24, 176 warnafter: 60 * 60, 177 // nil audience 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 // node with feature gate disabled 197 sa: sa, 198 node: node, 199 // really fast 200 exp: 0, 201 // nil audience 202 aud: nil, 203 err: "token bound to Node object requested, but \"ServiceAccountTokenNodeBinding\" feature gate is disabled", 204 }, 205 { 206 // node & pod with feature gate disabled 207 sa: sa, 208 node: node, 209 pod: pod, 210 // really fast 211 exp: 0, 212 // nil audience 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 // node alone 231 sa: sa, 232 node: node, 233 // enable node binding feature 234 featureNodeBinding: true, 235 // really fast 236 exp: 0, 237 // nil audience 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 // node and pod 256 sa: sa, 257 pod: pod, 258 node: node, 259 // enable embedding pod node info feature 260 featurePodNodeInfo: true, 261 // really fast 262 exp: 0, 263 // nil audience 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 // node and secret should error 283 sa: sa, 284 sec: sec, 285 node: node, 286 // enable embedding node info feature 287 featureNodeBinding: true, 288 // really fast 289 exp: 0, 290 // nil audience 291 aud: nil, 292 err: "internal error, token can only be bound to one object type", 293 }, 294 { 295 // ensure JTI is set 296 sa: sa, 297 // enable setting JTI feature 298 featureJTI: true, 299 // really fast 300 exp: 0, 301 // nil audience 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 // ensure it fails if node binding gate is disabled 320 sa: sa, 321 node: node, 322 featureNodeBinding: false, 323 // really fast 324 exp: 0, 325 // nil audience 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 // comparing json spews has the benefit over 334 // reflect.DeepEqual that we are also asserting that 335 // claims structs are json serializable 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 // set feature flags for the duration of the test case 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 }