mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 03:11:40 +00:00
This change updates KMS v2 to not create a new DEK for every encryption. Instead, we re-use the DEK while the key ID is stable. Specifically: We no longer use a random 12 byte nonce per encryption. Instead, we use both a random 4 byte nonce and an 8 byte nonce set via an atomic counter. Since each DEK is randomly generated and never re-used, the combination of DEK and counter are always unique. Thus there can never be a nonce collision. AES GCM strongly encourages the use of a 12 byte nonce, hence the additional 4 byte random nonce. We could leave those 4 bytes set to all zeros, but there is no harm in setting them to random data (it may help in some edge cases such as live VM migration). If the plugin is not healthy, the last DEK will be used for encryption for up to three minutes (there is no difference on the behavior of reads which have always used the DEK cache). This will reduce the impact of a short plugin outage while making it easy to perform storage migration after a key ID change (i.e. simply wait ten minutes after the key ID change before starting the migration). The DEK rotation cycle is performed in sync with the KMS v2 status poll thus we always have the correct information to determine if a read is stale in regards to storage migration. Signed-off-by: Monis Khan <mok@microsoft.com>
187 lines
5.0 KiB
Go
187 lines
5.0 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package transformation
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"testing"
|
|
|
|
apiserverconfigv1 "k8s.io/apiserver/pkg/apis/config/v1"
|
|
"k8s.io/apiserver/pkg/storage/value"
|
|
aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
|
|
)
|
|
|
|
const (
|
|
aesGCMPrefix = "k8s:enc:aesgcm:v1:key1:"
|
|
aesCBCPrefix = "k8s:enc:aescbc:v1:key1:"
|
|
|
|
aesGCMConfigYAML = `
|
|
kind: EncryptionConfiguration
|
|
apiVersion: apiserver.config.k8s.io/v1
|
|
resources:
|
|
- resources:
|
|
- secrets
|
|
providers:
|
|
- aesgcm:
|
|
keys:
|
|
- name: key1
|
|
secret: c2VjcmV0IGlzIHNlY3VyZQ==
|
|
`
|
|
|
|
aesCBCConfigYAML = `
|
|
kind: EncryptionConfiguration
|
|
apiVersion: apiserver.config.k8s.io/v1
|
|
resources:
|
|
- resources:
|
|
- secrets
|
|
providers:
|
|
- aescbc:
|
|
keys:
|
|
- name: key1
|
|
secret: c2VjcmV0IGlzIHNlY3VyZQ==
|
|
`
|
|
|
|
identityConfigYAML = `
|
|
kind: EncryptionConfiguration
|
|
apiVersion: apiserver.config.k8s.io/v1
|
|
resources:
|
|
- resources:
|
|
- secrets
|
|
providers:
|
|
- identity: {}
|
|
`
|
|
)
|
|
|
|
// TestSecretsShouldBeEnveloped is an integration test between KubeAPI and etcd that checks:
|
|
// 1. Secrets are encrypted on write
|
|
// 2. Secrets are decrypted on read
|
|
// when EncryptionConfiguration is passed to KubeAPI server.
|
|
func TestSecretsShouldBeTransformed(t *testing.T) {
|
|
var testCases = []struct {
|
|
transformerConfigContent string
|
|
transformerPrefix string
|
|
unSealFunc unSealSecret
|
|
}{
|
|
{aesGCMConfigYAML, aesGCMPrefix, unSealWithGCMTransformer},
|
|
{aesCBCConfigYAML, aesCBCPrefix, unSealWithCBCTransformer},
|
|
// TODO: add secretbox
|
|
}
|
|
for _, tt := range testCases {
|
|
test, err := newTransformTest(t, tt.transformerConfigContent, false, "")
|
|
if err != nil {
|
|
test.cleanUp()
|
|
t.Errorf("failed to setup test for envelop %s, error was %v", tt.transformerPrefix, err)
|
|
continue
|
|
}
|
|
test.secret, err = test.createSecret(testSecret, testNamespace)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test secret, error: %v", err)
|
|
}
|
|
test.runResource(test.logger, tt.unSealFunc, tt.transformerPrefix, "", "v1", "secrets", test.secret.Name, test.secret.Namespace)
|
|
test.cleanUp()
|
|
}
|
|
}
|
|
|
|
// Baseline (no enveloping) - use to contrast with enveloping benchmarks.
|
|
func BenchmarkBase(b *testing.B) {
|
|
runBenchmark(b, "")
|
|
}
|
|
|
|
// Identity transformer is a NOOP (crypto-wise) - use to contrast with AESGCM and AESCBC benchmark results.
|
|
func BenchmarkIdentityWrite(b *testing.B) {
|
|
runBenchmark(b, identityConfigYAML)
|
|
}
|
|
|
|
func BenchmarkAESGCMEnvelopeWrite(b *testing.B) {
|
|
runBenchmark(b, aesGCMConfigYAML)
|
|
}
|
|
|
|
func BenchmarkAESCBCEnvelopeWrite(b *testing.B) {
|
|
runBenchmark(b, aesCBCConfigYAML)
|
|
}
|
|
|
|
func runBenchmark(b *testing.B, transformerConfig string) {
|
|
b.StopTimer()
|
|
test, err := newTransformTest(b, transformerConfig, false, "")
|
|
defer test.cleanUp()
|
|
if err != nil {
|
|
b.Fatalf("failed to setup benchmark for config %s, error was %v", transformerConfig, err)
|
|
}
|
|
|
|
b.StartTimer()
|
|
test.benchmark(b)
|
|
b.StopTimer()
|
|
test.printMetrics()
|
|
}
|
|
|
|
func unSealWithGCMTransformer(ctx context.Context, cipherText []byte, dataCtx value.Context,
|
|
transformerConfig apiserverconfigv1.ProviderConfiguration) ([]byte, error) {
|
|
|
|
block, err := newAESCipher(transformerConfig.AESGCM.Keys[0].Secret)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create block cipher: %v", err)
|
|
}
|
|
|
|
gcmTransformer, err := aestransformer.NewGCMTransformer(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create transformer from block: %v", err)
|
|
}
|
|
|
|
clearText, _, err := gcmTransformer.TransformFromStorage(ctx, cipherText, dataCtx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decypt secret: %v", err)
|
|
}
|
|
|
|
return clearText, nil
|
|
}
|
|
|
|
func unSealWithCBCTransformer(ctx context.Context, cipherText []byte, dataCtx value.Context,
|
|
transformerConfig apiserverconfigv1.ProviderConfiguration) ([]byte, error) {
|
|
|
|
block, err := newAESCipher(transformerConfig.AESCBC.Keys[0].Secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cbcTransformer := aestransformer.NewCBCTransformer(block)
|
|
|
|
clearText, _, err := cbcTransformer.TransformFromStorage(ctx, cipherText, dataCtx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decypt secret: %v", err)
|
|
}
|
|
|
|
return clearText, nil
|
|
}
|
|
|
|
func newAESCipher(key string) (cipher.Block, error) {
|
|
k, err := base64.StdEncoding.DecodeString(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode config secret: %v", err)
|
|
}
|
|
|
|
block, err := aes.NewCipher(k)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %v", err)
|
|
}
|
|
|
|
return block, nil
|
|
}
|