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  }