k8s.io/kubernetes@v1.29.3/test/integration/controlplane/transformation/kms_transformation_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 /* 5 Copyright 2017 The Kubernetes Authors. 6 7 Licensed under the Apache License, Version 2.0 (the "License"); 8 you may not use this file except in compliance with the License. 9 You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13 Unless required by applicable law or agreed to in writing, software 14 distributed under the License is distributed on an "AS IS" BASIS, 15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 See the License for the specific language governing permissions and 17 limitations under the License. 18 */ 19 20 package transformation 21 22 import ( 23 "bytes" 24 "context" 25 "crypto/aes" 26 "encoding/base64" 27 "encoding/binary" 28 "fmt" 29 "math/rand" 30 "os" 31 "path/filepath" 32 "regexp" 33 "strings" 34 "testing" 35 "time" 36 37 "github.com/google/go-cmp/cmp" 38 clientv3 "go.etcd.io/etcd/client/v3" 39 "golang.org/x/crypto/cryptobyte" 40 41 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 42 "k8s.io/apimachinery/pkg/api/meta" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" 45 "k8s.io/apimachinery/pkg/runtime/schema" 46 "k8s.io/apimachinery/pkg/util/sets" 47 "k8s.io/apimachinery/pkg/util/wait" 48 "k8s.io/apiserver/pkg/features" 49 genericapiserver "k8s.io/apiserver/pkg/server" 50 encryptionconfigcontroller "k8s.io/apiserver/pkg/server/options/encryptionconfig/controller" 51 "k8s.io/apiserver/pkg/storage/value" 52 aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes" 53 mock "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/testing/v1beta1" 54 utilfeature "k8s.io/apiserver/pkg/util/feature" 55 "k8s.io/client-go/dynamic" 56 "k8s.io/client-go/rest" 57 featuregatetesting "k8s.io/component-base/featuregate/testing" 58 kmsapi "k8s.io/kms/apis/v1beta1" 59 "k8s.io/kubernetes/test/integration" 60 "k8s.io/kubernetes/test/integration/etcd" 61 "k8s.io/kubernetes/test/integration/framework" 62 ) 63 64 const ( 65 dekKeySizeLen = 2 66 kmsAPIVersion = "v1beta1" 67 ) 68 69 type envelope struct { 70 providerName string 71 rawEnvelope []byte 72 plainTextDEK []byte 73 } 74 75 func (r envelope) prefix() string { 76 return fmt.Sprintf("k8s:enc:kms:v1:%s:", r.providerName) 77 } 78 79 func (r envelope) prefixLen() int { 80 return len(r.prefix()) 81 } 82 83 func (r envelope) dekLen() int { 84 // DEK's length is stored in the two bytes that follow the prefix. 85 return int(binary.BigEndian.Uint16(r.rawEnvelope[r.prefixLen() : r.prefixLen()+dekKeySizeLen])) 86 } 87 88 func (r envelope) cipherTextDEK() []byte { 89 return r.rawEnvelope[r.prefixLen()+dekKeySizeLen : r.prefixLen()+dekKeySizeLen+r.dekLen()] 90 } 91 92 func (r envelope) startOfPayload(providerName string) int { 93 return r.prefixLen() + dekKeySizeLen + r.dekLen() 94 } 95 96 func (r envelope) cipherTextPayload() []byte { 97 return r.rawEnvelope[r.startOfPayload(r.providerName):] 98 } 99 100 func (r envelope) plainTextPayload(secretETCDPath string) ([]byte, error) { 101 block, err := aes.NewCipher(r.plainTextDEK) 102 if err != nil { 103 return nil, fmt.Errorf("failed to initialize AES Cipher: %v", err) 104 } 105 // etcd path of the key is used as the authenticated context - need to pass it to decrypt 106 ctx := context.Background() 107 dataCtx := value.DefaultContext([]byte(secretETCDPath)) 108 aesgcmTransformer, err := aestransformer.NewGCMTransformer(block) 109 if err != nil { 110 return nil, fmt.Errorf("failed to create transformer from block: %v", err) 111 } 112 plainSecret, _, err := aesgcmTransformer.TransformFromStorage(ctx, r.cipherTextPayload(), dataCtx) 113 if err != nil { 114 return nil, fmt.Errorf("failed to transform from storage via AESGCM, err: %w", err) 115 } 116 117 return plainSecret, nil 118 } 119 120 // TestKMSProvider is an integration test between KubeAPI, ETCD and KMS Plugin 121 // Concretely, this test verifies the following integration contracts: 122 // 1. Raw records in ETCD that were processed by KMS Provider should be prefixed with k8s:enc:kms:v1:grpc-kms-provider-name: 123 // 2. Data Encryption Key (DEK) should be generated by envelopeTransformer and passed to KMS gRPC Plugin 124 // 3. KMS gRPC Plugin should encrypt the DEK with a Key Encryption Key (KEK) and pass it back to envelopeTransformer 125 // 4. The cipherTextPayload (ex. Secret) should be encrypted via AES CBC transform 126 // 5. Prefix-EncryptedDEK-EncryptedPayload structure should be deposited to ETCD 127 // 6. Direct AES GCM decryption of the cipherTextPayload written with AES CBC transform does not work 128 // 7. Existing AES CBC secrets should be un-enveloped on direct reads from Kube API Server 129 // 8. No-op updates to the secret should cause new AES GCM key to be used 130 // 9. Direct AES GCM decryption works after the new AES GCM key is used 131 func TestKMSProvider(t *testing.T) { 132 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv1, true)() 133 134 encryptionConfig := ` 135 kind: EncryptionConfiguration 136 apiVersion: apiserver.config.k8s.io/v1 137 resources: 138 - resources: 139 - secrets 140 providers: 141 - kms: 142 name: kms-provider 143 cachesize: 1000 144 endpoint: unix:///@kms-provider.sock 145 ` 146 providerName := "kms-provider" 147 pluginMock := mock.NewBase64Plugin(t, "@kms-provider.sock") 148 test, err := newTransformTest(t, encryptionConfig, false, "", nil) 149 if err != nil { 150 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) 151 } 152 defer test.cleanUp() 153 154 test.secret, err = test.createSecret(testSecret, testNamespace) 155 if err != nil { 156 t.Fatalf("Failed to create test secret, error: %v", err) 157 } 158 159 // Since Data Encryption Key (DEK) is randomly generated (per encryption operation), we need to ask KMS Mock for it. 160 plainTextDEK := pluginMock.LastEncryptRequest() 161 162 secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace) 163 rawEnvelope, err := test.getRawSecretFromETCD() 164 if err != nil { 165 t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) 166 } 167 envelopeData := envelope{ 168 providerName: providerName, 169 rawEnvelope: rawEnvelope, 170 plainTextDEK: plainTextDEK, 171 } 172 173 wantPrefix := "k8s:enc:kms:v1:kms-provider:" 174 if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefix)) { 175 t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope) 176 } 177 178 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 179 defer cancel() 180 decryptResponse, err := pluginMock.Decrypt(ctx, &kmsapi.DecryptRequest{Version: kmsAPIVersion, Cipher: envelopeData.cipherTextDEK()}) 181 if err != nil { 182 t.Fatalf("failed to decrypt DEK, %v", err) 183 } 184 dekPlainAsWouldBeSeenByETCD := decryptResponse.Plain 185 186 if !bytes.Equal(plainTextDEK, dekPlainAsWouldBeSeenByETCD) { 187 t.Fatalf("expected plainTextDEK %v to be passed to KMS Plugin, but got %s", 188 plainTextDEK, dekPlainAsWouldBeSeenByETCD) 189 } 190 191 plainSecret, err := envelopeData.plainTextPayload(secretETCDPath) 192 if err != nil { 193 t.Fatalf("failed to transform from storage via AESCBC, err: %v", err) 194 } 195 196 if !strings.Contains(string(plainSecret), secretVal) { 197 t.Fatalf("expected %q after decryption, but got %q", secretVal, string(plainSecret)) 198 } 199 200 secretClient := test.restClient.CoreV1().Secrets(testNamespace) 201 // Secrets should be un-enveloped on direct reads from Kube API Server. 202 s, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{}) 203 if err != nil { 204 t.Fatalf("failed to get Secret from %s, err: %v", testNamespace, err) 205 } 206 if secretVal != string(s.Data[secretKey]) { 207 t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey])) 208 } 209 210 // write data using AES CBC to simulate a downgrade 211 oldSecretBytes, err := base64.StdEncoding.DecodeString(oldSecret) 212 if err != nil { 213 t.Fatalf("failed to base64 decode old secret, err: %v", err) 214 } 215 oldKeyBytes, err := base64.StdEncoding.DecodeString(oldAESCBCKey) 216 if err != nil { 217 t.Fatalf("failed to base64 decode old key, err: %v", err) 218 } 219 block, err := aes.NewCipher(oldKeyBytes) 220 if err != nil { 221 t.Fatalf("invalid key, err: %v", err) 222 } 223 224 oldEncryptedSecretBytes, err := aestransformer.NewCBCTransformer(block).TransformToStorage(ctx, oldSecretBytes, value.DefaultContext(secretETCDPath)) 225 if err != nil { 226 t.Fatalf("failed to encrypt old secret, err: %v", err) 227 } 228 229 oldEncryptedSecretBuf := cryptobyte.NewBuilder(nil) 230 oldEncryptedSecretBuf.AddBytes([]byte(wantPrefix)) 231 oldEncryptedSecretBuf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { 232 b.AddBytes([]byte(oldAESCBCKey)) 233 }) 234 oldEncryptedSecretBuf.AddBytes(oldEncryptedSecretBytes) 235 236 _, err = test.writeRawRecordToETCD(secretETCDPath, oldEncryptedSecretBuf.BytesOrPanic()) 237 if err != nil { 238 t.Fatalf("failed to write old encrypted secret, err: %v", err) 239 } 240 241 // confirm that direct AES GCM decryption does not work 242 failingRawEnvelope, err := test.getRawSecretFromETCD() 243 if err != nil { 244 t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) 245 } 246 failingOldEnvelope := envelope{ 247 providerName: providerName, 248 rawEnvelope: failingRawEnvelope, 249 plainTextDEK: oldKeyBytes, 250 } 251 failingOldPlainSecret, err := failingOldEnvelope.plainTextPayload(secretETCDPath) 252 if err == nil { 253 t.Fatalf("AESGCM decryption failure not seen, data: %s", string(failingOldPlainSecret)) 254 } 255 256 // Existing AES CBC secrets should be un-enveloped on direct reads from Kube API Server. 257 oldSecretObj, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{}) 258 if err != nil { 259 t.Fatalf("failed to read old secret via Kube API, err: %v", err) 260 } 261 if oldSecretVal != string(oldSecretObj.Data[secretKey]) { 262 t.Fatalf("expected %s from KubeAPI, but got %s", oldSecretVal, string(oldSecretObj.Data[secretKey])) 263 } 264 265 // no-op update should cause new AES GCM key to be used 266 oldSecretUpdated, err := secretClient.Update(ctx, oldSecretObj, metav1.UpdateOptions{}) 267 if err != nil { 268 t.Fatalf("failed to update old secret via Kube API, err: %v", err) 269 } 270 if oldSecretObj.ResourceVersion == oldSecretUpdated.ResourceVersion { 271 t.Fatalf("old secret not updated on no-op write: %s", oldSecretObj.ResourceVersion) 272 } 273 274 // confirm that direct AES GCM decryption works 275 oldRawEnvelope, err := test.getRawSecretFromETCD() 276 if err != nil { 277 t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) 278 } 279 oldEnvelope := envelope{ 280 providerName: providerName, 281 rawEnvelope: oldRawEnvelope, 282 plainTextDEK: pluginMock.LastEncryptRequest(), 283 } 284 if !bytes.HasPrefix(oldRawEnvelope, []byte(wantPrefix)) { 285 t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, oldRawEnvelope) 286 } 287 oldPlainSecret, err := oldEnvelope.plainTextPayload(secretETCDPath) 288 if err != nil { 289 t.Fatalf("failed to transform from storage via AESGCM, err: %v", err) 290 } 291 if !strings.Contains(string(oldPlainSecret), oldSecretVal) { 292 t.Fatalf("expected %q after decryption, but got %q", oldSecretVal, string(oldPlainSecret)) 293 } 294 } 295 296 // TestECHotReload is an integration test that verifies hot reload of KMS encryption config works. 297 // This test asserts following scenarios: 298 // 1. start at 'kms-provider' 299 // 2. create some secrets 300 // 3. add 'new-kms-provider' as write KMS (this is okay because we only have 1 API server) 301 // 4. wait for config to be observed 302 // 5. run storage migration on secrets 303 // 6. confirm that secrets have the new prefix 304 // 7. remove 'kms-provider' 305 // 8. wait for config to be observed 306 // 9. confirm that reads still work 307 // 10. confirm that cluster wide secret read still works 308 // 11. confirm that api server can restart with last applied encryption config 309 func TestEncryptionConfigHotReload(t *testing.T) { 310 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv1, true)() 311 312 // this makes the test super responsive. It's set to a default of 1 minute. 313 encryptionconfigcontroller.EncryptionConfigFileChangePollDuration = time.Second 314 315 storageConfig := framework.SharedEtcd() 316 encryptionConfig := ` 317 kind: EncryptionConfiguration 318 apiVersion: apiserver.config.k8s.io/v1 319 resources: 320 - resources: 321 - secrets 322 providers: 323 - kms: 324 name: kms-provider 325 cachesize: 1000 326 endpoint: unix:///@kms-provider.sock 327 ` 328 329 genericapiserver.SetHostnameFuncForTests("testAPIServerID") 330 _ = mock.NewBase64Plugin(t, "@kms-provider.sock") 331 var restarted bool 332 test, err := newTransformTest(t, encryptionConfig, true, "", storageConfig) 333 if err != nil { 334 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) 335 } 336 defer func() { 337 if !restarted { 338 test.cleanUp() 339 } 340 }() 341 ctx := testContext(t) 342 343 // the global metrics registry persists across test runs - reset it here so we can make assertions 344 copyConfig := rest.CopyConfig(test.kubeAPIServer.ClientConfig) 345 copyConfig.GroupVersion = &schema.GroupVersion{} 346 copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer() 347 rc, err := rest.RESTClientFor(copyConfig) 348 if err != nil { 349 t.Fatal(err) 350 } 351 if err := rc.Delete().AbsPath("/metrics").Do(ctx).Error(); err != nil { 352 t.Fatal(err) 353 } 354 355 // assert that the metrics we collect during the test run match expectations 356 // NOTE: 2 successful automatic reload resulted from 2 config file updates 357 wantMetricStrings := []string{ 358 `apiserver_encryption_config_controller_automatic_reload_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",status="success"} FP`, 359 `apiserver_encryption_config_controller_automatic_reload_success_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795"} 2`, 360 } 361 362 test.secret, err = test.createSecret(testSecret, testNamespace) 363 if err != nil { 364 t.Fatalf("Failed to create test secret, error: %v", err) 365 } 366 367 // create a new secret in default namespace. This is to assert cluster wide read works after hot reload. 368 _, err = test.createSecret(fmt.Sprintf("%s-%s", testSecret, "1"), "default") 369 if err != nil { 370 t.Fatalf("Failed to create test secret in default namespace, error: %v", err) 371 } 372 373 _, err = test.createConfigMap(testConfigmap, testNamespace) 374 if err != nil { 375 t.Fatalf("Failed to create test configmap, error: %v", err) 376 } 377 378 // test if hot reload controller is healthy 379 mustBeHealthy(t, "/poststarthook/start-encryption-provider-config-automatic-reload", "ok", test.kubeAPIServer.ClientConfig) 380 381 encryptionConfigWithNewProvider := ` 382 kind: EncryptionConfiguration 383 apiVersion: apiserver.config.k8s.io/v1 384 resources: 385 - resources: 386 - secrets 387 providers: 388 - kms: 389 name: new-kms-provider-for-secrets 390 cachesize: 1000 391 endpoint: unix:///@new-kms-provider.sock 392 - kms: 393 name: kms-provider 394 cachesize: 1000 395 endpoint: unix:///@kms-provider.sock 396 - resources: 397 - configmaps 398 providers: 399 - kms: 400 name: new-kms-provider-for-configmaps 401 cachesize: 1000 402 endpoint: unix:///@new-kms-provider.sock 403 - identity: {} 404 ` 405 // start new KMS Plugin 406 _ = mock.NewBase64Plugin(t, "@new-kms-provider.sock") 407 // update encryption config 408 updateFile(t, test.configDir, encryptionConfigFileName, []byte(encryptionConfigWithNewProvider)) 409 410 wantPrefixForSecrets := "k8s:enc:kms:v1:new-kms-provider-for-secrets:" 411 412 // implementing this brute force approach instead of fancy channel notification to avoid test specific code in prod. 413 // wait for config to be observed 414 verifyIfKMSTransformersSwapped(t, wantPrefixForSecrets, "", test) 415 416 // run storage migration 417 // get secrets 418 419 secretsList, err := test.restClient.CoreV1().Secrets("").List( 420 ctx, 421 metav1.ListOptions{}, 422 ) 423 if err != nil { 424 t.Fatalf("failed to list secrets, err: %v", err) 425 } 426 427 for _, secret := range secretsList.Items { 428 // update secret 429 _, err = test.restClient.CoreV1().Secrets(secret.Namespace).Update( 430 ctx, 431 &secret, 432 metav1.UpdateOptions{}, 433 ) 434 if err != nil { 435 t.Fatalf("failed to update secret, err: %v", err) 436 } 437 } 438 439 // get configmaps 440 configmapsList, err := test.restClient.CoreV1().ConfigMaps("").List( 441 ctx, 442 metav1.ListOptions{}, 443 ) 444 if err != nil { 445 t.Fatalf("failed to list configmaps, err: %v", err) 446 } 447 448 for _, configmap := range configmapsList.Items { 449 // update configmap 450 _, err = test.restClient.CoreV1().ConfigMaps(configmap.Namespace).Update( 451 ctx, 452 &configmap, 453 metav1.UpdateOptions{}, 454 ) 455 if err != nil { 456 t.Fatalf("failed to update configmap, err: %v", err) 457 } 458 } 459 460 // assert that resources has new prefix 461 secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace) 462 rawEnvelope, err := test.getRawSecretFromETCD() 463 if err != nil { 464 t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) 465 } 466 467 // assert secret 468 if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefixForSecrets)) { 469 t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefixForSecrets, rawEnvelope) 470 } 471 472 rawConfigmapEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "configmaps", testConfigmap, testNamespace)) 473 if err != nil { 474 t.Fatalf("failed to read %s from etcd: %v", test.getETCDPathForResource(test.storageConfig.Prefix, "", "configmaps", testConfigmap, testNamespace), err) 475 } 476 477 // assert prefix for configmap 478 wantPrefixForConfigmaps := "k8s:enc:kms:v1:new-kms-provider-for-configmaps:" 479 if !bytes.HasPrefix(rawConfigmapEnvelope.Kvs[0].Value, []byte(wantPrefixForConfigmaps)) { 480 t.Fatalf("expected configmap to be prefixed with %s, but got %s", wantPrefixForConfigmaps, rawConfigmapEnvelope.Kvs[0].Value) 481 } 482 483 // remove old KMS provider 484 // verifyIfKMSTransformersSwapped sometimes passes even before the changes in the encryption config file are observed. 485 // this causes the metrics tests to fail, which validate two config changes. 486 // this may happen when an existing KMS provider is already running (e.g., new-kms-provider-for-secrets in this case). 487 // to ensure that the changes are observed, we added one more provider (kms-provider-to-encrypt-all) and are validating it in verifyIfKMSTransformersSwapped. 488 encryptionConfigWithoutOldProvider := ` 489 kind: EncryptionConfiguration 490 apiVersion: apiserver.config.k8s.io/v1 491 resources: 492 - resources: 493 - secrets 494 providers: 495 - kms: 496 name: new-kms-provider-for-secrets 497 cachesize: 1000 498 endpoint: unix:///@new-kms-provider.sock 499 - resources: 500 - configmaps 501 providers: 502 - kms: 503 name: new-kms-provider-for-configmaps 504 cachesize: 1000 505 endpoint: unix:///@new-kms-provider.sock 506 - resources: 507 - '*.*' 508 providers: 509 - kms: 510 name: kms-provider-to-encrypt-all 511 cachesize: 1000 512 endpoint: unix:///@new-encrypt-all-kms-provider.sock 513 - identity: {} 514 ` 515 // start new KMS Plugin 516 _ = mock.NewBase64Plugin(t, "@new-encrypt-all-kms-provider.sock") 517 518 // update encryption config and wait for hot reload 519 updateFile(t, test.configDir, encryptionConfigFileName, []byte(encryptionConfigWithoutOldProvider)) 520 521 wantPrefixForEncryptAll := "k8s:enc:kms:v1:kms-provider-to-encrypt-all:" 522 523 // wait for config to be observed 524 verifyIfKMSTransformersSwapped(t, wantPrefixForSecrets, wantPrefixForEncryptAll, test) 525 526 // confirm that reading secrets still works 527 _, err = test.restClient.CoreV1().Secrets(testNamespace).Get( 528 ctx, 529 testSecret, 530 metav1.GetOptions{}, 531 ) 532 if err != nil { 533 t.Fatalf("failed to read secret, err: %v", err) 534 } 535 536 // make sure cluster wide secrets read still works 537 _, err = test.restClient.CoreV1().Secrets("").List(ctx, metav1.ListOptions{}) 538 if err != nil { 539 t.Fatalf("failed to list secrets, err: %v", err) 540 } 541 542 // make sure cluster wide configmaps read still works 543 _, err = test.restClient.CoreV1().ConfigMaps("").List(ctx, metav1.ListOptions{}) 544 if err != nil { 545 t.Fatalf("failed to list configmaps, err: %v", err) 546 } 547 548 // restart kube-apiserver with last applied encryption config and assert that server can start 549 previousConfigDir := test.configDir 550 test.shutdownAPIServer() 551 restarted = true 552 test, err = newTransformTest(t, test.transformerConfig, true, previousConfigDir, storageConfig) 553 if err != nil { 554 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) 555 } 556 defer test.cleanUp() 557 558 _, err = test.restClient.CoreV1().Secrets(testNamespace).Get( 559 ctx, 560 testSecret, 561 metav1.GetOptions{}, 562 ) 563 if err != nil { 564 t.Fatalf("failed to read secret, err: %v", err) 565 } 566 567 // confirm that reading cluster wide secrets still works after restart 568 if _, err = test.restClient.CoreV1().Secrets("").List(ctx, metav1.ListOptions{}); err != nil { 569 t.Fatalf("failed to list secrets, err: %v", err) 570 } 571 572 // make sure cluster wide configmaps read still works 573 if _, err = test.restClient.CoreV1().ConfigMaps("").List(ctx, metav1.ListOptions{}); err != nil { 574 t.Fatalf("failed to list configmaps, err: %v", err) 575 } 576 577 // recreate rest client with the new transformTest 578 copyConfig = rest.CopyConfig(test.kubeAPIServer.ClientConfig) 579 copyConfig.GroupVersion = &schema.GroupVersion{} 580 copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer() 581 rc, err = rest.RESTClientFor(copyConfig) 582 if err != nil { 583 t.Fatal(err) 584 } 585 defer func() { 586 body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx) 587 if err != nil { 588 t.Fatal(err) 589 } 590 var gotMetricStrings []string 591 trimFP := regexp.MustCompile(`(.*)(} \d+\.\d+.*)`) 592 for _, line := range strings.Split(string(body), "\n") { 593 if strings.HasPrefix(line, "apiserver_encryption_config_controller_") { 594 if strings.Contains(line, "_seconds") { 595 line = trimFP.ReplaceAllString(line, `$1`) + "} FP" // ignore floating point metric values 596 } 597 gotMetricStrings = append(gotMetricStrings, line) 598 } 599 } 600 if diff := cmp.Diff(wantMetricStrings, gotMetricStrings); diff != "" { 601 t.Errorf("unexpected metrics diff (-want +got): %s", diff) 602 } 603 }() 604 } 605 606 func TestEncryptAll(t *testing.T) { 607 encryptionConfig := ` 608 kind: EncryptionConfiguration 609 apiVersion: apiserver.config.k8s.io/v1 610 resources: 611 - resources: 612 - '*.*' 613 providers: 614 - kms: 615 name: encrypt-all-kms-provider 616 cachesize: 1000 617 endpoint: unix:///@encrypt-all-kms-provider.sock 618 ` 619 620 t.Run("encrypt all resources", func(t *testing.T) { 621 _ = mock.NewBase64Plugin(t, "@encrypt-all-kms-provider.sock") 622 // To ensure we are checking all REST resources 623 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, "AllAlpha", true)() 624 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, "AllBeta", true)() 625 // Need to enable this explicitly as the feature is deprecated 626 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv1, true)() 627 628 test, err := newTransformTest(t, encryptionConfig, false, "", nil) 629 if err != nil { 630 t.Fatalf("failed to start KUBE API Server with encryptionConfig") 631 } 632 defer test.cleanUp() 633 634 etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(test.kubeAPIServer.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...) 635 636 _, serverResources, err := test.restClient.Discovery().ServerGroupsAndResources() 637 if err != nil { 638 t.Fatal(err) 639 } 640 resources := etcd.GetResources(t, serverResources) 641 client := dynamic.NewForConfigOrDie(test.kubeAPIServer.ClientConfig) 642 643 etcdStorageData := etcd.GetEtcdStorageDataForNamespace(testNamespace) 644 restResourceSet := sets.New[schema.GroupVersionResource]() 645 stubResourceSet := sets.New[schema.GroupVersionResource]() 646 for _, resource := range resources { 647 gvr := resource.Mapping.Resource 648 stub := etcdStorageData[gvr].Stub 649 650 // continue if stub is empty 651 if stub == "" { 652 t.Errorf("skipping resource %s because stub is empty", gvr) 653 continue 654 } 655 restResourceSet.Insert(gvr) 656 dynamicClient, obj, err := etcd.JSONToUnstructured(stub, testNamespace, &meta.RESTMapping{ 657 Resource: gvr, 658 GroupVersionKind: gvr.GroupVersion().WithKind(resource.Mapping.GroupVersionKind.Kind), 659 Scope: resource.Mapping.Scope, 660 }, client) 661 if err != nil { 662 t.Fatal(err) 663 } 664 665 _, err = dynamicClient.Create(context.TODO(), obj, metav1.CreateOptions{}) 666 if err != nil { 667 t.Fatal(err) 668 } 669 } 670 for gvr, data := range etcdStorageData { 671 if data.Stub == "" { 672 continue 673 } 674 stubResourceSet.Insert(gvr) 675 } 676 if !restResourceSet.Equal(stubResourceSet) { 677 t.Errorf("failed to check all REST resources: %q", restResourceSet.SymmetricDifference(stubResourceSet).UnsortedList()) 678 } 679 rawClient, etcdClient, err := integration.GetEtcdClients(test.kubeAPIServer.ServerOpts.Etcd.StorageConfig.Transport) 680 if err != nil { 681 t.Fatalf("failed to create etcd client: %v", err) 682 } 683 // kvClient is a wrapper around rawClient and to avoid leaking goroutines we need to 684 // close the client (which we can do by closing rawClient). 685 defer rawClient.Close() 686 687 response, err := etcdClient.Get(context.TODO(), "/"+test.kubeAPIServer.ServerOpts.Etcd.StorageConfig.Prefix, clientv3.WithPrefix()) 688 if err != nil { 689 t.Fatalf("failed to retrieve secret from etcd %v", err) 690 } 691 692 // assert that total key values in response in greater than 0 693 if len(response.Kvs) == 0 { 694 t.Fatalf("expected total number of keys to be greater than 0, but got %d", len(response.Kvs)) 695 } 696 697 // assert that total response keys are greater or equal to total resources 698 if len(response.Kvs) < len(resources) { 699 t.Fatalf("expected total number of keys to be greater or equal to total resources, but got %d", len(response.Kvs)) 700 } 701 702 wantPrefix := "k8s:enc:kms:v1:encrypt-all-kms-provider:" 703 for _, kv := range response.Kvs { 704 // the following resources are not encrypted as they are not REST APIs and hence are not expected 705 // to be encrypted because it would be impossible to perform a storage migration on them 706 if strings.Contains(kv.String(), "masterleases") || 707 strings.Contains(kv.String(), "peerserverleases") || 708 strings.Contains(kv.String(), "serviceips") || 709 strings.Contains(kv.String(), "servicenodeports") { 710 // assert that these resources are not encrypted with any provider 711 if bytes.HasPrefix(kv.Value, []byte("k8s:enc:")) { 712 t.Errorf("expected resource %s to not be prefixed with %s, but got %s", kv.Key, "k8s:enc:", kv.Value) 713 } 714 continue 715 } 716 717 // assert that all other resources are encrypted 718 if !bytes.HasPrefix(kv.Value, []byte(wantPrefix)) { 719 t.Errorf("expected resource %s to be prefixed with %s, but got %s", kv.Key, wantPrefix, kv.Value) 720 } 721 } 722 }) 723 } 724 725 func TestEncryptAllWithWildcard(t *testing.T) { 726 encryptionConfig := ` 727 kind: EncryptionConfiguration 728 apiVersion: apiserver.config.k8s.io/v1 729 resources: 730 - resources: 731 - configmaps 732 providers: 733 - identity: {} 734 - resources: 735 - '*.batch' 736 providers: 737 - kms: 738 name: kms-provider 739 cachesize: 1000 740 endpoint: unix:///@kms-provider.sock 741 - resources: 742 - '*.*' 743 providers: 744 - kms: 745 name: encrypt-all-kms-provider 746 cachesize: 1000 747 endpoint: unix:///@encrypt-all-kms-provider.sock 748 ` 749 _ = mock.NewBase64Plugin(t, "@kms-provider.sock") 750 _ = mock.NewBase64Plugin(t, "@encrypt-all-kms-provider.sock") 751 752 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv1, true)() 753 754 test, err := newTransformTest(t, encryptionConfig, false, "", nil) 755 if err != nil { 756 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) 757 } 758 defer test.cleanUp() 759 760 wantPrefix := "k8s:enc:kms:v1:kms-provider:" 761 wantPrefixForEncryptAll := "k8s:enc:kms:v1:encrypt-all-kms-provider:" 762 763 _, err = test.createJob("test-job", "default") 764 if err != nil { 765 t.Fatalf("failed to create job: %v", err) 766 } 767 768 rawJobsEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "jobs", "test-job", "default")) 769 if err != nil { 770 t.Fatalf("failed to read %s from etcd: %v", test.getETCDPathForResource(test.storageConfig.Prefix, "", "jobs", "test-job", "default"), err) 771 } 772 773 // assert prefix for jobs 774 if !bytes.HasPrefix(rawJobsEnvelope.Kvs[0].Value, []byte(wantPrefix)) { 775 t.Fatalf("expected jobs to be prefixed with %s, but got %s", wantPrefix, rawJobsEnvelope.Kvs[0].Value) 776 } 777 778 _, err = test.createDeployment("test-deployment", "default") 779 if err != nil { 780 t.Fatalf("failed to create deployment: %v", err) 781 } 782 783 rawDeploymentsEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "deployments", "test-deployment", "default")) 784 if err != nil { 785 t.Fatalf("failed to read %s from etcd: %v", test.getETCDPathForResource(test.storageConfig.Prefix, "", "deployments", "test-deployment", "default"), err) 786 } 787 788 // assert prefix for deployments 789 if !bytes.HasPrefix(rawDeploymentsEnvelope.Kvs[0].Value, []byte(wantPrefixForEncryptAll)) { 790 t.Fatalf("expected deployments to be prefixed with %s, but got %s", wantPrefixForEncryptAll, rawDeploymentsEnvelope.Kvs[0].Value) 791 } 792 793 test.secret, err = test.createSecret(testSecret, testNamespace) 794 if err != nil { 795 t.Fatalf("Failed to create test secret, error: %v", err) 796 } 797 798 rawSecretEnvelope, err := test.getRawSecretFromETCD() 799 if err != nil { 800 t.Fatalf("failed to read secrets from etcd: %v", err) 801 } 802 803 // assert prefix for secrets 804 if !bytes.HasPrefix(rawSecretEnvelope, []byte(wantPrefixForEncryptAll)) { 805 t.Fatalf("expected secrets to be prefixed with %s, but got %s", wantPrefixForEncryptAll, rawSecretEnvelope) 806 } 807 808 _, err = test.createConfigMap(testConfigmap, testNamespace) 809 if err != nil { 810 t.Fatalf("Failed to create test configmap, error: %v", err) 811 } 812 813 rawConfigMapEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "configmaps", testConfigmap, testNamespace)) 814 if err != nil { 815 t.Fatalf("failed to read configmaps from etcd: %v", err) 816 } 817 818 // assert configmaps do not have the encrypted data prefix 819 if bytes.HasPrefix(rawConfigMapEnvelope.Kvs[0].Value, []byte("k8s:enc:")) { 820 t.Fatalf("expected configmaps to be not encrypted, got %s", rawConfigMapEnvelope.Kvs[0].Value) 821 } 822 } 823 824 func TestEncryptionConfigHotReloadFilePolling(t *testing.T) { 825 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv1, true)() 826 827 // this makes the test super responsive. It's set to a default of 1 minute. 828 encryptionconfigcontroller.EncryptionConfigFileChangePollDuration = time.Second 829 830 testCases := []struct { 831 sleep time.Duration 832 name string 833 updateFile func(filePath, fileContent string) error 834 }{ 835 { 836 name: "truncate file", 837 updateFile: func(filePath string, fileContent string) error { 838 // os.WriteFile truncates the file before writing 839 return os.WriteFile(filePath, []byte(fileContent), 0644) 840 }, 841 // significantly longer than KMSCloseGracePeriod 842 sleep: 20 * time.Second, 843 }, 844 { 845 name: "delete and create file", 846 updateFile: func(filePath, fileContent string) error { 847 // os.Remove deletes the file before creating a new one 848 if err := os.Remove(filePath); err != nil { 849 return fmt.Errorf("failed to remove encryption config, err: %w", err) 850 } 851 852 file, err := os.Create(filePath) 853 if err != nil { 854 return fmt.Errorf("failed to create encryption config, err: %w", err) 855 } 856 defer file.Close() 857 858 if _, err := file.Write([]byte(fileContent)); err != nil { 859 return fmt.Errorf("failed to write encryption config, err: %w", err) 860 } 861 862 return nil 863 }, 864 }, 865 { 866 name: "move file", 867 updateFile: func(filePath, fileContent string) error { 868 // write new config to a temp file 869 tmpFilePath := filePath + ".tmp" 870 if err := os.WriteFile(tmpFilePath, []byte(fileContent), 0644); err != nil { 871 return fmt.Errorf("failed to write config to tmp file, err: %w", err) 872 } 873 874 // move the temp file to the original file 875 if err := os.Rename(tmpFilePath, filePath); err != nil { 876 return fmt.Errorf("failed to move encryption config, err: %w", err) 877 } 878 879 return nil 880 }, 881 }, 882 } 883 884 for _, tc := range testCases { 885 t.Run(tc.name, func(t *testing.T) { 886 encryptionConfig := ` 887 kind: EncryptionConfiguration 888 apiVersion: apiserver.config.k8s.io/v1 889 resources: 890 - resources: 891 - secrets 892 providers: 893 - kms: 894 name: kms-provider 895 cachesize: 1000 896 endpoint: unix:///@kms-provider.sock 897 timeout: 1s 898 ` 899 _ = mock.NewBase64Plugin(t, "@kms-provider.sock") 900 901 test, err := newTransformTest(t, encryptionConfig, true, "", nil) 902 if err != nil { 903 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) 904 } 905 defer test.cleanUp() 906 907 test.secret, err = test.createSecret(testSecret, testNamespace) 908 if err != nil { 909 t.Fatalf("Failed to create test secret, error: %v", err) 910 } 911 912 // test if hot reload controller is healthy 913 mustBeHealthy(t, "/poststarthook/start-encryption-provider-config-automatic-reload", "ok", test.kubeAPIServer.ClientConfig) 914 915 encryptionConfigWithNewProvider := ` 916 kind: EncryptionConfiguration 917 apiVersion: apiserver.config.k8s.io/v1 918 resources: 919 - resources: 920 - secrets 921 providers: 922 - kms: 923 name: new-kms-provider-for-secrets 924 cachesize: 1000 925 endpoint: unix:///@new-kms-provider.sock 926 timeout: 1s 927 - kms: 928 name: kms-provider 929 cachesize: 1000 930 endpoint: unix:///@kms-provider.sock 931 timeout: 1s 932 - resources: 933 - configmaps 934 providers: 935 - kms: 936 name: new-kms-provider-for-configmaps 937 cachesize: 1000 938 endpoint: unix:///@new-kms-provider.sock 939 timeout: 1s 940 - identity: {} 941 ` 942 // start new KMS Plugin 943 _ = mock.NewBase64Plugin(t, "@new-kms-provider.sock") 944 // update encryption config 945 if err := tc.updateFile(filepath.Join(test.configDir, encryptionConfigFileName), encryptionConfigWithNewProvider); err != nil { 946 t.Fatalf("failed to update encryption config, err: %v", err) 947 } 948 949 wantPrefix := "k8s:enc:kms:v1:new-kms-provider-for-secrets:" 950 verifyPrefixOfSecretResource(t, wantPrefix, test) 951 952 // make sure things still work at a "later" time 953 if tc.sleep != 0 { 954 time.Sleep(tc.sleep) 955 } 956 _, err = test.createSecret(fmt.Sprintf("secret-%d", rand.Intn(100000)), "default") 957 if err != nil { 958 t.Fatalf("Failed to create test secret, error: %v", err) 959 } 960 _, err = test.restClient.CoreV1().Secrets("").List( 961 context.TODO(), 962 metav1.ListOptions{}, 963 ) 964 if err != nil { 965 t.Fatalf("failed to re-list secrets, err: %v", err) 966 } 967 }) 968 } 969 } 970 971 func verifyPrefixOfSecretResource(t *testing.T, wantPrefix string, test *transformTest) { 972 // implementing this brute force approach instead of fancy channel notification to avoid test specific code in prod. 973 // wait for config to be observed 974 verifyIfKMSTransformersSwapped(t, wantPrefix, "", test) 975 976 // run storage migration 977 secretsList, err := test.restClient.CoreV1().Secrets("").List( 978 context.TODO(), 979 metav1.ListOptions{}, 980 ) 981 if err != nil { 982 t.Fatalf("failed to list secrets, err: %v", err) 983 } 984 985 for _, secret := range secretsList.Items { 986 _, err = test.restClient.CoreV1().Secrets(secret.Namespace).Update( 987 context.TODO(), 988 &secret, 989 metav1.UpdateOptions{}, 990 ) 991 if err != nil { 992 t.Fatalf("failed to update secret, err: %v", err) 993 } 994 } 995 996 secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace) 997 rawEnvelope, err := test.getRawSecretFromETCD() 998 if err != nil { 999 t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) 1000 } 1001 1002 // assert that resources has new prefix 1003 if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefix)) { 1004 t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope) 1005 } 1006 } 1007 1008 func verifyIfKMSTransformersSwapped(t *testing.T, wantPrefix, wantPrefixForEncryptAll string, test *transformTest) { 1009 t.Helper() 1010 1011 var swapErr error 1012 // delete and recreate same secret flakes, so create a new secret with a different index until new prefix is observed 1013 // generate a random int to be used in secret name 1014 idx := rand.Intn(100000) 1015 1016 pollErr := wait.PollImmediate(time.Second, wait.ForeverTestTimeout, func() (bool, error) { 1017 // create secret 1018 secretName := fmt.Sprintf("secret-%d", idx) 1019 _, err := test.createSecret(secretName, "default") 1020 if err != nil { 1021 t.Fatalf("Failed to create test secret, error: %v", err) 1022 } 1023 1024 rawEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", secretName, "default")) 1025 if err != nil { 1026 t.Fatalf("failed to read %s from etcd: %v", test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", secretName, "default"), err) 1027 } 1028 1029 // check prefix 1030 if !bytes.HasPrefix(rawEnvelope.Kvs[0].Value, []byte(wantPrefix)) { 1031 idx++ 1032 1033 swapErr = fmt.Errorf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope.Kvs[0].Value) 1034 1035 // return nil error to continue polling till timeout 1036 return false, nil 1037 } 1038 1039 if wantPrefixForEncryptAll != "" { 1040 deploymentName := fmt.Sprintf("deployment-%d", idx) 1041 _, err := test.createDeployment(deploymentName, "default") 1042 if err != nil { 1043 t.Fatalf("Failed to create test secret, error: %v", err) 1044 } 1045 1046 rawEnvelope, err := test.readRawRecordFromETCD(test.getETCDPathForResource(test.storageConfig.Prefix, "", "deployments", deploymentName, "default")) 1047 if err != nil { 1048 t.Fatalf("failed to read %s from etcd: %v", test.getETCDPathForResource(test.storageConfig.Prefix, "", "deployments", deploymentName, "default"), err) 1049 } 1050 1051 // check prefix 1052 if !bytes.HasPrefix(rawEnvelope.Kvs[0].Value, []byte(wantPrefixForEncryptAll)) { 1053 idx++ 1054 1055 swapErr = fmt.Errorf("expected deployment to be prefixed with %s, but got %s", wantPrefixForEncryptAll, rawEnvelope.Kvs[0].Value) 1056 1057 // return nil error to continue polling till timeout 1058 return false, nil 1059 } 1060 } 1061 1062 return true, nil 1063 }) 1064 if pollErr == wait.ErrWaitTimeout { 1065 t.Fatalf("failed to verify if kms transformers swapped, err: %v", swapErr) 1066 } 1067 } 1068 1069 func updateFile(t *testing.T, configDir, filename string, newContent []byte) { 1070 t.Helper() 1071 1072 // Create a temporary file 1073 tempFile, err := os.CreateTemp(configDir, "tempfile") 1074 if err != nil { 1075 t.Fatal(err) 1076 } 1077 defer tempFile.Close() 1078 1079 // Write the new content to the temporary file 1080 _, err = tempFile.Write(newContent) 1081 if err != nil { 1082 t.Fatal(err) 1083 } 1084 1085 // Atomically replace the original file with the temporary file 1086 err = os.Rename(tempFile.Name(), filepath.Join(configDir, filename)) 1087 if err != nil { 1088 t.Fatal(err) 1089 } 1090 } 1091 1092 func TestKMSHealthz(t *testing.T) { 1093 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv1, true)() 1094 1095 encryptionConfig := ` 1096 kind: EncryptionConfiguration 1097 apiVersion: apiserver.config.k8s.io/v1 1098 resources: 1099 - resources: 1100 - secrets 1101 providers: 1102 - kms: 1103 name: provider-1 1104 endpoint: unix:///@kms-provider-1.sock 1105 - kms: 1106 name: provider-2 1107 endpoint: unix:///@kms-provider-2.sock 1108 ` 1109 1110 pluginMock1 := mock.NewBase64Plugin(t, "@kms-provider-1.sock") 1111 pluginMock2 := mock.NewBase64Plugin(t, "@kms-provider-2.sock") 1112 1113 test, err := newTransformTest(t, encryptionConfig, false, "", nil) 1114 if err != nil { 1115 t.Fatalf("failed to start kube-apiserver, error: %v", err) 1116 } 1117 defer test.cleanUp() 1118 1119 // Name of the healthz check is always "kms-provider-0" and it covers all kms plugins. 1120 1121 // Stage 1 - Since all kms-plugins are guaranteed to be up, healthz checks for: 1122 // healthz/kms-provider-0 and /healthz/kms-provider-1 should be OK. 1123 mustBeHealthy(t, "/kms-provider-0", "ok", test.kubeAPIServer.ClientConfig) 1124 mustBeHealthy(t, "/kms-provider-1", "ok", test.kubeAPIServer.ClientConfig) 1125 mustNotHaveLivez(t, "/kms-provider-0", "404 page not found", test.kubeAPIServer.ClientConfig) 1126 mustNotHaveLivez(t, "/kms-provider-1", "404 page not found", test.kubeAPIServer.ClientConfig) 1127 1128 // Stage 2 - kms-plugin for provider-1 is down. Therefore, expect the healthz check 1129 // to fail and report that provider-1 is down 1130 pluginMock1.EnterFailedState() 1131 mustBeUnHealthy(t, "/kms-provider-0", 1132 "internal server error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled", 1133 test.kubeAPIServer.ClientConfig) 1134 1135 mustNotHaveLivez(t, "/kms-provider-0", "404 page not found", test.kubeAPIServer.ClientConfig) 1136 mustBeHealthy(t, "/kms-provider-1", "ok", test.kubeAPIServer.ClientConfig) 1137 mustNotHaveLivez(t, "/kms-provider-1", "404 page not found", test.kubeAPIServer.ClientConfig) 1138 pluginMock1.ExitFailedState() 1139 1140 // Stage 3 - kms-plugin for provider-1 is now up. Therefore, expect the health check for provider-1 1141 // to succeed now, but provider-2 is now down. 1142 pluginMock2.EnterFailedState() 1143 mustBeHealthy(t, "/kms-provider-0", "ok", test.kubeAPIServer.ClientConfig) 1144 mustBeUnHealthy(t, "/kms-provider-1", 1145 "internal server error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled", 1146 test.kubeAPIServer.ClientConfig) 1147 pluginMock2.ExitFailedState() 1148 1149 // Stage 4 - All kms-plugins are once again up, 1150 // the healthz check should be OK. 1151 mustBeHealthy(t, "/kms-provider-0", "ok", test.kubeAPIServer.ClientConfig) 1152 mustBeHealthy(t, "/kms-provider-1", "ok", test.kubeAPIServer.ClientConfig) 1153 } 1154 1155 func TestKMSHealthzWithReload(t *testing.T) { 1156 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KMSv1, true)() 1157 1158 encryptionConfig := ` 1159 kind: EncryptionConfiguration 1160 apiVersion: apiserver.config.k8s.io/v1 1161 resources: 1162 - resources: 1163 - secrets 1164 providers: 1165 - kms: 1166 name: provider-1 1167 endpoint: unix:///@kms-provider-1.sock 1168 - kms: 1169 name: provider-2 1170 endpoint: unix:///@kms-provider-2.sock 1171 ` 1172 1173 pluginMock1 := mock.NewBase64Plugin(t, "@kms-provider-1.sock") 1174 pluginMock2 := mock.NewBase64Plugin(t, "@kms-provider-2.sock") 1175 1176 test, err := newTransformTest(t, encryptionConfig, true, "", nil) 1177 if err != nil { 1178 t.Fatalf("Failed to start kube-apiserver, error: %v", err) 1179 } 1180 defer test.cleanUp() 1181 1182 // Name of the healthz check is always "kms-provider-0" and it covers all kms plugins. 1183 1184 // Stage 1 - Since all kms-plugins are guaranteed to be up, 1185 // the healthz check should be OK. 1186 mustBeHealthy(t, "/kms-providers", "ok", test.kubeAPIServer.ClientConfig) 1187 1188 // Stage 2 - kms-plugin for provider-1 is down. Therefore, expect the healthz check 1189 // to fail and report that provider-1 is down 1190 pluginMock1.EnterFailedState() 1191 mustBeUnHealthy(t, "/kms-providers", 1192 "internal server error: kms-provider-0: failed to perform encrypt section of the healthz check for KMS Provider provider-1, error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled", 1193 test.kubeAPIServer.ClientConfig) 1194 pluginMock1.ExitFailedState() 1195 1196 // Stage 3 - kms-plugin for provider-1 is now up. Therefore, expect the health check for provider-1 1197 // to succeed now, but provider-2 is now down. 1198 pluginMock2.EnterFailedState() 1199 mustBeUnHealthy(t, "/kms-providers", 1200 "internal server error: kms-provider-1: failed to perform encrypt section of the healthz check for KMS Provider provider-2, error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled", 1201 test.kubeAPIServer.ClientConfig) 1202 pluginMock2.ExitFailedState() 1203 1204 // Stage 4 - All kms-plugins are once again up, 1205 // the healthz check should be OK. 1206 mustBeHealthy(t, "/kms-providers", "ok", test.kubeAPIServer.ClientConfig) 1207 }