//go:build !windows // +build !windows /* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package transformation import ( "bytes" "context" "crypto/aes" "crypto/cipher" "encoding/binary" "fmt" "io" "path" "regexp" "strings" "testing" "time" "github.com/gogo/protobuf/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" clientv3 "go.etcd.io/etcd/client/v3" corev1 "k8s.io/api/core/v1" apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/options/encryptionconfig" "k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/value" aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes" "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2" kmstypes "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2/v2" kmsv2mock "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/testing/v2" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/klog/v2" kmsv2api "k8s.io/kms/apis/v2" kmsv2svc "k8s.io/kms/pkg/service" "k8s.io/kubernetes/pkg/api/legacyscheme" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/controlplane" "k8s.io/kubernetes/pkg/kubeapiserver" secretstore "k8s.io/kubernetes/pkg/registry/core/secret/storage" "k8s.io/kubernetes/test/integration" "k8s.io/kubernetes/test/integration/etcd" "k8s.io/kubernetes/test/integration/framework" ) type envelopekmsv2 struct { providerName string rawEnvelope []byte plainTextDEKSource []byte useSeed bool } func (r envelopekmsv2) prefix() string { return fmt.Sprintf("k8s:enc:kms:v2:%s:", r.providerName) } func (r envelopekmsv2) prefixLen() int { return len(r.prefix()) } func (r envelopekmsv2) cipherTextDEKSource() ([]byte, error) { o := &kmstypes.EncryptedObject{} if err := proto.Unmarshal(r.rawEnvelope[r.startOfPayload(r.providerName):], o); err != nil { return nil, err } if err := kmsv2.ValidateEncryptedObject(o); err != nil { return nil, err } if r.useSeed && o.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED { return nil, fmt.Errorf("wrong type used with useSeed=true") } if !r.useSeed && o.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_AES_GCM_KEY { return nil, fmt.Errorf("wrong type used with useSeed=false") } return o.EncryptedDEKSource, nil } func (r envelopekmsv2) startOfPayload(_ string) int { return r.prefixLen() } func (r envelopekmsv2) cipherTextPayload() ([]byte, error) { o := &kmstypes.EncryptedObject{} if err := proto.Unmarshal(r.rawEnvelope[r.startOfPayload(r.providerName):], o); err != nil { return nil, err } if err := kmsv2.ValidateEncryptedObject(o); err != nil { return nil, err } return o.EncryptedData, nil } func (r envelopekmsv2) plainTextPayload(secretETCDPath string) ([]byte, error) { var transformer value.Read var err error if r.useSeed { transformer, err = aestransformer.NewHKDFExtendedNonceGCMTransformer(r.plainTextDEKSource) } else { var block cipher.Block block, err = aes.NewCipher(r.plainTextDEKSource) if err != nil { return nil, err } transformer, err = aestransformer.NewGCMTransformer(block) } if err != nil { return nil, err } ctx := context.Background() dataCtx := value.DefaultContext(secretETCDPath) data, err := r.cipherTextPayload() if err != nil { return nil, fmt.Errorf("failed to get cipher text payload: %v", err) } plainSecret, _, err := transformer.TransformFromStorage(ctx, data, dataCtx) if err != nil { return nil, fmt.Errorf("failed to transform from storage via AESGCM, err: %w", err) } return plainSecret, nil } // TestDefaultValues tests default flag values without setting any of the feature flags or // calling SetKDFForTests, and assert that the data stored in etcd is using KDF func TestDefaultValues(t *testing.T) { if encryptionconfig.GetKDF() != true { t.Fatalf("without updating the feature flags, default value of KMSv2KDF should be enabled.") } if utilfeature.DefaultFeatureGate.Enabled(features.KMSv2) != true { t.Fatalf("without updating the feature flags, default value of KMSv2 should be enabled.") } if utilfeature.DefaultFeatureGate.Enabled(features.KMSv1) != false { t.Fatalf("without updating the feature flags, default value of KMSv1 should be disabled.") } // since encryptionconfig.GetKDF() is true by default, following test should verify if // object.EncryptedDEKSourceType == kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED ctx, cancel := context.WithTimeout(context.Background(), time.Minute) t.Cleanup(cancel) encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - pods providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock") test, err := newTransformTest(t, encryptionConfig, false, "", nil) if err != nil { t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } t.Cleanup(test.cleanUp) client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig) if _, err := client.CoreV1().Pods(testNamespace).Create(ctx, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, metav1.CreateOptions{}); err != nil { t.Fatal(err) } config := test.kubeAPIServer.ServerOpts.Etcd.StorageConfig rawClient, etcdClient, err := integration.GetEtcdClients(config.Transport) if err != nil { t.Fatalf("failed to create etcd client: %v", err) } t.Cleanup(func() { _ = rawClient.Close() }) response, err := etcdClient.Get(ctx, "/"+config.Prefix+"/pods/"+testNamespace+"/", clientv3.WithPrefix()) if err != nil { t.Fatal(err) } if len(response.Kvs) != 1 { t.Fatalf("expected 1 KVs, but got %d", len(response.Kvs)) } object := kmstypes.EncryptedObject{} v := bytes.TrimPrefix(response.Kvs[0].Value, []byte("k8s:enc:kms:v2:kms-provider:")) if err := proto.Unmarshal(v, &object); err != nil { t.Fatal(err) } if object.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED { t.Errorf("invalid type: %d", object.EncryptedDEKSourceType) } } // TestKMSv2Provider is an integration test between KubeAPI, ETCD and KMSv2 Plugin // Concretely, this test verifies the following integration contracts: // 1. Raw records in ETCD that were processed by KMSv2 Provider should be prefixed with k8s:enc:kms:v2:: // 2. Data Encryption Key (DEK) / DEK seed should be generated by envelopeTransformer and passed to KMS gRPC Plugin // 3. KMS gRPC Plugin should encrypt the DEK/seed with a Key Encryption Key (KEK) and pass it back to envelopeTransformer // 4. The cipherTextPayload (ex. Secret) should be encrypted via AES GCM transform / extended nonce GCM // 5. kmstypes.EncryptedObject structure should be serialized and deposited in ETCD func TestKMSv2Provider(t *testing.T) { defaultUseSeed := encryptionconfig.GetKDF() t.Run("regular gcm", func(t *testing.T) { defer encryptionconfig.SetKDFForTests(false)() testKMSv2Provider(t, !defaultUseSeed) }) t.Run("extended nonce gcm", func(t *testing.T) { defer encryptionconfig.SetKDFForTests(true)() testKMSv2Provider(t, defaultUseSeed) }) } func testKMSv2Provider(t *testing.T, useSeed bool) { encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - secrets providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` genericapiserver.SetHostnameFuncForTests("testAPIServerID") providerName := "kms-provider" pluginMock := kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock") test, err := newTransformTest(t, encryptionConfig, false, "", nil) if err != nil { t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } defer test.cleanUp() ctx := testContext(t) // the global metrics registry persists across test runs - reset it here so we can make assertions copyConfig := rest.CopyConfig(test.kubeAPIServer.ClientConfig) copyConfig.GroupVersion = &schema.GroupVersion{} copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer() rc, err := rest.RESTClientFor(copyConfig) if err != nil { t.Fatal(err) } if err := rc.Delete().AbsPath("/metrics").Do(ctx).Error(); err != nil { t.Fatal(err) } // assert that the metrics we collect during the test run match expectations wantMetricStrings := []string{ `apiserver_envelope_encryption_dek_source_cache_size{provider_name="kms-provider"} 1`, `apiserver_envelope_encryption_key_id_hash_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",key_id_hash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b",provider_name="kms-provider",transformation_type="from_storage"} FP`, `apiserver_envelope_encryption_key_id_hash_last_timestamp_seconds{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",key_id_hash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b",provider_name="kms-provider",transformation_type="to_storage"} FP`, `apiserver_envelope_encryption_key_id_hash_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",key_id_hash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b",provider_name="kms-provider",transformation_type="from_storage"} 2`, `apiserver_envelope_encryption_key_id_hash_total{apiserver_id_hash="sha256:3c607df3b2bf22c9d9f01d5314b4bbf411c48ef43ff44ff29b1d55b41367c795",key_id_hash="sha256:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b",provider_name="kms-provider",transformation_type="to_storage"} 1`, } defer func() { body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx) if err != nil { t.Fatal(err) } var gotMetricStrings []string trimFP := regexp.MustCompile(`(.*)(} \d+\.\d+.*)`) for _, line := range strings.Split(string(body), "\n") { if strings.HasPrefix(line, "apiserver_envelope_") { if strings.HasPrefix(line, "apiserver_envelope_encryption_dek_cache_fill_percent") { continue // this can be ignored as it is KMS v1 only } if strings.Contains(line, "_seconds") { line = trimFP.ReplaceAllString(line, `$1`) + "} FP" // ignore floating point metric values } gotMetricStrings = append(gotMetricStrings, line) } } if diff := cmp.Diff(wantMetricStrings, gotMetricStrings); diff != "" { t.Errorf("unexpected metrics diff (-want +got): %s", diff) } }() test.secret, err = test.createSecret(testSecret, testNamespace) if err != nil { t.Fatalf("Failed to create test secret, error: %v", err) } plainTextDEKSource := pluginMock.LastEncryptRequest() secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace) rawEnvelope, err := test.getRawSecretFromETCD() if err != nil { t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) } envelopeData := envelopekmsv2{ providerName: providerName, rawEnvelope: rawEnvelope, plainTextDEKSource: plainTextDEKSource, useSeed: useSeed, } wantPrefix := envelopeData.prefix() if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefix)) { t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope) } ciphertext, err := envelopeData.cipherTextDEKSource() if err != nil { t.Fatalf("failed to get ciphertext DEK/seed from KMSv2 Plugin: %v", err) } decryptResponse, err := pluginMock.Decrypt(ctx, &kmsv2api.DecryptRequest{Uid: string(uuid.NewUUID()), Ciphertext: ciphertext}) if err != nil { t.Fatalf("failed to decrypt DEK, %v", err) } dekSourcePlainAsWouldBeSeenByETCD := decryptResponse.Plaintext if !bytes.Equal(plainTextDEKSource, dekSourcePlainAsWouldBeSeenByETCD) { t.Fatalf("expected plainTextDEKSource %v to be passed to KMS Plugin, but got %s", plainTextDEKSource, dekSourcePlainAsWouldBeSeenByETCD) } plainSecret, err := envelopeData.plainTextPayload(secretETCDPath) if err != nil { t.Fatalf("failed to transform from storage via AESGCM, err: %v", err) } if !strings.Contains(string(plainSecret), secretVal) { t.Fatalf("expected %q after decryption, but got %q", secretVal, string(plainSecret)) } secretClient := test.restClient.CoreV1().Secrets(testNamespace) // Secrets should be un-enveloped on direct reads from Kube API Server. s, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{}) if err != nil { t.Fatalf("failed to get Secret from %s, err: %v", testNamespace, err) } if secretVal != string(s.Data[secretKey]) { t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey])) } } // TestKMSv2ProviderKeyIDStaleness is an integration test between KubeAPI and KMSv2 Plugin // Concretely, this test verifies the following contracts for no-op updates: // 1. When the key ID is unchanged, the resource version must not change // 2. When the key ID changes, the resource version changes (but only once) // 3. For all subsequent updates, the resource version must not change // 4. When kms-plugin is down, expect creation of new pod and encryption to succeed while the DEK/seed is valid // 5. when kms-plugin is down, no-op update for a pod should succeed and not result in RV change while the DEK/seed is valid // 6. When kms-plugin is down, expect creation of new pod and encryption to fail once the DEK/seed is invalid // 7. when kms-plugin is down, no-op update for a pod should succeed and not result in RV change even once the DEK/seed is valid func TestKMSv2ProviderKeyIDStaleness(t *testing.T) { t.Run("regular gcm", func(t *testing.T) { defer encryptionconfig.SetKDFForTests(false)() testKMSv2ProviderKeyIDStaleness(t) }) t.Run("extended nonce gcm", func(t *testing.T) { testKMSv2ProviderKeyIDStaleness(t) }) } func testKMSv2ProviderKeyIDStaleness(t *testing.T) { encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - pods - deployments.apps providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` pluginMock := kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock") test, err := newTransformTest(t, encryptionConfig, false, "", nil) if err != nil { t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } defer test.cleanUp() dynamicClient := dynamic.NewForConfigOrDie(test.kubeAPIServer.ClientConfig) testPod, err := test.createPod(testNamespace, dynamicClient) if err != nil { t.Fatalf("Failed to create test pod, error: %v, ns: %s", err, testNamespace) } version1 := testPod.GetResourceVersion() // 1. no-op update for the test pod should not result in any RV change updatedPod, err := test.inplaceUpdatePod(testNamespace, testPod, dynamicClient) if err != nil { t.Fatalf("Failed to update test pod, error: %v, ns: %s", err, testNamespace) } version2 := updatedPod.GetResourceVersion() if version1 != version2 { t.Fatalf("Resource version should not have changed. old pod: %v, new pod: %v", testPod, updatedPod) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) useSeed := encryptionconfig.GetKDF() var firstEncryptedDEKSource []byte var f checkFunc if useSeed { f = func(_ int, _ uint64, etcdKey string, obj kmstypes.EncryptedObject) { firstEncryptedDEKSource = obj.EncryptedDEKSource if obj.KeyID != "1" { t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID) } } } else { f = func(_ int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject) { firstEncryptedDEKSource = obj.EncryptedDEKSource if obj.KeyID != "1" { t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID) } // with the first key we perform encryption during the following steps: // - create const want = 1_000_000_000 + 1 // zero value of counter is one billion if want != counter { t.Errorf("key %s: counter nonce is invalid: want %d, got %d", etcdKey, want, counter) } } } assertPodDEKSources(ctx, t, test.kubeAPIServer.ServerOpts.Etcd.StorageConfig, 1, 1, "k8s:enc:kms:v2:kms-provider:", f, ) if len(firstEncryptedDEKSource) == 0 { t.Fatal("unexpected empty DEK or seed") } // 2. no-op update for the test pod with keyID update should result in RV change pluginMock.UpdateKeyID() if err := kmsv2mock.WaitForBase64PluginToBeUpdated(pluginMock); err != nil { t.Fatalf("Failed to update keyID for plugin, err: %v", err) } // Wait 1 sec (poll interval to check resource version) until a resource version change is detected or timeout at 1 minute. version3 := "" err = wait.Poll(time.Second, time.Minute, func() (bool, error) { t.Log("polling for in-place update rv change") updatedPod, err = test.inplaceUpdatePod(testNamespace, updatedPod, dynamicClient) if err != nil { return false, err } version3 = updatedPod.GetResourceVersion() if version1 != version3 { return true, nil } return false, nil }) if err != nil { t.Fatalf("Failed to detect one resource version update within the allotted time after keyID is updated and pod has been inplace updated, err: %v, ns: %s", err, testNamespace) } if version1 == version3 { t.Fatalf("Resource version should have changed after keyID update. old pod: %v, new pod: %v", testPod, updatedPod) } var wantCount uint64 = 1_000_000_000 // zero value of counter is one billion wantCount++ // in place update with RV change // with the second key we perform encryption during the following steps: // - in place update with RV change // - delete (which does an update to set deletion timestamp) // - create var checkDEK checkFunc if useSeed { checkDEK = func(_ int, _ uint64, etcdKey string, obj kmstypes.EncryptedObject) { if len(obj.EncryptedDEKSource) == 0 { t.Error("unexpected empty DEK source") } if bytes.Equal(obj.EncryptedDEKSource, firstEncryptedDEKSource) { t.Errorf("key %s: incorrectly has the same ESEED", etcdKey) } if obj.KeyID != "2" { t.Errorf("key %s: want key ID %s, got %s", etcdKey, "2", obj.KeyID) } } } else { checkDEK = func(_ int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject) { if len(obj.EncryptedDEKSource) == 0 { t.Error("unexpected empty DEK source") } if bytes.Equal(obj.EncryptedDEKSource, firstEncryptedDEKSource) { t.Errorf("key %s: incorrectly has the same EDEK", etcdKey) } if obj.KeyID != "2" { t.Errorf("key %s: want key ID %s, got %s", etcdKey, "2", obj.KeyID) } if wantCount != counter { t.Errorf("key %s: counter nonce is invalid: want %d, got %d", etcdKey, wantCount, counter) } } } // 3. no-op update for the updated pod should not result in RV change updatedPod, err = test.inplaceUpdatePod(testNamespace, updatedPod, dynamicClient) if err != nil { t.Fatalf("Failed to update test pod, error: %v, ns: %s", err, testNamespace) } version4 := updatedPod.GetResourceVersion() if version3 != version4 { t.Fatalf("Resource version should not have changed again after the initial version updated as a result of the keyID update. old pod: %v, new pod: %v", testPod, updatedPod) } // delete the pod so that it can be recreated if err := test.deletePod(testNamespace, dynamicClient); err != nil { t.Fatalf("failed to delete test pod: %v", err) } wantCount++ // we cannot assert against the counter being 2 since the pod gets deleted // 4. when kms-plugin is down, expect creation of new pod and encryption to succeed because the DEK is still valid pluginMock.EnterFailedState() mustBeUnHealthy(t, "/kms-providers", "internal server error: kms-provider-0: rpc error: code = FailedPrecondition desc = failed precondition - key disabled", test.kubeAPIServer.ClientConfig) newPod, err := test.createPod(testNamespace, dynamicClient) if err != nil { t.Fatalf("Create test pod should have succeeded due to valid DEK, ns: %s, got: %v", testNamespace, err) } wantCount++ version5 := newPod.GetResourceVersion() // 5. when kms-plugin is down and DEK is valid, no-op update for a pod should succeed and not result in RV change updatedPod, err = test.inplaceUpdatePod(testNamespace, newPod, dynamicClient) if err != nil { t.Fatalf("Failed to perform no-op update on pod when kms-plugin is down, error: %v, ns: %s", err, testNamespace) } version6 := updatedPod.GetResourceVersion() if version5 != version6 { t.Fatalf("Resource version should not have changed again after the initial version updated as a result of the keyID update. old pod: %v, new pod: %v", newPod, updatedPod) } // Invalidate the DEK by moving the current time forward origNowFunc := kmsv2.NowFunc t.Cleanup(func() { kmsv2.NowFunc = origNowFunc }) kmsv2.NowFunc = func() time.Time { return origNowFunc().Add(5 * time.Minute) } // 6. when kms-plugin is down, expect creation of new pod and encryption to fail because the DEK is invalid _, err = test.createPod(testNamespace, dynamicClient) if err == nil || !strings.Contains(err.Error(), `encryptedDEKSource with keyID hash "sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35" expired at 2`) { t.Fatalf("Create test pod should have failed due to encryption, ns: %s, got: %v", testNamespace, err) } // 7. when kms-plugin is down and DEK is invalid, no-op update for a pod should succeed and not result in RV change updatedNewPod, err := test.inplaceUpdatePod(testNamespace, newPod, dynamicClient) if err != nil { t.Fatalf("Failed to perform no-op update on pod when kms-plugin is down, error: %v, ns: %s", err, testNamespace) } version7 := updatedNewPod.GetResourceVersion() if version5 != version7 { t.Fatalf("Resource version should not have changed again after the initial version updated as a result of the keyID update. old pod: %v, new pod: %v", newPod, updatedNewPod) } assertPodDEKSources(ctx, t, test.kubeAPIServer.ServerOpts.Etcd.StorageConfig, 1, 1, "k8s:enc:kms:v2:kms-provider:", checkDEK, ) // fix plugin and wait for new writes to start working again kmsv2.NowFunc = origNowFunc pluginMock.ExitFailedState() err = wait.Poll(time.Second, 3*time.Minute, func() (bool, error) { t.Log("polling for plugin to be functional") _, err = test.createDeployment("panda", testNamespace) if err != nil { t.Logf("failed to create deployment, plugin is likely still unhealthy: %v", err) } return err == nil, nil }) if err != nil { t.Fatalf("failed to restore plugin health, err: %v, ns: %s", err, testNamespace) } // 8. confirm that no-op update for a pod succeeds and still does not result in RV change updatedNewPod2, err := test.inplaceUpdatePod(testNamespace, updatedNewPod, dynamicClient) if err != nil { t.Fatalf("Failed to perform no-op update on pod when kms-plugin is up, error: %v, ns: %s", err, testNamespace) } version8 := updatedNewPod2.GetResourceVersion() if version7 != version8 { t.Fatalf("Resource version should not have changed after plugin health is restored. old pod: %v, new pod: %v", updatedNewPod, updatedNewPod2) } // flip the current config defer encryptionconfig.SetKDFForTests(!useSeed)() // 9. confirm that no-op update for a pod results in RV change due to KDF config change var version9 string err = wait.Poll(time.Second, 3*time.Minute, func() (bool, error) { t.Log("polling for in-place update rv change due to KDF config change") updatedNewPod2, err = test.inplaceUpdatePod(testNamespace, updatedNewPod2, dynamicClient) if err != nil { return false, err } version9 = updatedNewPod2.GetResourceVersion() if version8 != version9 { return true, nil } return false, nil }) if err != nil { t.Fatalf("Failed to detect one resource version update within the allotted time after KDF config change and pod has been inplace updated, err: %v, ns: %s", err, testNamespace) } } func TestKMSv2ProviderDEKSourceReuse(t *testing.T) { t.Run("regular gcm", func(t *testing.T) { defer encryptionconfig.SetKDFForTests(false)() testKMSv2ProviderDEKSourceReuse(t, func(i int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject) { if obj.KeyID != "1" { t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID) } // zero value of counter is one billion so the first value will be one billion plus one // hence we add that to our zero based index to calculate the expected nonce if uint64(i+1_000_000_000+1) != counter { t.Errorf("key %s: counter nonce is invalid: want %d, got %d", etcdKey, i+1, counter) } }, ) }) t.Run("extended nonce gcm", func(t *testing.T) { defer encryptionconfig.SetKDFForTests(true)() testKMSv2ProviderDEKSourceReuse(t, func(_ int, _ uint64, etcdKey string, obj kmstypes.EncryptedObject) { if obj.KeyID != "1" { t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID) } }, ) }) } func testKMSv2ProviderDEKSourceReuse(t *testing.T, f checkFunc) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) t.Cleanup(cancel) encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - pods providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock") test, err := newTransformTest(t, encryptionConfig, false, "", nil) if err != nil { t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } t.Cleanup(test.cleanUp) client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig) const podCount = 1_000 for i := 0; i < podCount; i++ { if _, err := client.CoreV1().Pods(testNamespace).Create(ctx, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("dek-reuse-%04d", i+1), // making creation order match returned list order / nonce counter }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, metav1.CreateOptions{}); err != nil { t.Fatal(err) } } assertPodDEKSources(ctx, t, test.kubeAPIServer.ServerOpts.Etcd.StorageConfig, podCount, 1, // key ID does not change during the test so we should only have a single DEK "k8s:enc:kms:v2:kms-provider:", f, ) } type checkFunc func(i int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject) func assertPodDEKSources(ctx context.Context, t *testing.T, config storagebackend.Config, podCount, dekSourcesCount int, kmsPrefix string, f checkFunc) { t.Helper() rawClient, etcdClient, err := integration.GetEtcdClients(config.Transport) if err != nil { t.Fatalf("failed to create etcd client: %v", err) } t.Cleanup(func() { _ = rawClient.Close() }) response, err := etcdClient.Get(ctx, "/"+config.Prefix+"/pods/"+testNamespace+"/", clientv3.WithPrefix()) if err != nil { t.Fatal(err) } if len(response.Kvs) != podCount { t.Fatalf("expected %d KVs, but got %d", podCount, len(response.Kvs)) } useSeed := encryptionconfig.GetKDF() out := make([]kmstypes.EncryptedObject, len(response.Kvs)) for i, kv := range response.Kvs { v := bytes.TrimPrefix(kv.Value, []byte(kmsPrefix)) if err := proto.Unmarshal(v, &out[i]); err != nil { t.Fatal(err) } if err := kmsv2.ValidateEncryptedObject(&out[i]); err != nil { t.Fatal(err) } var infoLen int if useSeed { infoLen = 32 } info := out[i].EncryptedData[:infoLen] nonce := out[i].EncryptedData[infoLen : 12+infoLen] randN := nonce[:4] count := nonce[4:] if bytes.Equal(randN, make([]byte, len(randN))) { t.Errorf("key %s: got all zeros for first four bytes", string(kv.Key)) } if useSeed { if bytes.Equal(info, make([]byte, infoLen)) { t.Errorf("key %s: got all zeros for info", string(kv.Key)) } } counter := binary.LittleEndian.Uint64(count) f(i, counter, string(kv.Key), out[i]) } uniqueDEKSources := sets.NewString() for _, object := range out { object := object uniqueDEKSources.Insert(string(object.EncryptedDEKSource)) if useSeed { if object.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED { t.Errorf("invalid type: %d", object.EncryptedDEKSourceType) } } else { if object.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_AES_GCM_KEY { t.Errorf("invalid type: %d", object.EncryptedDEKSourceType) } } } if uniqueDEKSources.Has("") { t.Error("unexpected empty DEK source seen") } if uniqueDEKSources.Len() != dekSourcesCount { t.Errorf("expected %d DEK sources, got: %d", dekSourcesCount, uniqueDEKSources.Len()) } } func TestKMSv2Healthz(t *testing.T) { defer encryptionconfig.SetKDFForTests(randomBool())() encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - secrets providers: - kms: apiVersion: v2 name: provider-1 endpoint: unix:///@kms-provider-1.sock - kms: apiVersion: v2 name: provider-2 endpoint: unix:///@kms-provider-2.sock ` pluginMock1 := kmsv2mock.NewBase64Plugin(t, "@kms-provider-1.sock") pluginMock2 := kmsv2mock.NewBase64Plugin(t, "@kms-provider-2.sock") test, err := newTransformTest(t, encryptionConfig, false, "", nil) if err != nil { t.Fatalf("Failed to start kube-apiserver, error: %v", err) } defer test.cleanUp() // Name of the healthz check is always "kms-provider-0" and it covers all kms plugins. // Stage 1 - Since all kms-plugins are guaranteed to be up, // the healthz check should be OK. mustBeHealthy(t, "/kms-providers", "ok", test.kubeAPIServer.ClientConfig) // Stage 2 - kms-plugin for provider-1 is down. Therefore, expect the healthz check // to fail and report that provider-1 is down pluginMock1.EnterFailedState() mustBeUnHealthy(t, "/kms-providers", "internal server error: kms-provider-0: rpc error: code = FailedPrecondition desc = failed precondition - key disabled", test.kubeAPIServer.ClientConfig) pluginMock1.ExitFailedState() // Stage 3 - kms-plugin for provider-1 is now up. Therefore, expect the health check for provider-1 // to succeed now, but provider-2 is now down. pluginMock2.EnterFailedState() mustBeUnHealthy(t, "/kms-providers", "internal server error: kms-provider-1: rpc error: code = FailedPrecondition desc = failed precondition - key disabled", test.kubeAPIServer.ClientConfig) pluginMock2.ExitFailedState() // Stage 4 - All kms-plugins are once again up, // the healthz check should be OK. mustBeHealthy(t, "/kms-providers", "ok", test.kubeAPIServer.ClientConfig) // Stage 5 - All kms-plugins are unhealthy at the same time and we can observe both failures. pluginMock1.EnterFailedState() pluginMock2.EnterFailedState() mustBeUnHealthy(t, "/kms-providers", "internal server error: "+ "[kms-provider-0: failed to perform status section of the healthz check for KMS Provider provider-1, error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled,"+ " kms-provider-1: failed to perform status section of the healthz check for KMS Provider provider-2, error: rpc error: code = FailedPrecondition desc = failed precondition - key disabled]", test.kubeAPIServer.ClientConfig) } func TestKMSv2SingleService(t *testing.T) { defer encryptionconfig.SetKDFForTests(randomBool())() var kmsv2Calls int origEnvelopeKMSv2ServiceFactory := encryptionconfig.EnvelopeKMSv2ServiceFactory encryptionconfig.EnvelopeKMSv2ServiceFactory = func(ctx context.Context, endpoint, providerName string, callTimeout time.Duration) (kmsv2svc.Service, error) { kmsv2Calls++ return origEnvelopeKMSv2ServiceFactory(ctx, endpoint, providerName, callTimeout) } t.Cleanup(func() { encryptionconfig.EnvelopeKMSv2ServiceFactory = origEnvelopeKMSv2ServiceFactory }) // check resources provided by the three servers that we have wired together // - pods and config maps from KAS // - CRDs and CRs from API extensions // - API services from aggregator encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - pods - configmaps - customresourcedefinitions.apiextensions.k8s.io - pandas.awesome.bears.com - apiservices.apiregistration.k8s.io providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock") test, err := newTransformTest(t, encryptionConfig, false, "", nil) if err != nil { t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } t.Cleanup(test.cleanUp) // the storage registry for CRs is dynamic so create one to exercise the wiring etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(test.kubeAPIServer.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) t.Cleanup(cancel) gvr := schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v1", Resource: "pandas"} stub := etcd.GetEtcdStorageData()[gvr].Stub dynamicClient, obj, err := etcd.JSONToUnstructured(stub, "", &meta.RESTMapping{ Resource: gvr, GroupVersionKind: gvr.GroupVersion().WithKind("Panda"), Scope: meta.RESTScopeRoot, }, dynamic.NewForConfigOrDie(test.kubeAPIServer.ClientConfig)) if err != nil { t.Fatal(err) } _, err = dynamicClient.Create(ctx, obj, metav1.CreateOptions{}) if err != nil { t.Fatal(err) } if kmsv2Calls != 1 { t.Fatalf("expected a single call to KMS v2 service factory: %v", kmsv2Calls) } } // TestKMSv2FeatureFlag is an integration test between KubeAPI and ETCD // Concretely, this test verifies the following: // 1. When feature flag is enabled, loading a encryptionConfig with KMSv2 should work // 2. After a restart, loading a encryptionConfig with the same KMSv2 plugin from 1 should work, // decryption of data encrypted with v2 should work func TestKMSv2FeatureFlag(t *testing.T) { encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - secrets providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` providerName := "kms-provider" pluginMock := kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock") storageConfig := framework.SharedEtcd() // KMSv2 is enabled by default. Loading a encryptionConfig with KMSv2 should work test, err := newTransformTest(t, encryptionConfig, false, "", storageConfig) if err != nil { t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } defer func() { test.cleanUp() }() test.secret, err = test.createSecret(testSecret, testNamespace) if err != nil { t.Fatalf("Failed to create test secret, error: %v", err) } // Since Data Encryption Key (DEK) is randomly generated, we need to ask KMS Mock for it. plainTextDEKSource := pluginMock.LastEncryptRequest() secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace) rawEnvelope, err := test.getRawSecretFromETCD() if err != nil { t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err) } envelopeData := envelopekmsv2{ providerName: providerName, rawEnvelope: rawEnvelope, plainTextDEKSource: plainTextDEKSource, useSeed: true, // expect KMSv2KDF to be enabled by default for this test } wantPrefix := envelopeData.prefix() if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefix)) { t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope) } ctx := testContext(t) ciphertext, err := envelopeData.cipherTextDEKSource() if err != nil { t.Fatalf("failed to get ciphertext DEK from KMSv2 Plugin: %v", err) } decryptResponse, err := pluginMock.Decrypt(ctx, &kmsv2api.DecryptRequest{Uid: string(uuid.NewUUID()), Ciphertext: ciphertext}) if err != nil { t.Fatalf("failed to decrypt DEK, %v", err) } dekPlainAsWouldBeSeenByETCD := decryptResponse.Plaintext if !bytes.Equal(plainTextDEKSource, dekPlainAsWouldBeSeenByETCD) { t.Fatalf("expected plainTextDEKSource %v to be passed to KMS Plugin, but got %s", plainTextDEKSource, dekPlainAsWouldBeSeenByETCD) } plainSecret, err := envelopeData.plainTextPayload(secretETCDPath) if err != nil { t.Fatalf("failed to transform from storage via AESGCM, err: %v", err) } if !strings.Contains(string(plainSecret), secretVal) { t.Fatalf("expected %q after decryption, but got %q", secretVal, string(plainSecret)) } secretClient := test.restClient.CoreV1().Secrets(testNamespace) // Secrets should be un-enveloped on direct reads from Kube API Server. s, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{}) if err != nil { t.Fatalf("failed to get Secret from %s, err: %v", testNamespace, err) } if secretVal != string(s.Data[secretKey]) { t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey])) } test.shutdownAPIServer() // After a restart, loading a encryptionConfig with the same KMSv2 plugin before the restart should work, decryption of data encrypted with v2 should work test, err = newTransformTest(t, encryptionConfig, false, "", storageConfig) if err != nil { t.Fatalf("Failed to restart api server, error: %v", err) } // Getting an old secret that was encrypted by the same plugin should not fail. s, err = test.restClient.CoreV1().Secrets(testNamespace).Get( ctx, testSecret, metav1.GetOptions{}, ) if err != nil { t.Fatalf("failed to read secret, err: %v", err) } if secretVal != string(s.Data[secretKey]) { t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey])) } } var benchSecret *api.Secret func BenchmarkKMSv2KDF(b *testing.B) { b.StopTimer() klog.SetOutput(io.Discard) klog.LogToStderr(false) defer encryptionconfig.SetKDFForTests(false)() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) b.Cleanup(cancel) ctx = request.WithNamespace(ctx, testNamespace) encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - secrets providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` _ = kmsv2mock.NewBase64Plugin(b, "@kms-provider.sock") test, err := newTransformTest(b, encryptionConfig, false, "", nil) if err != nil { b.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } b.Cleanup(test.cleanUp) client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig) restOptionsGetter := getRESTOptionsGetterForSecrets(b, test) secretStorage, err := secretstore.NewREST(restOptionsGetter) if err != nil { b.Fatal(err) } b.Cleanup(secretStorage.Destroy) const dataLen = 1_000 secrets := make([]*api.Secret, dataLen) for i := 0; i < dataLen; i++ { secrets[i] = &api.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("test-secret-%d", i), Namespace: testNamespace, }, Data: map[string][]byte{ "lots_of_data": bytes.Repeat([]byte{1, 3, 3, 7}, i*dataLen/4), }, } } b.StartTimer() for i := 0; i < b.N; i++ { _, err := secretStorage.DeleteCollection(ctx, noValidation, &metav1.DeleteOptions{}, nil) if err != nil { b.Fatal(err) } for i := 0; i < dataLen; i++ { out, err := secretStorage.Create(ctx, secrets[i], noValidation, &metav1.CreateOptions{}) if err != nil { b.Fatal(err) } benchSecret = out.(*api.Secret) out, err = secretStorage.Get(ctx, benchSecret.Name, &metav1.GetOptions{}) if err != nil { b.Fatal(err) } benchSecret = out.(*api.Secret) } } b.StopTimer() secretList, err := client.CoreV1().Secrets(testNamespace).List(ctx, metav1.ListOptions{}) if err != nil { b.Fatal(err) } if secretLen := len(secretList.Items); secretLen != dataLen { b.Errorf("unexpected secret len: want %d, got %d", dataLen, secretLen) } } func getRESTOptionsGetterForSecrets(t testing.TB, test *transformTest) generic.RESTOptionsGetter { t.Helper() s := test.kubeAPIServer.ServerOpts etcdConfigCopy := *s.Etcd etcdConfigCopy.SkipHealthEndpoints = true // avoid running health check go routines etcdConfigCopy.EncryptionProviderConfigAutomaticReload = true // hack to use DynamicTransformers in t.Cleanup below // mostly copied from BuildGenericConfig genericConfig := genericapiserver.NewConfig(legacyscheme.Codecs) genericConfig.MergedResourceConfig = controlplane.DefaultAPIResourceConfigSource() if err := s.APIEnablement.ApplyTo(genericConfig, controlplane.DefaultAPIResourceConfigSource(), legacyscheme.Scheme); err != nil { t.Fatal(err) } storageFactoryConfig := kubeapiserver.NewStorageFactoryConfig() storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig storageFactory, err := storageFactoryConfig.Complete(&etcdConfigCopy).New() if err != nil { t.Fatal(err) } if err := etcdConfigCopy.ApplyWithStorageFactoryTo(storageFactory, genericConfig); err != nil { t.Fatal(err) } transformers, ok := genericConfig.ResourceTransformers.(*encryptionconfig.DynamicTransformers) if !ok { t.Fatalf("incorrect type for ResourceTransformers: %T", genericConfig.ResourceTransformers) } t.Cleanup(func() { // this is a hack to cause the existing transformers to shutdown transformers.Set(nil, nil, nil, 0) time.Sleep(10 * time.Second) // block this cleanup for longer than kmsCloseGracePeriod }) if genericConfig.RESTOptionsGetter == nil { t.Fatal("not REST options found") } opts, err := genericConfig.RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: "", Resource: "secrets"}) if err != nil { t.Fatal(err) } if err := runtime.CheckCodec(opts.StorageConfig.Codec, &api.Secret{}, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}); err != nil { t.Fatal(err) } return genericConfig.RESTOptionsGetter } func noValidation(_ context.Context, _ runtime.Object) error { return nil } var benchRESTSecret *corev1.Secret func BenchmarkKMSv2REST(b *testing.B) { b.StopTimer() klog.SetOutput(io.Discard) klog.LogToStderr(false) defer encryptionconfig.SetKDFForTests(true)() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) b.Cleanup(cancel) encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - secrets providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` _ = kmsv2mock.NewBase64Plugin(b, "@kms-provider.sock") test, err := newTransformTest(b, encryptionConfig, false, "", nil) if err != nil { b.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } b.Cleanup(test.cleanUp) client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig) const dataLen = 1_000 secretStorage := client.CoreV1().Secrets(testNamespace) secrets := make([]*corev1.Secret, dataLen) for i := 0; i < dataLen; i++ { secrets[i] = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("test-secret-%d", i), Namespace: testNamespace, }, Data: map[string][]byte{ "lots_of_data": bytes.Repeat([]byte{1, 3, 3, 7}, i*dataLen/4), }, } } b.StartTimer() for i := 0; i < b.N; i++ { err := secretStorage.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{}) if err != nil { b.Fatal(err) } for i := 0; i < dataLen; i++ { out, err := secretStorage.Create(ctx, secrets[i], metav1.CreateOptions{}) if err != nil { b.Fatal(err) } benchRESTSecret = out out, err = secretStorage.Get(ctx, benchRESTSecret.Name, metav1.GetOptions{}) if err != nil { b.Fatal(err) } benchRESTSecret = out } } b.StopTimer() secretList, err := client.CoreV1().Secrets(testNamespace).List(ctx, metav1.ListOptions{}) if err != nil { b.Fatal(err) } if secretLen := len(secretList.Items); secretLen != dataLen { b.Errorf("unexpected secret len: want %d, got %d", dataLen, secretLen) } } func randomBool() bool { return utilrand.Int()%2 == 1 } // TestKMSv2ProviderLegacyData confirms that legacy data recorded from the earliest released commit can still be read. func TestKMSv2ProviderLegacyData(t *testing.T) { t.Run("regular gcm", func(t *testing.T) { defer encryptionconfig.SetKDFForTests(false)() testKMSv2ProviderLegacyData(t) }) t.Run("extended nonce gcm", func(t *testing.T) { defer encryptionconfig.SetKDFForTests(true)() testKMSv2ProviderLegacyData(t) }) } func testKMSv2ProviderLegacyData(t *testing.T) { encryptionConfig := ` kind: EncryptionConfiguration apiVersion: apiserver.config.k8s.io/v1 resources: - resources: - secrets providers: - kms: apiVersion: v2 name: kms-provider endpoint: unix:///@kms-provider.sock ` _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock") // the value.Context.AuthenticatedData during read is the etcd storage path of the associated resource // thus we need to manually construct the storage config so that we can have a static path const legacyDataEtcdPrefix = "43da1478-5e9c-4ef3-a92a-2b19d540c8a5" storageConfig := storagebackend.NewDefaultConfig(path.Join(legacyDataEtcdPrefix, "registry"), nil) storageConfig.Transport.ServerList = []string{framework.GetEtcdURL()} test, err := newTransformTest(t, encryptionConfig, false, "", storageConfig) if err != nil { t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err) } defer test.cleanUp() const legacyDataNamespace = "kms-v2-legacy-data" if _, err := test.createNamespace(legacyDataNamespace); err != nil { t.Fatal(err) } // commit: 1b4df30b3cdfeaba6024e81e559a6cd09a089d65 // tag: v1.27.0 const dekSourceAESGCMKeyName = "dek-source-aes-gcm-key" const dekSourceAESGCMKeyData = "k8s:enc:kms:v2:kms-provider:\n\x8a\x03\xb0ƙ\xdc\x01ʚ;\x00\x00\x00\x00\x8c\x80\v\xf0\x1dKo{0\v\xb5\xd5Ń\x1a\xb5\x0e\xcf\xd7Ve\xed邸\xdfE\xedMk\xcf!\x15\xc0\v\t\x82Wh \x9e\x8f\x1b\\9\b\xa4\x80\x02m\xf4P\x14z\xee\xf7\x8c\x1a\x84n5\xfdG\x83v#\x0e\xd4\f\x83YwH\xe1\x1c\xbf\x12\xc6\x1b\xba\x8br\b\x82z\xf8\xdb`\xa7]P\xb1\xe6!Lb\x8d\xb8I\x1aEL\xa0\xae+\xbe\x15R\x8e\x9b\x064\xf6P\xb6;\x9f\xa6\x8d\x96\xb2\x01\xa1\x8e\xe4a\xdf/\x90u\xde6\x9a\xc2ͻb\x88+\x16\x98=\xe9\x03\xdd\xd7HvC\n\xe5\x8cv\x05~\n\xabX_N\x9a\x84wp\xa8\x13\x0f\x82Y9x\xed\x89\x15\xb9\xe1ꦐ\xc6`R\n0\x04\xf2\xa6ѥ\x85\nk\xf4\xcf\xe4ul\x1c*[A\x12\xa0\xd9\xf2\xb5!\x82\xe4\x00\xa4L'&\xf5pln\"\xe0=\f[\xe1\xb0U97\x11|\xdaNk\xc3=\xc2\xf2\x85<7\x1e\x01\xb8\xa9\xf4\x89\xdb~\xe1\x8c\xe1\x1f\x05B@WʼS\n聛LY\x86$\xf6\x01ݝ\xcd\x1d\xe9\x02]\xf4i\xda\xfa\xc2\x0eUr\xc5Dʽdb\x13\xbe\xfe\x1c\xc5\xe1\x84\xcc\xdf&\x93j\x1eK\x04\xba\x06\x16\x85\x0e\x1f\xca\b\x90\x06\x11K\x9d[\rV\xe8E\xd5(\x91\fn\xd4\x10\x9cH\x1cܝX\x94ȁ5\x1b\x8c\xbbz\xf9Ho{\x1d\x112\x90F\xe7\xd8h\xa8\xa1\xf6\x8c\x8cvʲ1\xf9#\x82\xa3\xbe7ed\xd9\x14\xf3\x06\xff\xb7߫i\x12\x011\x1a,gafIvwT0ASoKdZ/D1L2SlH73LUMj5qa3hroljfS51wc=\"2\n\x1blocal-kek.kms.kubernetes.io\x12\x13encrypted-local-kek" dekSourceAESGCMKeyPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", dekSourceAESGCMKeyName, legacyDataNamespace) if _, err := test.writeRawRecordToETCD(dekSourceAESGCMKeyPath, []byte(dekSourceAESGCMKeyData)); err != nil { t.Fatal(err) } expectedDEKSourceAESGCMKeyObject := &kmstypes.EncryptedObject{ EncryptedData: []byte("\xb0ƙ\xdc\x01ʚ;\x00\x00\x00\x00\x8c\x80\v\xf0\x1dKo{0\v\xb5\xd5Ń\x1a\xb5\x0e\xcf\xd7Ve\xed邸\xdfE\xedMk\xcf!\x15\xc0\v\t\x82Wh \x9e\x8f\x1b\\9\b\xa4\x80\x02m\xf4P\x14z\xee\xf7\x8c\x1a\x84n5\xfdG\x83v#\x0e\xd4\f\x83YwH\xe1\x1c\xbf\x12\xc6\x1b\xba\x8br\b\x82z\xf8\xdb`\xa7]P\xb1\xe6!Lb\x8d\xb8I\x1aEL\xa0\xae+\xbe\x15R\x8e\x9b\x064\xf6P\xb6;\x9f\xa6\x8d\x96\xb2\x01\xa1\x8e\xe4a\xdf/\x90u\xde6\x9a\xc2ͻb\x88+\x16\x98=\xe9\x03\xdd\xd7HvC\n\xe5\x8cv\x05~\n\xabX_N\x9a\x84wp\xa8\x13\x0f\x82Y9x\xed\x89\x15\xb9\xe1ꦐ\xc6`R\n0\x04\xf2\xa6ѥ\x85\nk\xf4\xcf\xe4ul\x1c*[A\x12\xa0\xd9\xf2\xb5!\x82\xe4\x00\xa4L'&\xf5pln\"\xe0=\f[\xe1\xb0U97\x11|\xdaNk\xc3=\xc2\xf2\x85<7\x1e\x01\xb8\xa9\xf4\x89\xdb~\xe1\x8c\xe1\x1f\x05B@WʼS\n聛LY\x86$\xf6\x01ݝ\xcd\x1d\xe9\x02]\xf4i\xda\xfa\xc2\x0eUr\xc5Dʽdb\x13\xbe\xfe\x1c\xc5\xe1\x84\xcc\xdf&\x93j\x1eK\x04\xba\x06\x16\x85\x0e\x1f\xca\b\x90\x06\x11K\x9d[\rV\xe8E\xd5(\x91\fn\xd4\x10\x9cH\x1cܝX\x94ȁ5\x1b\x8c\xbbz\xf9Ho{\x1d\x112\x90F\xe7\xd8h\xa8\xa1\xf6\x8c\x8cvʲ1\xf9#\x82\xa3\xbe7ed\xd9\x14\xf3\x06\xff\xb7߫i"), KeyID: "1", EncryptedDEKSource: []byte("gafIvwT0ASoKdZ/D1L2SlH73LUMj5qa3hroljfS51wc="), Annotations: map[string][]byte{"local-kek.kms.kubernetes.io": []byte("encrypted-local-kek")}, EncryptedDEKSourceType: kmstypes.EncryptedDEKSourceType_AES_GCM_KEY, } legacyDEKSourceAESGCMKeyObject := &kmstypes.EncryptedObject{} if err := proto.Unmarshal([]byte(strings.TrimPrefix(dekSourceAESGCMKeyData, "k8s:enc:kms:v2:kms-provider:")), legacyDEKSourceAESGCMKeyObject); err != nil { t.Fatal(err) } if err := kmsv2.ValidateEncryptedObject(legacyDEKSourceAESGCMKeyObject); err != nil { t.Fatal(err) } if diff := cmp.Diff(expectedDEKSourceAESGCMKeyObject, legacyDEKSourceAESGCMKeyObject); len(diff) > 0 { t.Errorf("kms v2 legacy encrypted object diff (-want, +got):\n%s", diff) } // commit: 855e7c48de7388eb330da0f8d9d2394ee818fb8d // tag: v1.28.0 const dekSourceHKDFSHA256XNonceAESGCMSeedName = "dek-source-hkdf-sha256-xnonce-aes-gcm-seed" const dekSourceHKDFSHA256XNonceAESGCMSeedData = "k8s:enc:kms:v2:kms-provider:\n\xd1\x03\x12\x0f\x87\xae\xa2\xa2\xf5J\x11\x06о\x8a\x81\xb6\x15\xdf4H\xa3\xb7i\x93^)\xe5i\xe2\x7f\xfdo\xee\"\x170\xb7\n\xa0\v\xec\xe0\xfa\t#\tƖ\xf6ǧw\x01\xb9\xab\xb3\xf4\xdf\xec\x9eJ\x95$&Z=\x8awAc\xa5\xb2;\t\xd5\xf9\x05\xc1E)\x8b\xb8\x14\xc9\xda\x14{I\rV?\xe5:\xf0BM\x9b\xad\xaaHn>W/Q\xa3\xf5\xba\xe7˚\n\xe7\"\xa7\\p\x8c\xba2\xf2\xb0x<Ą\x88\x9a\xf1\xb5:d=z\xe3\xc3&\x03\x99m\x96\xe7\\\xe3\xa3\xd7i\xb2\f\x84g\xf94\xd2\f\xd6~\xed\xac\xf8\x1b\xc6(,[\xd1\xff\xba\x89ȇD\x02):wTM12\xb5\xfdl\xd2\xf2\x85\x120\xd3\xd9aak\xce\xddI֥\x0fk2\xf6I\xd0\xf9\xc2\xda\vŗ\xd7\u05fb\x83\xd6\xf7\x1a\xbf\x13iH\x8f\xe4\xb3#\xf3\xdf\xc8$y\rL(F\\Xt\x86\xbb\xe5K\x88=\x92\xe9\x04\xf70\x1e\t>\xac;\x9e\xe6\xf0+ۙ4\x1d\x1aV9Մ-g\xf3\xc7Z\x00\xf73\x0e\xe7\xa6\xcf\xc4\xfc\xe0\xb2\x1f\xa0\xbb\x1a\x81\xb3\xe4צ\x7f\xc6\b\x94͉\xad@\xac\x81\x015\x0f\xe8A\xe9B\xfb2\x81o\x9c?*\f\x8c\x15\xa8)\"\xe8\xff\x8d8\xd5!O\x17\xc5㍀\x83\xd3´\xca1;\xf7\xb0\xf4\x90x\xa6\x01\x95\x85\xc0\xaf\xf6\x82Qk\xab\xc1\x82W/Q\xa3\xf5\xba\xe7˚\n\xe7\"\xa7\\p\x8c\xba2\xf2\xb0x<Ą\x88\x9a\xf1\xb5:d=z\xe3\xc3&\x03\x99m\x96\xe7\\\xe3\xa3\xd7i\xb2\f\x84g\xf94\xd2\f\xd6~\xed\xac\xf8\x1b\xc6(,[\xd1\xff\xba\x89ȇD\x02):wTM12\xb5\xfdl\xd2\xf2\x85\x120\xd3\xd9aak\xce\xddI֥\x0fk2\xf6I\xd0\xf9\xc2\xda\vŗ\xd7\u05fb\x83\xd6\xf7\x1a\xbf\x13iH\x8f\xe4\xb3#\xf3\xdf\xc8$y\rL(F\\Xt\x86\xbb\xe5K\x88=\x92\xe9\x04\xf70\x1e\t>\xac;\x9e\xe6\xf0+ۙ4\x1d\x1aV9Մ-g\xf3\xc7Z\x00\xf73\x0e\xe7\xa6\xcf\xc4\xfc\xe0\xb2\x1f\xa0\xbb\x1a\x81\xb3\xe4צ\x7f\xc6\b\x94͉\xad@\xac\x81\x015\x0f\xe8A\xe9B\xfb2\x81o\x9c?*\f\x8c\x15\xa8)\"\xe8\xff\x8d8\xd5!O\x17\xc5㍀\x83\xd3´\xca1;\xf7\xb0\xf4\x90x\xa6\x01\x95\x85\xc0\xaf\xf6\x82Qk\xab\xc1\x82 0 { t.Errorf("kms v2 legacy encrypted object diff (-want, +got):\n%s", diff) } ctx := testContext(t) legacySecrets, err := test.restClient.CoreV1().Secrets(legacyDataNamespace).List(ctx, metav1.ListOptions{}) if err != nil { t.Fatal(err) } dekSourceAESGCMKeyTime := metav1.NewTime(time.Date(2023, 9, 1, 11, 56, 49, 0, time.FixedZone("EDT", -4*60*60))) dekSourceHKDFSHA256XNonceAESGCMSeedTime := metav1.NewTime(time.Date(2023, 9, 1, 10, 23, 13, 0, time.FixedZone("EDT", -4*60*60))) expectedLegacySecrets := &corev1.SecretList{ Items: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: dekSourceAESGCMKeyName, Namespace: legacyDataNamespace, UID: "1f4a8f7b-01b4-49d1-b898-751eb56937f1", CreationTimestamp: dekSourceAESGCMKeyTime, ManagedFields: []metav1.ManagedFieldsEntry{ { Manager: "___TestKMSv2Provider_in_k8s_io_kubernetes_test_integration_controlplane_transformation.test", Operation: "Update", APIVersion: "v1", Time: &dekSourceAESGCMKeyTime, FieldsType: "FieldsV1", FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:data":{".":{},"f:api_key":{}},"f:type":{}}`)}, }, }, }, Data: map[string][]byte{ secretKey: []byte(secretVal), }, Type: corev1.SecretTypeOpaque, }, { ObjectMeta: metav1.ObjectMeta{ Name: dekSourceHKDFSHA256XNonceAESGCMSeedName, Namespace: legacyDataNamespace, UID: "87c514b4-9c26-4041-ad0d-0d07dca557ed", CreationTimestamp: dekSourceHKDFSHA256XNonceAESGCMSeedTime, ManagedFields: []metav1.ManagedFieldsEntry{ { Manager: "___TestKMSv2Provider_extended_nonce_gcm_in_k8s_io_kubernetes_test_integration_controlplane_transformation.test", Operation: "Update", APIVersion: "v1", Time: &dekSourceHKDFSHA256XNonceAESGCMSeedTime, FieldsType: "FieldsV1", FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:data":{".":{},"f:api_key":{}},"f:type":{}}`)}, }, }, }, Data: map[string][]byte{ secretKey: []byte(secretVal), }, Type: corev1.SecretTypeOpaque, }, }, } if diff := cmp.Diff(expectedLegacySecrets, legacySecrets, // resource version is set after decoding based on etcd state - it is not stored in the etcd value cmpopts.IgnoreFields(corev1.Secret{}, "ResourceVersion"), cmpopts.IgnoreFields(metav1.ListMeta{}, "ResourceVersion"), ); len(diff) > 0 { t.Errorf("kms v2 legacy secret data diff (-want, +got):\n%s", diff) } }