1
2
3
4
19
20 package transformation
21
22 import (
23 "bytes"
24 "context"
25 "crypto/aes"
26 "crypto/cipher"
27 "encoding/binary"
28 "fmt"
29 "io"
30 "path"
31 "regexp"
32 "strings"
33 "testing"
34 "time"
35
36 "github.com/gogo/protobuf/proto"
37 "github.com/google/go-cmp/cmp"
38 "github.com/google/go-cmp/cmp/cmpopts"
39 clientv3 "go.etcd.io/etcd/client/v3"
40
41 corev1 "k8s.io/api/core/v1"
42 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
43 "k8s.io/apimachinery/pkg/api/meta"
44 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
45 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
46 "k8s.io/apimachinery/pkg/runtime"
47 "k8s.io/apimachinery/pkg/runtime/schema"
48 utilrand "k8s.io/apimachinery/pkg/util/rand"
49 "k8s.io/apimachinery/pkg/util/sets"
50 "k8s.io/apimachinery/pkg/util/uuid"
51 "k8s.io/apimachinery/pkg/util/wait"
52 "k8s.io/apiserver/pkg/endpoints/request"
53 "k8s.io/apiserver/pkg/features"
54 "k8s.io/apiserver/pkg/registry/generic"
55 genericapiserver "k8s.io/apiserver/pkg/server"
56 "k8s.io/apiserver/pkg/server/options/encryptionconfig"
57 "k8s.io/apiserver/pkg/storage/storagebackend"
58 "k8s.io/apiserver/pkg/storage/value"
59 aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
60 "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2"
61 kmstypes "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2/v2"
62 kmsv2mock "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/testing/v2"
63 utilfeature "k8s.io/apiserver/pkg/util/feature"
64 "k8s.io/client-go/dynamic"
65 "k8s.io/client-go/kubernetes"
66 "k8s.io/client-go/rest"
67 "k8s.io/klog/v2"
68 kmsv2api "k8s.io/kms/apis/v2"
69 kmsv2svc "k8s.io/kms/pkg/service"
70 "k8s.io/kubernetes/pkg/api/legacyscheme"
71 api "k8s.io/kubernetes/pkg/apis/core"
72 "k8s.io/kubernetes/pkg/controlplane"
73 "k8s.io/kubernetes/pkg/kubeapiserver"
74 secretstore "k8s.io/kubernetes/pkg/registry/core/secret/storage"
75 "k8s.io/kubernetes/test/integration"
76 "k8s.io/kubernetes/test/integration/etcd"
77 "k8s.io/kubernetes/test/integration/framework"
78 )
79
80 type envelopekmsv2 struct {
81 providerName string
82 rawEnvelope []byte
83 plainTextDEKSource []byte
84 useSeed bool
85 }
86
87 func (r envelopekmsv2) prefix() string {
88 return fmt.Sprintf("k8s:enc:kms:v2:%s:", r.providerName)
89 }
90
91 func (r envelopekmsv2) prefixLen() int {
92 return len(r.prefix())
93 }
94
95 func (r envelopekmsv2) cipherTextDEKSource() ([]byte, error) {
96 o := &kmstypes.EncryptedObject{}
97 if err := proto.Unmarshal(r.rawEnvelope[r.startOfPayload(r.providerName):], o); err != nil {
98 return nil, err
99 }
100
101 if err := kmsv2.ValidateEncryptedObject(o); err != nil {
102 return nil, err
103 }
104
105 if r.useSeed && o.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED {
106 return nil, fmt.Errorf("wrong type used with useSeed=true")
107 }
108
109 if !r.useSeed && o.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_AES_GCM_KEY {
110 return nil, fmt.Errorf("wrong type used with useSeed=false")
111 }
112
113 return o.EncryptedDEKSource, nil
114 }
115
116 func (r envelopekmsv2) startOfPayload(_ string) int {
117 return r.prefixLen()
118 }
119
120 func (r envelopekmsv2) cipherTextPayload() ([]byte, error) {
121 o := &kmstypes.EncryptedObject{}
122 if err := proto.Unmarshal(r.rawEnvelope[r.startOfPayload(r.providerName):], o); err != nil {
123 return nil, err
124 }
125
126 if err := kmsv2.ValidateEncryptedObject(o); err != nil {
127 return nil, err
128 }
129
130 return o.EncryptedData, nil
131 }
132
133 func (r envelopekmsv2) plainTextPayload(secretETCDPath string) ([]byte, error) {
134 var transformer value.Read
135 var err error
136 if r.useSeed {
137 transformer, err = aestransformer.NewHKDFExtendedNonceGCMTransformer(r.plainTextDEKSource)
138 } else {
139 var block cipher.Block
140 block, err = aes.NewCipher(r.plainTextDEKSource)
141 if err != nil {
142 return nil, err
143 }
144 transformer, err = aestransformer.NewGCMTransformer(block)
145 }
146 if err != nil {
147 return nil, err
148 }
149
150 ctx := context.Background()
151 dataCtx := value.DefaultContext(secretETCDPath)
152
153 data, err := r.cipherTextPayload()
154 if err != nil {
155 return nil, fmt.Errorf("failed to get cipher text payload: %v", err)
156 }
157 plainSecret, _, err := transformer.TransformFromStorage(ctx, data, dataCtx)
158 if err != nil {
159 return nil, fmt.Errorf("failed to transform from storage via AESGCM, err: %w", err)
160 }
161
162 return plainSecret, nil
163 }
164
165
166
167 func TestDefaultValues(t *testing.T) {
168 if encryptionconfig.GetKDF() != true {
169 t.Fatalf("without updating the feature flags, default value of KMSv2KDF should be enabled.")
170 }
171 if utilfeature.DefaultFeatureGate.Enabled(features.KMSv2) != true {
172 t.Fatalf("without updating the feature flags, default value of KMSv2 should be enabled.")
173 }
174 if utilfeature.DefaultFeatureGate.Enabled(features.KMSv1) != false {
175 t.Fatalf("without updating the feature flags, default value of KMSv1 should be disabled.")
176 }
177
178
179 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
180 t.Cleanup(cancel)
181
182 encryptionConfig := `
183 kind: EncryptionConfiguration
184 apiVersion: apiserver.config.k8s.io/v1
185 resources:
186 - resources:
187 - pods
188 providers:
189 - kms:
190 apiVersion: v2
191 name: kms-provider
192 endpoint: unix:///@kms-provider.sock
193 `
194 _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock")
195
196 test, err := newTransformTest(t, encryptionConfig, false, "", nil)
197 if err != nil {
198 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
199 }
200 t.Cleanup(test.cleanUp)
201
202 client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig)
203 if _, err := client.CoreV1().Pods(testNamespace).Create(ctx, &corev1.Pod{
204 ObjectMeta: metav1.ObjectMeta{
205 Name: "test",
206 },
207 Spec: corev1.PodSpec{
208 Containers: []corev1.Container{
209 {
210 Name: "busybox",
211 Image: "busybox",
212 },
213 },
214 },
215 }, metav1.CreateOptions{}); err != nil {
216 t.Fatal(err)
217 }
218
219 config := test.kubeAPIServer.ServerOpts.Etcd.StorageConfig
220 rawClient, etcdClient, err := integration.GetEtcdClients(config.Transport)
221 if err != nil {
222 t.Fatalf("failed to create etcd client: %v", err)
223 }
224 t.Cleanup(func() { _ = rawClient.Close() })
225
226 response, err := etcdClient.Get(ctx, "/"+config.Prefix+"/pods/"+testNamespace+"/", clientv3.WithPrefix())
227 if err != nil {
228 t.Fatal(err)
229 }
230 if len(response.Kvs) != 1 {
231 t.Fatalf("expected 1 KVs, but got %d", len(response.Kvs))
232 }
233 object := kmstypes.EncryptedObject{}
234 v := bytes.TrimPrefix(response.Kvs[0].Value, []byte("k8s:enc:kms:v2:kms-provider:"))
235 if err := proto.Unmarshal(v, &object); err != nil {
236 t.Fatal(err)
237 }
238 if object.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED {
239 t.Errorf("invalid type: %d", object.EncryptedDEKSourceType)
240 }
241 }
242
243
244
245
246
247
248
249
250 func TestKMSv2Provider(t *testing.T) {
251 defaultUseSeed := encryptionconfig.GetKDF()
252
253 t.Run("regular gcm", func(t *testing.T) {
254 defer encryptionconfig.SetKDFForTests(false)()
255 testKMSv2Provider(t, !defaultUseSeed)
256 })
257 t.Run("extended nonce gcm", func(t *testing.T) {
258 defer encryptionconfig.SetKDFForTests(true)()
259 testKMSv2Provider(t, defaultUseSeed)
260 })
261 }
262
263 func testKMSv2Provider(t *testing.T, useSeed bool) {
264 encryptionConfig := `
265 kind: EncryptionConfiguration
266 apiVersion: apiserver.config.k8s.io/v1
267 resources:
268 - resources:
269 - secrets
270 providers:
271 - kms:
272 apiVersion: v2
273 name: kms-provider
274 endpoint: unix:///@kms-provider.sock
275 `
276 genericapiserver.SetHostnameFuncForTests("testAPIServerID")
277 providerName := "kms-provider"
278 pluginMock := kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock")
279
280 test, err := newTransformTest(t, encryptionConfig, false, "", nil)
281 if err != nil {
282 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
283 }
284 defer test.cleanUp()
285
286 ctx := testContext(t)
287
288
289 copyConfig := rest.CopyConfig(test.kubeAPIServer.ClientConfig)
290 copyConfig.GroupVersion = &schema.GroupVersion{}
291 copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
292 rc, err := rest.RESTClientFor(copyConfig)
293 if err != nil {
294 t.Fatal(err)
295 }
296 if err := rc.Delete().AbsPath("/metrics").Do(ctx).Error(); err != nil {
297 t.Fatal(err)
298 }
299
300
301 wantMetricStrings := []string{
302 `apiserver_envelope_encryption_dek_source_cache_size{provider_name="kms-provider"} 1`,
303 `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`,
304 `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`,
305 `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`,
306 `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`,
307 }
308 defer func() {
309 body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx)
310 if err != nil {
311 t.Fatal(err)
312 }
313 var gotMetricStrings []string
314 trimFP := regexp.MustCompile(`(.*)(} \d+\.\d+.*)`)
315 for _, line := range strings.Split(string(body), "\n") {
316 if strings.HasPrefix(line, "apiserver_envelope_") {
317 if strings.HasPrefix(line, "apiserver_envelope_encryption_dek_cache_fill_percent") {
318 continue
319 }
320
321 if strings.Contains(line, "_seconds") {
322 line = trimFP.ReplaceAllString(line, `$1`) + "} FP"
323 }
324
325 gotMetricStrings = append(gotMetricStrings, line)
326 }
327 }
328 if diff := cmp.Diff(wantMetricStrings, gotMetricStrings); diff != "" {
329 t.Errorf("unexpected metrics diff (-want +got): %s", diff)
330 }
331 }()
332
333 test.secret, err = test.createSecret(testSecret, testNamespace)
334 if err != nil {
335 t.Fatalf("Failed to create test secret, error: %v", err)
336 }
337
338 plainTextDEKSource := pluginMock.LastEncryptRequest()
339
340 secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace)
341 rawEnvelope, err := test.getRawSecretFromETCD()
342 if err != nil {
343 t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
344 }
345
346 envelopeData := envelopekmsv2{
347 providerName: providerName,
348 rawEnvelope: rawEnvelope,
349 plainTextDEKSource: plainTextDEKSource,
350 useSeed: useSeed,
351 }
352
353 wantPrefix := envelopeData.prefix()
354 if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefix)) {
355 t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope)
356 }
357
358 ciphertext, err := envelopeData.cipherTextDEKSource()
359 if err != nil {
360 t.Fatalf("failed to get ciphertext DEK/seed from KMSv2 Plugin: %v", err)
361 }
362 decryptResponse, err := pluginMock.Decrypt(ctx, &kmsv2api.DecryptRequest{Uid: string(uuid.NewUUID()), Ciphertext: ciphertext})
363 if err != nil {
364 t.Fatalf("failed to decrypt DEK, %v", err)
365 }
366 dekSourcePlainAsWouldBeSeenByETCD := decryptResponse.Plaintext
367
368 if !bytes.Equal(plainTextDEKSource, dekSourcePlainAsWouldBeSeenByETCD) {
369 t.Fatalf("expected plainTextDEKSource %v to be passed to KMS Plugin, but got %s",
370 plainTextDEKSource, dekSourcePlainAsWouldBeSeenByETCD)
371 }
372
373 plainSecret, err := envelopeData.plainTextPayload(secretETCDPath)
374 if err != nil {
375 t.Fatalf("failed to transform from storage via AESGCM, err: %v", err)
376 }
377
378 if !strings.Contains(string(plainSecret), secretVal) {
379 t.Fatalf("expected %q after decryption, but got %q", secretVal, string(plainSecret))
380 }
381
382 secretClient := test.restClient.CoreV1().Secrets(testNamespace)
383
384 s, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{})
385 if err != nil {
386 t.Fatalf("failed to get Secret from %s, err: %v", testNamespace, err)
387 }
388 if secretVal != string(s.Data[secretKey]) {
389 t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
390 }
391 }
392
393
394
395
396
397
398
399
400
401
402 func TestKMSv2ProviderKeyIDStaleness(t *testing.T) {
403 t.Run("regular gcm", func(t *testing.T) {
404 defer encryptionconfig.SetKDFForTests(false)()
405 testKMSv2ProviderKeyIDStaleness(t)
406 })
407 t.Run("extended nonce gcm", func(t *testing.T) {
408 testKMSv2ProviderKeyIDStaleness(t)
409 })
410 }
411
412 func testKMSv2ProviderKeyIDStaleness(t *testing.T) {
413 encryptionConfig := `
414 kind: EncryptionConfiguration
415 apiVersion: apiserver.config.k8s.io/v1
416 resources:
417 - resources:
418 - pods
419 - deployments.apps
420 providers:
421 - kms:
422 apiVersion: v2
423 name: kms-provider
424 endpoint: unix:///@kms-provider.sock
425 `
426 pluginMock := kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock")
427
428 test, err := newTransformTest(t, encryptionConfig, false, "", nil)
429 if err != nil {
430 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
431 }
432 defer test.cleanUp()
433
434 dynamicClient := dynamic.NewForConfigOrDie(test.kubeAPIServer.ClientConfig)
435
436 testPod, err := test.createPod(testNamespace, dynamicClient)
437 if err != nil {
438 t.Fatalf("Failed to create test pod, error: %v, ns: %s", err, testNamespace)
439 }
440 version1 := testPod.GetResourceVersion()
441
442
443 updatedPod, err := test.inplaceUpdatePod(testNamespace, testPod, dynamicClient)
444 if err != nil {
445 t.Fatalf("Failed to update test pod, error: %v, ns: %s", err, testNamespace)
446 }
447 version2 := updatedPod.GetResourceVersion()
448 if version1 != version2 {
449 t.Fatalf("Resource version should not have changed. old pod: %v, new pod: %v", testPod, updatedPod)
450 }
451
452 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
453 t.Cleanup(cancel)
454
455 useSeed := encryptionconfig.GetKDF()
456
457 var firstEncryptedDEKSource []byte
458 var f checkFunc
459 if useSeed {
460 f = func(_ int, _ uint64, etcdKey string, obj kmstypes.EncryptedObject) {
461 firstEncryptedDEKSource = obj.EncryptedDEKSource
462
463 if obj.KeyID != "1" {
464 t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID)
465 }
466 }
467 } else {
468 f = func(_ int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject) {
469 firstEncryptedDEKSource = obj.EncryptedDEKSource
470
471 if obj.KeyID != "1" {
472 t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID)
473 }
474
475
476
477 const want = 1_000_000_000 + 1
478 if want != counter {
479 t.Errorf("key %s: counter nonce is invalid: want %d, got %d", etcdKey, want, counter)
480 }
481 }
482 }
483 assertPodDEKSources(ctx, t, test.kubeAPIServer.ServerOpts.Etcd.StorageConfig,
484 1, 1, "k8s:enc:kms:v2:kms-provider:", f,
485 )
486 if len(firstEncryptedDEKSource) == 0 {
487 t.Fatal("unexpected empty DEK or seed")
488 }
489
490
491 pluginMock.UpdateKeyID()
492 if err := kmsv2mock.WaitForBase64PluginToBeUpdated(pluginMock); err != nil {
493 t.Fatalf("Failed to update keyID for plugin, err: %v", err)
494 }
495
496
497 version3 := ""
498 err = wait.Poll(time.Second, time.Minute,
499 func() (bool, error) {
500 t.Log("polling for in-place update rv change")
501 updatedPod, err = test.inplaceUpdatePod(testNamespace, updatedPod, dynamicClient)
502 if err != nil {
503 return false, err
504 }
505 version3 = updatedPod.GetResourceVersion()
506 if version1 != version3 {
507 return true, nil
508 }
509 return false, nil
510 })
511 if err != nil {
512 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)
513 }
514
515 if version1 == version3 {
516 t.Fatalf("Resource version should have changed after keyID update. old pod: %v, new pod: %v", testPod, updatedPod)
517 }
518
519 var wantCount uint64 = 1_000_000_000
520 wantCount++
521
522
523
524
525
526 var checkDEK checkFunc
527 if useSeed {
528 checkDEK = func(_ int, _ uint64, etcdKey string, obj kmstypes.EncryptedObject) {
529 if len(obj.EncryptedDEKSource) == 0 {
530 t.Error("unexpected empty DEK source")
531 }
532
533 if bytes.Equal(obj.EncryptedDEKSource, firstEncryptedDEKSource) {
534 t.Errorf("key %s: incorrectly has the same ESEED", etcdKey)
535 }
536
537 if obj.KeyID != "2" {
538 t.Errorf("key %s: want key ID %s, got %s", etcdKey, "2", obj.KeyID)
539 }
540 }
541 } else {
542 checkDEK = func(_ int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject) {
543 if len(obj.EncryptedDEKSource) == 0 {
544 t.Error("unexpected empty DEK source")
545 }
546
547 if bytes.Equal(obj.EncryptedDEKSource, firstEncryptedDEKSource) {
548 t.Errorf("key %s: incorrectly has the same EDEK", etcdKey)
549 }
550
551 if obj.KeyID != "2" {
552 t.Errorf("key %s: want key ID %s, got %s", etcdKey, "2", obj.KeyID)
553 }
554
555 if wantCount != counter {
556 t.Errorf("key %s: counter nonce is invalid: want %d, got %d", etcdKey, wantCount, counter)
557 }
558 }
559 }
560
561
562 updatedPod, err = test.inplaceUpdatePod(testNamespace, updatedPod, dynamicClient)
563 if err != nil {
564 t.Fatalf("Failed to update test pod, error: %v, ns: %s", err, testNamespace)
565 }
566 version4 := updatedPod.GetResourceVersion()
567 if version3 != version4 {
568 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)
569 }
570
571
572 if err := test.deletePod(testNamespace, dynamicClient); err != nil {
573 t.Fatalf("failed to delete test pod: %v", err)
574 }
575 wantCount++
576
577
578 pluginMock.EnterFailedState()
579 mustBeUnHealthy(t, "/kms-providers",
580 "internal server error: kms-provider-0: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
581 test.kubeAPIServer.ClientConfig)
582
583 newPod, err := test.createPod(testNamespace, dynamicClient)
584 if err != nil {
585 t.Fatalf("Create test pod should have succeeded due to valid DEK, ns: %s, got: %v", testNamespace, err)
586 }
587 wantCount++
588 version5 := newPod.GetResourceVersion()
589
590
591 updatedPod, err = test.inplaceUpdatePod(testNamespace, newPod, dynamicClient)
592 if err != nil {
593 t.Fatalf("Failed to perform no-op update on pod when kms-plugin is down, error: %v, ns: %s", err, testNamespace)
594 }
595 version6 := updatedPod.GetResourceVersion()
596 if version5 != version6 {
597 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)
598 }
599
600
601 origNowFunc := kmsv2.NowFunc
602 t.Cleanup(func() { kmsv2.NowFunc = origNowFunc })
603 kmsv2.NowFunc = func() time.Time { return origNowFunc().Add(5 * time.Minute) }
604
605
606 _, err = test.createPod(testNamespace, dynamicClient)
607 if err == nil || !strings.Contains(err.Error(), `encryptedDEKSource with keyID hash "sha256:d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35" expired at 2`) {
608 t.Fatalf("Create test pod should have failed due to encryption, ns: %s, got: %v", testNamespace, err)
609 }
610
611
612 updatedNewPod, err := test.inplaceUpdatePod(testNamespace, newPod, dynamicClient)
613 if err != nil {
614 t.Fatalf("Failed to perform no-op update on pod when kms-plugin is down, error: %v, ns: %s", err, testNamespace)
615 }
616 version7 := updatedNewPod.GetResourceVersion()
617 if version5 != version7 {
618 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)
619 }
620
621 assertPodDEKSources(ctx, t, test.kubeAPIServer.ServerOpts.Etcd.StorageConfig,
622 1, 1, "k8s:enc:kms:v2:kms-provider:", checkDEK,
623 )
624
625
626 kmsv2.NowFunc = origNowFunc
627 pluginMock.ExitFailedState()
628 err = wait.Poll(time.Second, 3*time.Minute,
629 func() (bool, error) {
630 t.Log("polling for plugin to be functional")
631 _, err = test.createDeployment("panda", testNamespace)
632 if err != nil {
633 t.Logf("failed to create deployment, plugin is likely still unhealthy: %v", err)
634 }
635 return err == nil, nil
636 })
637 if err != nil {
638 t.Fatalf("failed to restore plugin health, err: %v, ns: %s", err, testNamespace)
639 }
640
641
642 updatedNewPod2, err := test.inplaceUpdatePod(testNamespace, updatedNewPod, dynamicClient)
643 if err != nil {
644 t.Fatalf("Failed to perform no-op update on pod when kms-plugin is up, error: %v, ns: %s", err, testNamespace)
645 }
646 version8 := updatedNewPod2.GetResourceVersion()
647 if version7 != version8 {
648 t.Fatalf("Resource version should not have changed after plugin health is restored. old pod: %v, new pod: %v", updatedNewPod, updatedNewPod2)
649 }
650
651 defer encryptionconfig.SetKDFForTests(!useSeed)()
652
653
654 var version9 string
655 err = wait.Poll(time.Second, 3*time.Minute,
656 func() (bool, error) {
657 t.Log("polling for in-place update rv change due to KDF config change")
658 updatedNewPod2, err = test.inplaceUpdatePod(testNamespace, updatedNewPod2, dynamicClient)
659 if err != nil {
660 return false, err
661 }
662 version9 = updatedNewPod2.GetResourceVersion()
663 if version8 != version9 {
664 return true, nil
665 }
666 return false, nil
667 })
668 if err != nil {
669 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)
670 }
671 }
672
673 func TestKMSv2ProviderDEKSourceReuse(t *testing.T) {
674 t.Run("regular gcm", func(t *testing.T) {
675 defer encryptionconfig.SetKDFForTests(false)()
676 testKMSv2ProviderDEKSourceReuse(t,
677 func(i int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject) {
678 if obj.KeyID != "1" {
679 t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID)
680 }
681
682
683
684 if uint64(i+1_000_000_000+1) != counter {
685 t.Errorf("key %s: counter nonce is invalid: want %d, got %d", etcdKey, i+1, counter)
686 }
687 },
688 )
689 })
690 t.Run("extended nonce gcm", func(t *testing.T) {
691 defer encryptionconfig.SetKDFForTests(true)()
692 testKMSv2ProviderDEKSourceReuse(t,
693 func(_ int, _ uint64, etcdKey string, obj kmstypes.EncryptedObject) {
694 if obj.KeyID != "1" {
695 t.Errorf("key %s: want key ID %s, got %s", etcdKey, "1", obj.KeyID)
696 }
697 },
698 )
699 })
700 }
701
702 func testKMSv2ProviderDEKSourceReuse(t *testing.T, f checkFunc) {
703 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
704 t.Cleanup(cancel)
705
706 encryptionConfig := `
707 kind: EncryptionConfiguration
708 apiVersion: apiserver.config.k8s.io/v1
709 resources:
710 - resources:
711 - pods
712 providers:
713 - kms:
714 apiVersion: v2
715 name: kms-provider
716 endpoint: unix:///@kms-provider.sock
717 `
718 _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock")
719
720 test, err := newTransformTest(t, encryptionConfig, false, "", nil)
721 if err != nil {
722 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
723 }
724 t.Cleanup(test.cleanUp)
725
726 client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig)
727
728 const podCount = 1_000
729
730 for i := 0; i < podCount; i++ {
731 if _, err := client.CoreV1().Pods(testNamespace).Create(ctx, &corev1.Pod{
732 ObjectMeta: metav1.ObjectMeta{
733 Name: fmt.Sprintf("dek-reuse-%04d", i+1),
734 },
735 Spec: corev1.PodSpec{
736 Containers: []corev1.Container{
737 {
738 Name: "busybox",
739 Image: "busybox",
740 },
741 },
742 },
743 }, metav1.CreateOptions{}); err != nil {
744 t.Fatal(err)
745 }
746 }
747
748 assertPodDEKSources(ctx, t, test.kubeAPIServer.ServerOpts.Etcd.StorageConfig,
749 podCount, 1,
750 "k8s:enc:kms:v2:kms-provider:", f,
751 )
752 }
753
754 type checkFunc func(i int, counter uint64, etcdKey string, obj kmstypes.EncryptedObject)
755
756 func assertPodDEKSources(ctx context.Context, t *testing.T, config storagebackend.Config, podCount, dekSourcesCount int, kmsPrefix string, f checkFunc) {
757 t.Helper()
758
759 rawClient, etcdClient, err := integration.GetEtcdClients(config.Transport)
760 if err != nil {
761 t.Fatalf("failed to create etcd client: %v", err)
762 }
763 t.Cleanup(func() { _ = rawClient.Close() })
764
765 response, err := etcdClient.Get(ctx, "/"+config.Prefix+"/pods/"+testNamespace+"/", clientv3.WithPrefix())
766 if err != nil {
767 t.Fatal(err)
768 }
769
770 if len(response.Kvs) != podCount {
771 t.Fatalf("expected %d KVs, but got %d", podCount, len(response.Kvs))
772 }
773
774 useSeed := encryptionconfig.GetKDF()
775
776 out := make([]kmstypes.EncryptedObject, len(response.Kvs))
777 for i, kv := range response.Kvs {
778 v := bytes.TrimPrefix(kv.Value, []byte(kmsPrefix))
779 if err := proto.Unmarshal(v, &out[i]); err != nil {
780 t.Fatal(err)
781 }
782
783 if err := kmsv2.ValidateEncryptedObject(&out[i]); err != nil {
784 t.Fatal(err)
785 }
786
787 var infoLen int
788 if useSeed {
789 infoLen = 32
790 }
791
792 info := out[i].EncryptedData[:infoLen]
793 nonce := out[i].EncryptedData[infoLen : 12+infoLen]
794 randN := nonce[:4]
795 count := nonce[4:]
796
797 if bytes.Equal(randN, make([]byte, len(randN))) {
798 t.Errorf("key %s: got all zeros for first four bytes", string(kv.Key))
799 }
800
801 if useSeed {
802 if bytes.Equal(info, make([]byte, infoLen)) {
803 t.Errorf("key %s: got all zeros for info", string(kv.Key))
804 }
805 }
806
807 counter := binary.LittleEndian.Uint64(count)
808 f(i, counter, string(kv.Key), out[i])
809 }
810
811 uniqueDEKSources := sets.NewString()
812 for _, object := range out {
813 object := object
814 uniqueDEKSources.Insert(string(object.EncryptedDEKSource))
815 if useSeed {
816 if object.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED {
817 t.Errorf("invalid type: %d", object.EncryptedDEKSourceType)
818 }
819 } else {
820 if object.EncryptedDEKSourceType != kmstypes.EncryptedDEKSourceType_AES_GCM_KEY {
821 t.Errorf("invalid type: %d", object.EncryptedDEKSourceType)
822 }
823 }
824 }
825
826 if uniqueDEKSources.Has("") {
827 t.Error("unexpected empty DEK source seen")
828 }
829
830 if uniqueDEKSources.Len() != dekSourcesCount {
831 t.Errorf("expected %d DEK sources, got: %d", dekSourcesCount, uniqueDEKSources.Len())
832 }
833 }
834
835 func TestKMSv2Healthz(t *testing.T) {
836 defer encryptionconfig.SetKDFForTests(randomBool())()
837
838 encryptionConfig := `
839 kind: EncryptionConfiguration
840 apiVersion: apiserver.config.k8s.io/v1
841 resources:
842 - resources:
843 - secrets
844 providers:
845 - kms:
846 apiVersion: v2
847 name: provider-1
848 endpoint: unix:///@kms-provider-1.sock
849 - kms:
850 apiVersion: v2
851 name: provider-2
852 endpoint: unix:///@kms-provider-2.sock
853 `
854
855 pluginMock1 := kmsv2mock.NewBase64Plugin(t, "@kms-provider-1.sock")
856 pluginMock2 := kmsv2mock.NewBase64Plugin(t, "@kms-provider-2.sock")
857
858 test, err := newTransformTest(t, encryptionConfig, false, "", nil)
859 if err != nil {
860 t.Fatalf("Failed to start kube-apiserver, error: %v", err)
861 }
862 defer test.cleanUp()
863
864
865
866
867
868 mustBeHealthy(t, "/kms-providers", "ok", test.kubeAPIServer.ClientConfig)
869
870
871
872 pluginMock1.EnterFailedState()
873 mustBeUnHealthy(t, "/kms-providers",
874 "internal server error: kms-provider-0: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
875 test.kubeAPIServer.ClientConfig)
876 pluginMock1.ExitFailedState()
877
878
879
880 pluginMock2.EnterFailedState()
881 mustBeUnHealthy(t, "/kms-providers",
882 "internal server error: kms-provider-1: rpc error: code = FailedPrecondition desc = failed precondition - key disabled",
883 test.kubeAPIServer.ClientConfig)
884 pluginMock2.ExitFailedState()
885
886
887
888 mustBeHealthy(t, "/kms-providers", "ok", test.kubeAPIServer.ClientConfig)
889
890
891 pluginMock1.EnterFailedState()
892 pluginMock2.EnterFailedState()
893 mustBeUnHealthy(t, "/kms-providers",
894 "internal server error: "+
895 "[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,"+
896 " 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]",
897 test.kubeAPIServer.ClientConfig)
898 }
899
900 func TestKMSv2SingleService(t *testing.T) {
901 defer encryptionconfig.SetKDFForTests(randomBool())()
902
903 var kmsv2Calls int
904 origEnvelopeKMSv2ServiceFactory := encryptionconfig.EnvelopeKMSv2ServiceFactory
905 encryptionconfig.EnvelopeKMSv2ServiceFactory = func(ctx context.Context, endpoint, providerName string, callTimeout time.Duration) (kmsv2svc.Service, error) {
906 kmsv2Calls++
907 return origEnvelopeKMSv2ServiceFactory(ctx, endpoint, providerName, callTimeout)
908 }
909 t.Cleanup(func() {
910 encryptionconfig.EnvelopeKMSv2ServiceFactory = origEnvelopeKMSv2ServiceFactory
911 })
912
913
914
915
916
917 encryptionConfig := `
918 kind: EncryptionConfiguration
919 apiVersion: apiserver.config.k8s.io/v1
920 resources:
921 - resources:
922 - pods
923 - configmaps
924 - customresourcedefinitions.apiextensions.k8s.io
925 - pandas.awesome.bears.com
926 - apiservices.apiregistration.k8s.io
927 providers:
928 - kms:
929 apiVersion: v2
930 name: kms-provider
931 endpoint: unix:///@kms-provider.sock
932 `
933
934 _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock")
935
936 test, err := newTransformTest(t, encryptionConfig, false, "", nil)
937 if err != nil {
938 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
939 }
940 t.Cleanup(test.cleanUp)
941
942
943 etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(test.kubeAPIServer.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
944
945 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
946 t.Cleanup(cancel)
947
948 gvr := schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v1", Resource: "pandas"}
949 stub := etcd.GetEtcdStorageData()[gvr].Stub
950 dynamicClient, obj, err := etcd.JSONToUnstructured(stub, "", &meta.RESTMapping{
951 Resource: gvr,
952 GroupVersionKind: gvr.GroupVersion().WithKind("Panda"),
953 Scope: meta.RESTScopeRoot,
954 }, dynamic.NewForConfigOrDie(test.kubeAPIServer.ClientConfig))
955 if err != nil {
956 t.Fatal(err)
957 }
958 _, err = dynamicClient.Create(ctx, obj, metav1.CreateOptions{})
959 if err != nil {
960 t.Fatal(err)
961 }
962
963 if kmsv2Calls != 1 {
964 t.Fatalf("expected a single call to KMS v2 service factory: %v", kmsv2Calls)
965 }
966 }
967
968
969
970
971
972
973 func TestKMSv2FeatureFlag(t *testing.T) {
974 encryptionConfig := `
975 kind: EncryptionConfiguration
976 apiVersion: apiserver.config.k8s.io/v1
977 resources:
978 - resources:
979 - secrets
980 providers:
981 - kms:
982 apiVersion: v2
983 name: kms-provider
984 endpoint: unix:///@kms-provider.sock
985 `
986 providerName := "kms-provider"
987 pluginMock := kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock")
988 storageConfig := framework.SharedEtcd()
989
990
991 test, err := newTransformTest(t, encryptionConfig, false, "", storageConfig)
992 if err != nil {
993 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
994 }
995 defer func() {
996 test.cleanUp()
997 }()
998
999 test.secret, err = test.createSecret(testSecret, testNamespace)
1000 if err != nil {
1001 t.Fatalf("Failed to create test secret, error: %v", err)
1002 }
1003
1004
1005 plainTextDEKSource := pluginMock.LastEncryptRequest()
1006
1007 secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace)
1008 rawEnvelope, err := test.getRawSecretFromETCD()
1009 if err != nil {
1010 t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
1011 }
1012
1013 envelopeData := envelopekmsv2{
1014 providerName: providerName,
1015 rawEnvelope: rawEnvelope,
1016 plainTextDEKSource: plainTextDEKSource,
1017 useSeed: true,
1018 }
1019
1020 wantPrefix := envelopeData.prefix()
1021 if !bytes.HasPrefix(rawEnvelope, []byte(wantPrefix)) {
1022 t.Fatalf("expected secret to be prefixed with %s, but got %s", wantPrefix, rawEnvelope)
1023 }
1024
1025 ctx := testContext(t)
1026 ciphertext, err := envelopeData.cipherTextDEKSource()
1027 if err != nil {
1028 t.Fatalf("failed to get ciphertext DEK from KMSv2 Plugin: %v", err)
1029 }
1030 decryptResponse, err := pluginMock.Decrypt(ctx, &kmsv2api.DecryptRequest{Uid: string(uuid.NewUUID()), Ciphertext: ciphertext})
1031 if err != nil {
1032 t.Fatalf("failed to decrypt DEK, %v", err)
1033 }
1034 dekPlainAsWouldBeSeenByETCD := decryptResponse.Plaintext
1035
1036 if !bytes.Equal(plainTextDEKSource, dekPlainAsWouldBeSeenByETCD) {
1037 t.Fatalf("expected plainTextDEKSource %v to be passed to KMS Plugin, but got %s",
1038 plainTextDEKSource, dekPlainAsWouldBeSeenByETCD)
1039 }
1040
1041 plainSecret, err := envelopeData.plainTextPayload(secretETCDPath)
1042 if err != nil {
1043 t.Fatalf("failed to transform from storage via AESGCM, err: %v", err)
1044 }
1045
1046 if !strings.Contains(string(plainSecret), secretVal) {
1047 t.Fatalf("expected %q after decryption, but got %q", secretVal, string(plainSecret))
1048 }
1049
1050 secretClient := test.restClient.CoreV1().Secrets(testNamespace)
1051
1052 s, err := secretClient.Get(ctx, testSecret, metav1.GetOptions{})
1053 if err != nil {
1054 t.Fatalf("failed to get Secret from %s, err: %v", testNamespace, err)
1055 }
1056 if secretVal != string(s.Data[secretKey]) {
1057 t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
1058 }
1059 test.shutdownAPIServer()
1060
1061
1062
1063 test, err = newTransformTest(t, encryptionConfig, false, "", storageConfig)
1064 if err != nil {
1065 t.Fatalf("Failed to restart api server, error: %v", err)
1066 }
1067
1068
1069 s, err = test.restClient.CoreV1().Secrets(testNamespace).Get(
1070 ctx,
1071 testSecret,
1072 metav1.GetOptions{},
1073 )
1074 if err != nil {
1075 t.Fatalf("failed to read secret, err: %v", err)
1076 }
1077 if secretVal != string(s.Data[secretKey]) {
1078 t.Fatalf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
1079 }
1080 }
1081
1082 var benchSecret *api.Secret
1083
1084 func BenchmarkKMSv2KDF(b *testing.B) {
1085 b.StopTimer()
1086
1087 klog.SetOutput(io.Discard)
1088 klog.LogToStderr(false)
1089
1090 defer encryptionconfig.SetKDFForTests(false)()
1091
1092 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
1093 b.Cleanup(cancel)
1094
1095 ctx = request.WithNamespace(ctx, testNamespace)
1096
1097 encryptionConfig := `
1098 kind: EncryptionConfiguration
1099 apiVersion: apiserver.config.k8s.io/v1
1100 resources:
1101 - resources:
1102 - secrets
1103 providers:
1104 - kms:
1105 apiVersion: v2
1106 name: kms-provider
1107 endpoint: unix:///@kms-provider.sock
1108 `
1109 _ = kmsv2mock.NewBase64Plugin(b, "@kms-provider.sock")
1110
1111 test, err := newTransformTest(b, encryptionConfig, false, "", nil)
1112 if err != nil {
1113 b.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
1114 }
1115 b.Cleanup(test.cleanUp)
1116
1117 client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig)
1118
1119 restOptionsGetter := getRESTOptionsGetterForSecrets(b, test)
1120
1121 secretStorage, err := secretstore.NewREST(restOptionsGetter)
1122 if err != nil {
1123 b.Fatal(err)
1124 }
1125 b.Cleanup(secretStorage.Destroy)
1126
1127 const dataLen = 1_000
1128
1129 secrets := make([]*api.Secret, dataLen)
1130
1131 for i := 0; i < dataLen; i++ {
1132 secrets[i] = &api.Secret{
1133 ObjectMeta: metav1.ObjectMeta{
1134 Name: fmt.Sprintf("test-secret-%d", i),
1135 Namespace: testNamespace,
1136 },
1137 Data: map[string][]byte{
1138 "lots_of_data": bytes.Repeat([]byte{1, 3, 3, 7}, i*dataLen/4),
1139 },
1140 }
1141 }
1142
1143 b.StartTimer()
1144 for i := 0; i < b.N; i++ {
1145 _, err := secretStorage.DeleteCollection(ctx, noValidation, &metav1.DeleteOptions{}, nil)
1146 if err != nil {
1147 b.Fatal(err)
1148 }
1149
1150 for i := 0; i < dataLen; i++ {
1151 out, err := secretStorage.Create(ctx, secrets[i], noValidation, &metav1.CreateOptions{})
1152 if err != nil {
1153 b.Fatal(err)
1154 }
1155
1156 benchSecret = out.(*api.Secret)
1157
1158 out, err = secretStorage.Get(ctx, benchSecret.Name, &metav1.GetOptions{})
1159 if err != nil {
1160 b.Fatal(err)
1161 }
1162
1163 benchSecret = out.(*api.Secret)
1164 }
1165 }
1166 b.StopTimer()
1167
1168 secretList, err := client.CoreV1().Secrets(testNamespace).List(ctx, metav1.ListOptions{})
1169 if err != nil {
1170 b.Fatal(err)
1171 }
1172
1173 if secretLen := len(secretList.Items); secretLen != dataLen {
1174 b.Errorf("unexpected secret len: want %d, got %d", dataLen, secretLen)
1175 }
1176 }
1177
1178 func getRESTOptionsGetterForSecrets(t testing.TB, test *transformTest) generic.RESTOptionsGetter {
1179 t.Helper()
1180
1181 s := test.kubeAPIServer.ServerOpts
1182
1183 etcdConfigCopy := *s.Etcd
1184 etcdConfigCopy.SkipHealthEndpoints = true
1185 etcdConfigCopy.EncryptionProviderConfigAutomaticReload = true
1186
1187
1188
1189 genericConfig := genericapiserver.NewConfig(legacyscheme.Codecs)
1190
1191 genericConfig.MergedResourceConfig = controlplane.DefaultAPIResourceConfigSource()
1192
1193 if err := s.APIEnablement.ApplyTo(genericConfig, controlplane.DefaultAPIResourceConfigSource(), legacyscheme.Scheme); err != nil {
1194 t.Fatal(err)
1195 }
1196
1197 storageFactoryConfig := kubeapiserver.NewStorageFactoryConfig()
1198 storageFactoryConfig.APIResourceConfig = genericConfig.MergedResourceConfig
1199 storageFactory, err := storageFactoryConfig.Complete(&etcdConfigCopy).New()
1200 if err != nil {
1201 t.Fatal(err)
1202 }
1203 if err := etcdConfigCopy.ApplyWithStorageFactoryTo(storageFactory, genericConfig); err != nil {
1204 t.Fatal(err)
1205 }
1206
1207 transformers, ok := genericConfig.ResourceTransformers.(*encryptionconfig.DynamicTransformers)
1208 if !ok {
1209 t.Fatalf("incorrect type for ResourceTransformers: %T", genericConfig.ResourceTransformers)
1210 }
1211
1212 t.Cleanup(func() {
1213
1214 transformers.Set(nil, nil, nil, 0)
1215 time.Sleep(10 * time.Second)
1216 })
1217
1218 if genericConfig.RESTOptionsGetter == nil {
1219 t.Fatal("not REST options found")
1220 }
1221
1222 opts, err := genericConfig.RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: "", Resource: "secrets"})
1223 if err != nil {
1224 t.Fatal(err)
1225 }
1226
1227 if err := runtime.CheckCodec(opts.StorageConfig.Codec, &api.Secret{},
1228 schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}); err != nil {
1229 t.Fatal(err)
1230 }
1231
1232 return genericConfig.RESTOptionsGetter
1233 }
1234
1235 func noValidation(_ context.Context, _ runtime.Object) error { return nil }
1236
1237 var benchRESTSecret *corev1.Secret
1238
1239 func BenchmarkKMSv2REST(b *testing.B) {
1240 b.StopTimer()
1241
1242 klog.SetOutput(io.Discard)
1243 klog.LogToStderr(false)
1244
1245 defer encryptionconfig.SetKDFForTests(true)()
1246
1247 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
1248 b.Cleanup(cancel)
1249
1250 encryptionConfig := `
1251 kind: EncryptionConfiguration
1252 apiVersion: apiserver.config.k8s.io/v1
1253 resources:
1254 - resources:
1255 - secrets
1256 providers:
1257 - kms:
1258 apiVersion: v2
1259 name: kms-provider
1260 endpoint: unix:///@kms-provider.sock
1261 `
1262 _ = kmsv2mock.NewBase64Plugin(b, "@kms-provider.sock")
1263
1264 test, err := newTransformTest(b, encryptionConfig, false, "", nil)
1265 if err != nil {
1266 b.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
1267 }
1268 b.Cleanup(test.cleanUp)
1269
1270 client := kubernetes.NewForConfigOrDie(test.kubeAPIServer.ClientConfig)
1271
1272 const dataLen = 1_000
1273
1274 secretStorage := client.CoreV1().Secrets(testNamespace)
1275
1276 secrets := make([]*corev1.Secret, dataLen)
1277
1278 for i := 0; i < dataLen; i++ {
1279 secrets[i] = &corev1.Secret{
1280 ObjectMeta: metav1.ObjectMeta{
1281 Name: fmt.Sprintf("test-secret-%d", i),
1282 Namespace: testNamespace,
1283 },
1284 Data: map[string][]byte{
1285 "lots_of_data": bytes.Repeat([]byte{1, 3, 3, 7}, i*dataLen/4),
1286 },
1287 }
1288 }
1289
1290 b.StartTimer()
1291 for i := 0; i < b.N; i++ {
1292 err := secretStorage.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})
1293 if err != nil {
1294 b.Fatal(err)
1295 }
1296
1297 for i := 0; i < dataLen; i++ {
1298 out, err := secretStorage.Create(ctx, secrets[i], metav1.CreateOptions{})
1299 if err != nil {
1300 b.Fatal(err)
1301 }
1302
1303 benchRESTSecret = out
1304
1305 out, err = secretStorage.Get(ctx, benchRESTSecret.Name, metav1.GetOptions{})
1306 if err != nil {
1307 b.Fatal(err)
1308 }
1309
1310 benchRESTSecret = out
1311 }
1312 }
1313 b.StopTimer()
1314
1315 secretList, err := client.CoreV1().Secrets(testNamespace).List(ctx, metav1.ListOptions{})
1316 if err != nil {
1317 b.Fatal(err)
1318 }
1319
1320 if secretLen := len(secretList.Items); secretLen != dataLen {
1321 b.Errorf("unexpected secret len: want %d, got %d", dataLen, secretLen)
1322 }
1323 }
1324
1325 func randomBool() bool { return utilrand.Int()%2 == 1 }
1326
1327
1328 func TestKMSv2ProviderLegacyData(t *testing.T) {
1329 t.Run("regular gcm", func(t *testing.T) {
1330 defer encryptionconfig.SetKDFForTests(false)()
1331 testKMSv2ProviderLegacyData(t)
1332 })
1333 t.Run("extended nonce gcm", func(t *testing.T) {
1334 defer encryptionconfig.SetKDFForTests(true)()
1335 testKMSv2ProviderLegacyData(t)
1336 })
1337 }
1338
1339 func testKMSv2ProviderLegacyData(t *testing.T) {
1340 encryptionConfig := `
1341 kind: EncryptionConfiguration
1342 apiVersion: apiserver.config.k8s.io/v1
1343 resources:
1344 - resources:
1345 - secrets
1346 providers:
1347 - kms:
1348 apiVersion: v2
1349 name: kms-provider
1350 endpoint: unix:///@kms-provider.sock
1351 `
1352
1353 _ = kmsv2mock.NewBase64Plugin(t, "@kms-provider.sock")
1354
1355
1356
1357 const legacyDataEtcdPrefix = "43da1478-5e9c-4ef3-a92a-2b19d540c8a5"
1358
1359 storageConfig := storagebackend.NewDefaultConfig(path.Join(legacyDataEtcdPrefix, "registry"), nil)
1360 storageConfig.Transport.ServerList = []string{framework.GetEtcdURL()}
1361
1362 test, err := newTransformTest(t, encryptionConfig, false, "", storageConfig)
1363 if err != nil {
1364 t.Fatalf("failed to start KUBE API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
1365 }
1366 defer test.cleanUp()
1367
1368 const legacyDataNamespace = "kms-v2-legacy-data"
1369
1370 if _, err := test.createNamespace(legacyDataNamespace); err != nil {
1371 t.Fatal(err)
1372 }
1373
1374
1375
1376 const dekSourceAESGCMKeyName = "dek-source-aes-gcm-key"
1377 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"
1378 dekSourceAESGCMKeyPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", dekSourceAESGCMKeyName, legacyDataNamespace)
1379 if _, err := test.writeRawRecordToETCD(dekSourceAESGCMKeyPath, []byte(dekSourceAESGCMKeyData)); err != nil {
1380 t.Fatal(err)
1381 }
1382
1383 expectedDEKSourceAESGCMKeyObject := &kmstypes.EncryptedObject{
1384 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"),
1385 KeyID: "1",
1386 EncryptedDEKSource: []byte("gafIvwT0ASoKdZ/D1L2SlH73LUMj5qa3hroljfS51wc="),
1387 Annotations: map[string][]byte{"local-kek.kms.kubernetes.io": []byte("encrypted-local-kek")},
1388 EncryptedDEKSourceType: kmstypes.EncryptedDEKSourceType_AES_GCM_KEY,
1389 }
1390 legacyDEKSourceAESGCMKeyObject := &kmstypes.EncryptedObject{}
1391 if err := proto.Unmarshal([]byte(strings.TrimPrefix(dekSourceAESGCMKeyData, "k8s:enc:kms:v2:kms-provider:")), legacyDEKSourceAESGCMKeyObject); err != nil {
1392 t.Fatal(err)
1393 }
1394 if err := kmsv2.ValidateEncryptedObject(legacyDEKSourceAESGCMKeyObject); err != nil {
1395 t.Fatal(err)
1396 }
1397 if diff := cmp.Diff(expectedDEKSourceAESGCMKeyObject, legacyDEKSourceAESGCMKeyObject); len(diff) > 0 {
1398 t.Errorf("kms v2 legacy encrypted object diff (-want, +got):\n%s", diff)
1399 }
1400
1401
1402
1403 const dekSourceHKDFSHA256XNonceAESGCMSeedName = "dek-source-hkdf-sha256-xnonce-aes-gcm-seed"
1404 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\x82<D\x93\xcf\r\xdb\xdf\x1c\x94\x17Q\xfaS\xe6\xcb\xd4Xƿ\x80\x1d\xc4\x1c\x9dP74\x82JK\vy\xe9)\xbchY\xbe\xcc|\xe4\x97\xdd<;3\x90J\a\xee#\xb2y\xe3\t`\xef\xef\x1f\"k\x8b\x96\xa0\x98\xd9\xffs\xde&\xb7\xa6\x0e\xf1\x7f2ͅb\xe3\xda5\"c\\K\xe21\xa2\xec`\x1b\xe5R\xe6j\b@\x187\xe1\xdb\x04\xf6bNO\x0e\x12\x011\x1a,/+WnKXQEM/AhXICYRNBeGk+WSuB+7OBuSYJTbP66Zyc=\"2\n\x1blocal-kek.kms.kubernetes.io\x12\x13encrypted-local-kek(\x01"
1405 dekSourceHKDFSHA256XNonceAESGCMSeedPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", dekSourceHKDFSHA256XNonceAESGCMSeedName, legacyDataNamespace)
1406 if _, err := test.writeRawRecordToETCD(dekSourceHKDFSHA256XNonceAESGCMSeedPath, []byte(dekSourceHKDFSHA256XNonceAESGCMSeedData)); err != nil {
1407 t.Fatal(err)
1408 }
1409
1410 expectedDEKSourceHKDFSHA256XNonceAESGCMSeedObject := &kmstypes.EncryptedObject{
1411 EncryptedData: []byte("\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\x82<D\x93\xcf\r\xdb\xdf\x1c\x94\x17Q\xfaS\xe6\xcb\xd4Xƿ\x80\x1d\xc4\x1c\x9dP74\x82JK\vy\xe9)\xbchY\xbe\xcc|\xe4\x97\xdd<;3\x90J\a\xee#\xb2y\xe3\t`\xef\xef\x1f\"k\x8b\x96\xa0\x98\xd9\xffs\xde&\xb7\xa6\x0e\xf1\x7f2ͅb\xe3\xda5\"c\\K\xe21\xa2\xec`\x1b\xe5R\xe6j\b@\x187\xe1\xdb\x04\xf6bNO\x0e"),
1412 KeyID: "1",
1413 EncryptedDEKSource: []byte("/+WnKXQEM/AhXICYRNBeGk+WSuB+7OBuSYJTbP66Zyc="),
1414 Annotations: map[string][]byte{"local-kek.kms.kubernetes.io": []byte("encrypted-local-kek")},
1415 EncryptedDEKSourceType: kmstypes.EncryptedDEKSourceType_HKDF_SHA256_XNONCE_AES_GCM_SEED,
1416 }
1417 legacyDEKSourceHKDFSHA256XNonceAESGCMSeedObject := &kmstypes.EncryptedObject{}
1418 if err := proto.Unmarshal([]byte(strings.TrimPrefix(dekSourceHKDFSHA256XNonceAESGCMSeedData, "k8s:enc:kms:v2:kms-provider:")), legacyDEKSourceHKDFSHA256XNonceAESGCMSeedObject); err != nil {
1419 t.Fatal(err)
1420 }
1421 if err := kmsv2.ValidateEncryptedObject(legacyDEKSourceHKDFSHA256XNonceAESGCMSeedObject); err != nil {
1422 t.Fatal(err)
1423 }
1424 if diff := cmp.Diff(expectedDEKSourceHKDFSHA256XNonceAESGCMSeedObject, legacyDEKSourceHKDFSHA256XNonceAESGCMSeedObject); len(diff) > 0 {
1425 t.Errorf("kms v2 legacy encrypted object diff (-want, +got):\n%s", diff)
1426 }
1427
1428 ctx := testContext(t)
1429
1430 legacySecrets, err := test.restClient.CoreV1().Secrets(legacyDataNamespace).List(ctx, metav1.ListOptions{})
1431 if err != nil {
1432 t.Fatal(err)
1433 }
1434
1435 dekSourceAESGCMKeyTime := metav1.NewTime(time.Date(2023, 9, 1, 11, 56, 49, 0, time.FixedZone("EDT", -4*60*60)))
1436 dekSourceHKDFSHA256XNonceAESGCMSeedTime := metav1.NewTime(time.Date(2023, 9, 1, 10, 23, 13, 0, time.FixedZone("EDT", -4*60*60)))
1437
1438 expectedLegacySecrets := &corev1.SecretList{
1439 Items: []corev1.Secret{
1440 {
1441 ObjectMeta: metav1.ObjectMeta{
1442 Name: dekSourceAESGCMKeyName,
1443 Namespace: legacyDataNamespace,
1444 UID: "1f4a8f7b-01b4-49d1-b898-751eb56937f1",
1445 CreationTimestamp: dekSourceAESGCMKeyTime,
1446 ManagedFields: []metav1.ManagedFieldsEntry{
1447 {
1448 Manager: "___TestKMSv2Provider_in_k8s_io_kubernetes_test_integration_controlplane_transformation.test",
1449 Operation: "Update",
1450 APIVersion: "v1",
1451 Time: &dekSourceAESGCMKeyTime,
1452 FieldsType: "FieldsV1",
1453 FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:data":{".":{},"f:api_key":{}},"f:type":{}}`)},
1454 },
1455 },
1456 },
1457 Data: map[string][]byte{
1458 secretKey: []byte(secretVal),
1459 },
1460 Type: corev1.SecretTypeOpaque,
1461 },
1462 {
1463 ObjectMeta: metav1.ObjectMeta{
1464 Name: dekSourceHKDFSHA256XNonceAESGCMSeedName,
1465 Namespace: legacyDataNamespace,
1466 UID: "87c514b4-9c26-4041-ad0d-0d07dca557ed",
1467 CreationTimestamp: dekSourceHKDFSHA256XNonceAESGCMSeedTime,
1468 ManagedFields: []metav1.ManagedFieldsEntry{
1469 {
1470 Manager: "___TestKMSv2Provider_extended_nonce_gcm_in_k8s_io_kubernetes_test_integration_controlplane_transformation.test",
1471 Operation: "Update",
1472 APIVersion: "v1",
1473 Time: &dekSourceHKDFSHA256XNonceAESGCMSeedTime,
1474 FieldsType: "FieldsV1",
1475 FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:data":{".":{},"f:api_key":{}},"f:type":{}}`)},
1476 },
1477 },
1478 },
1479 Data: map[string][]byte{
1480 secretKey: []byte(secretVal),
1481 },
1482 Type: corev1.SecretTypeOpaque,
1483 },
1484 },
1485 }
1486
1487 if diff := cmp.Diff(expectedLegacySecrets, legacySecrets,
1488
1489 cmpopts.IgnoreFields(corev1.Secret{}, "ResourceVersion"),
1490 cmpopts.IgnoreFields(metav1.ListMeta{}, "ResourceVersion"),
1491 ); len(diff) > 0 {
1492 t.Errorf("kms v2 legacy secret data diff (-want, +got):\n%s", diff)
1493 }
1494 }
1495
View as plain text