Merge pull request #59824 from immutableT/kms-integration-test

Automatic merge from submit-queue (batch tested with PRs 54191, 59374, 59824, 55032, 59906). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Extracting common logic related to integration testing of storage transforms

**What this PR does / why we need it**:
This is a pre-staging/refactoring PR for a larger PR that adds integration test for gRPC KMS Plugin.
Concretely, this PR extracts the common logic necessary to setup and run integration tests for storage transforms.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:
Envelope Transform (KMS Plugin Integration tests) leverages this common logic, and will be submitted after this PR.

**Release note**:

```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2018-02-21 19:40:37 -08:00 committed by GitHub
commit d32c1d7bf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 440 additions and 386 deletions

View File

@ -2,6 +2,7 @@ package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
@ -12,9 +13,10 @@ go_test(
"crd_test.go",
"kube_apiserver_test.go",
"main_test.go",
"secrets_enveloping_test.go",
"secrets_transformation_test.go",
"synthetic_master_test.go",
],
embed = [":go_default_library"],
tags = ["integration"],
deps = [
"//cmd/kube-apiserver/app/testing:go_default_library",
@ -24,7 +26,6 @@ go_test(
"//pkg/master:go_default_library",
"//test/integration:go_default_library",
"//test/integration/framework:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
"//vendor/k8s.io/api/apps/v1beta1:go_default_library",
@ -45,7 +46,6 @@ go_test(
"//vendor/k8s.io/apiserver/pkg/authorization/authorizerfactory:go_default_library",
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/options/encryptionconfig:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/aes:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
@ -71,3 +71,24 @@ filegroup(
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = [
"transformation_testcase.go",
],
importpath = "k8s.io/kubernetes/test/integration/master",
deps = [
"//cmd/kube-apiserver/app/testing:go_default_library",
"//test/integration:go_default_library",
"//test/integration/framework:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/options/encryptionconfig:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
],
)

View File

@ -1,383 +0,0 @@
/*
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 master
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"testing"
"github.com/coreos/etcd/clientv3"
"github.com/ghodss/yaml"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/server/options/encryptionconfig"
"k8s.io/apiserver/pkg/storage/storagebackend"
"k8s.io/apiserver/pkg/storage/value"
aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
"k8s.io/client-go/kubernetes"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration"
"k8s.io/kubernetes/test/integration/framework"
)
const (
testNamespace = "secret-encryption-test"
testSecret = "test-secret"
encryptionConfigFileName = "encryption.conf"
aesGCMPrefix = "k8s:enc:aesgcm:v1:key1:"
aesCBCPrefix = "k8s:enc:aescbc:v1:key1:"
// Secret Data
secretKey = "api_key"
secretVal = "086a7ffc-0225-11e8-ba89-0ed5f89f718b"
aesGCMConfigYAML = `
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aesgcm:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
`
aesCBCConfigYAML = `
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
`
identityConfigYAML = `
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- identity: {}
`
)
type unSealSecret func(cipherText []byte, ctx value.Context, config encryptionconfig.ProviderConfig) ([]byte, error)
type envelopTest struct {
logger kubeapiservertesting.Logger
storageConfig *storagebackend.Config
configDir string
transformerConfig string
kubeAPIServer kubeapiservertesting.TestServer
restClient *kubernetes.Clientset
ns *corev1.Namespace
secret *corev1.Secret
}
func newEnvelopeTest(l kubeapiservertesting.Logger, transformerConfigYAML string) (*envelopTest, error) {
e := envelopTest{
logger: l,
transformerConfig: transformerConfigYAML,
storageConfig: framework.SharedEtcd(),
}
var err error
if transformerConfigYAML != "" {
if e.configDir, err = createKubeAPIServerEncryptionConfig(transformerConfigYAML); err != nil {
return nil, fmt.Errorf("error while creating KubeAPIServer encryption config: %v", err)
}
}
if e.kubeAPIServer, err = kubeapiservertesting.StartTestServer(l, e.getKubeAPIServerEncryptionOptions(), e.storageConfig); err != nil {
return nil, fmt.Errorf("failed to start KubeAPI server: %v", err)
}
if e.restClient, err = kubernetes.NewForConfig(e.kubeAPIServer.ClientConfig); err != nil {
return nil, fmt.Errorf("error while creating rest client: %v", err)
}
if e.ns, err = createTestNamespace(e.restClient, testNamespace); err != nil {
return nil, err
}
if e.secret, err = createTestSecret(e.restClient, testSecret, e.ns.Name); err != nil {
return nil, err
}
return &e, nil
}
func (e *envelopTest) cleanUp() {
os.RemoveAll(e.configDir)
e.restClient.CoreV1().Namespaces().Delete(e.ns.Name, metav1.NewDeleteOptions(0))
e.kubeAPIServer.TearDownFn()
}
func (e *envelopTest) run(unSealSecretFunc unSealSecret, expectedEnvelopePrefix string) {
response, err := readRawRecordFromETCD(&e.kubeAPIServer, e.getETCDPath())
if err != nil {
e.logger.Errorf("failed to read from etcd: %v", err)
return
}
if !bytes.HasPrefix(response.Kvs[0].Value, []byte(expectedEnvelopePrefix)) {
e.logger.Errorf("expected secret to be enveloped by %s, but got %s",
expectedEnvelopePrefix, response.Kvs[0].Value)
return
}
// etcd path of the key is used as the authenticated context - need to pass it to decrypt
ctx := value.DefaultContext([]byte(e.getETCDPath()))
// Envelope header precedes the payload
sealedData := response.Kvs[0].Value[len(expectedEnvelopePrefix):]
transformerConfig, err := parseTransformerConfig(e.transformerConfig)
if err != nil {
e.logger.Errorf("failed to parse transformer config: %v", err)
}
v, err := unSealSecretFunc(sealedData, ctx, *transformerConfig)
if err != nil {
e.logger.Errorf("failed to unseal secret: %v", err)
return
}
if !strings.Contains(string(v), secretVal) {
e.logger.Errorf("expected %q after decryption, but got %q", secretVal, string(v))
}
// Secrets should be un-enveloped on direct reads from Kube API Server.
s, err := e.restClient.CoreV1().Secrets(testNamespace).Get(testSecret, metav1.GetOptions{})
if secretVal != string(s.Data[secretKey]) {
e.logger.Errorf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
}
}
func (e *envelopTest) benchmark(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := createTestSecret(e.restClient, e.secret.Name+strconv.Itoa(i), e.ns.Name)
if err != nil {
b.Fatalf("failed to create a secret: %v", err)
}
}
}
func (e *envelopTest) getETCDPath() string {
return fmt.Sprintf("/%s/secrets/%s/%s", e.storageConfig.Prefix, e.ns.Name, e.secret.Name)
}
func (e *envelopTest) getKubeAPIServerEncryptionOptions() []string {
if e.transformerConfig != "" {
return []string{"--experimental-encryption-provider-config", path.Join(e.configDir, encryptionConfigFileName)}
}
return nil
}
// TestSecretsShouldBeEnveloped is an integration test between KubeAPI and etcd that checks:
// 1. Secrets are encrypted on write
// 2. Secrets are decrypted on read
// when EncryptionConfig is passed to KubeAPI server.
func TestSecretsShouldBeEnveloped(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 := newEnvelopeTest(t, tt.transformerConfigContent)
if err != nil {
test.cleanUp()
t.Errorf("failed to setup test for envelop %s, error was %v", tt.transformerPrefix, err)
continue
}
test.run(tt.unSealFunc, tt.transformerPrefix)
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 := newEnvelopeTest(b, transformerConfig)
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()
}
func createKubeAPIServerEncryptionConfig(transformerConfig string) (string, error) {
tempDir, err := ioutil.TempDir("", "secrets-encryption-test")
if err != nil {
return "", fmt.Errorf("failed to create temp directory: %v", err)
}
encryptionConfig := path.Join(tempDir, encryptionConfigFileName)
if err := ioutil.WriteFile(encryptionConfig, []byte(transformerConfig), 0644); err != nil {
os.RemoveAll(tempDir)
return "", fmt.Errorf("error while writing encryption config: %v", err)
}
return tempDir, nil
}
func createTestNamespace(client *kubernetes.Clientset, name string) (*corev1.Namespace, error) {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
if _, err := client.CoreV1().Namespaces().Create(ns); err != nil {
return nil, fmt.Errorf("unable to create testing namespace %v", err)
}
return ns, nil
}
func createTestSecret(client *kubernetes.Clientset, name, namespace string) (*corev1.Secret, error) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: map[string][]byte{
secretKey: []byte(secretVal),
},
}
if _, err := client.CoreV1().Secrets(secret.Namespace).Create(secret); err != nil {
return nil, fmt.Errorf("error while writing secret: %v", err)
}
return secret, nil
}
func readRawRecordFromETCD(kubeAPIServer *kubeapiservertesting.TestServer, path string) (*clientv3.GetResponse, error) {
etcdClient, err := integration.GetEtcdKVClient(kubeAPIServer.ServerOpts.Etcd.StorageConfig)
if err != nil {
return nil, fmt.Errorf("failed to create etcd client: %v", err)
}
response, err := etcdClient.Get(context.Background(), path, clientv3.WithPrefix())
if err != nil {
return nil, fmt.Errorf("failed to retrieve secret from etcd %v", err)
}
return response, nil
}
func parseTransformerConfig(configContent string) (*encryptionconfig.ProviderConfig, error) {
var config encryptionconfig.EncryptionConfig
err := yaml.Unmarshal([]byte(configContent), &config)
if err != nil {
return nil, fmt.Errorf("failed to extract transformer key: %v", err)
}
return &config.Resources[0].Providers[0], nil
}
func unSealWithGCMTransformer(cipherText []byte, ctx value.Context,
transformerConfig encryptionconfig.ProviderConfig) ([]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 := aestransformer.NewGCMTransformer(block)
clearText, _, err := gcmTransformer.TransformFromStorage(cipherText, ctx)
if err != nil {
return nil, fmt.Errorf("failed to decypt secret: %v", err)
}
return clearText, nil
}
func unSealWithCBCTransformer(cipherText []byte, ctx value.Context,
transformerConfig encryptionconfig.ProviderConfig) ([]byte, error) {
block, err := newAESCipher(transformerConfig.AESCBC.Keys[0].Secret)
if err != nil {
return nil, err
}
cbcTransformer := aestransformer.NewCBCTransformer(block)
clearText, _, err := cbcTransformer.TransformFromStorage(cipherText, ctx)
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
}

View File

@ -0,0 +1,177 @@
/*
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 master
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"testing"
"k8s.io/apiserver/pkg/server/options/encryptionconfig"
"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: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aesgcm:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
`
aesCBCConfigYAML = `
kind: EncryptionConfig
apiVersion: v1
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
`
identityConfigYAML = `
kind: EncryptionConfig
apiVersion: 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 EncryptionConfig 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)
if err != nil {
test.cleanUp()
t.Errorf("failed to setup test for envelop %s, error was %v", tt.transformerPrefix, err)
continue
}
test.run(tt.unSealFunc, tt.transformerPrefix)
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)
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()
}
func unSealWithGCMTransformer(cipherText []byte, ctx value.Context,
transformerConfig encryptionconfig.ProviderConfig) ([]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 := aestransformer.NewGCMTransformer(block)
clearText, _, err := gcmTransformer.TransformFromStorage(cipherText, ctx)
if err != nil {
return nil, fmt.Errorf("failed to decypt secret: %v", err)
}
return clearText, nil
}
func unSealWithCBCTransformer(cipherText []byte, ctx value.Context,
transformerConfig encryptionconfig.ProviderConfig) ([]byte, error) {
block, err := newAESCipher(transformerConfig.AESCBC.Keys[0].Secret)
if err != nil {
return nil, err
}
cbcTransformer := aestransformer.NewCBCTransformer(block)
clearText, _, err := cbcTransformer.TransformFromStorage(cipherText, ctx)
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
}

View File

@ -0,0 +1,239 @@
/*
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 master
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"testing"
"github.com/coreos/etcd/clientv3"
"github.com/ghodss/yaml"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/server/options/encryptionconfig"
"k8s.io/apiserver/pkg/storage/storagebackend"
"k8s.io/apiserver/pkg/storage/value"
"k8s.io/client-go/kubernetes"
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration"
"k8s.io/kubernetes/test/integration/framework"
)
const (
secretKey = "api_key"
secretVal = "086a7ffc-0225-11e8-ba89-0ed5f89f718b"
encryptionConfigFileName = "encryption.conf"
testNamespace = "secret-encryption-test"
testSecret = "test-secret"
)
type unSealSecret func(cipherText []byte, ctx value.Context, config encryptionconfig.ProviderConfig) ([]byte, error)
type transformTest struct {
logger kubeapiservertesting.Logger
storageConfig *storagebackend.Config
configDir string
transformerConfig string
kubeAPIServer kubeapiservertesting.TestServer
restClient *kubernetes.Clientset
ns *corev1.Namespace
secret *corev1.Secret
}
func newTransformTest(l kubeapiservertesting.Logger, transformerConfigYAML string) (*transformTest, error) {
e := transformTest{
logger: l,
transformerConfig: transformerConfigYAML,
storageConfig: framework.SharedEtcd(),
}
var err error
if transformerConfigYAML != "" {
if e.configDir, err = e.createEncryptionConfig(); err != nil {
return nil, fmt.Errorf("error while creating KubeAPIServer encryption config: %v", err)
}
}
if e.kubeAPIServer, err = kubeapiservertesting.StartTestServer(l, e.getEncryptionOptions(), e.storageConfig); err != nil {
return nil, fmt.Errorf("failed to start KubeAPI server: %v", err)
}
if e.restClient, err = kubernetes.NewForConfig(e.kubeAPIServer.ClientConfig); err != nil {
return nil, fmt.Errorf("error while creating rest client: %v", err)
}
if e.ns, err = e.createNamespace(testNamespace); err != nil {
return nil, err
}
if e.secret, err = e.createSecret(testSecret, e.ns.Name); err != nil {
return nil, err
}
return &e, nil
}
func (e *transformTest) cleanUp() {
os.RemoveAll(e.configDir)
e.restClient.CoreV1().Namespaces().Delete(e.ns.Name, metav1.NewDeleteOptions(0))
e.kubeAPIServer.TearDownFn()
}
func (e *transformTest) run(unSealSecretFunc unSealSecret, expectedEnvelopePrefix string) {
response, err := e.readRawRecordFromETCD(e.getETCDPath())
if err != nil {
e.logger.Errorf("failed to read from etcd: %v", err)
return
}
if !bytes.HasPrefix(response.Kvs[0].Value, []byte(expectedEnvelopePrefix)) {
e.logger.Errorf("expected secret to be prefixed with %s, but got %s",
expectedEnvelopePrefix, response.Kvs[0].Value)
return
}
// etcd path of the key is used as the authenticated context - need to pass it to decrypt
ctx := value.DefaultContext([]byte(e.getETCDPath()))
// Envelope header precedes the payload
sealedData := response.Kvs[0].Value[len(expectedEnvelopePrefix):]
transformerConfig, err := e.getEncryptionConfig()
if err != nil {
e.logger.Errorf("failed to parse transformer config: %v", err)
}
v, err := unSealSecretFunc(sealedData, ctx, *transformerConfig)
if err != nil {
e.logger.Errorf("failed to unseal secret: %v", err)
return
}
if !strings.Contains(string(v), secretVal) {
e.logger.Errorf("expected %q after decryption, but got %q", secretVal, string(v))
}
// Secrets should be un-enveloped on direct reads from Kube API Server.
s, err := e.restClient.CoreV1().Secrets(testNamespace).Get(testSecret, metav1.GetOptions{})
if secretVal != string(s.Data[secretKey]) {
e.logger.Errorf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
}
}
func (e *transformTest) benchmark(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := e.createSecret(e.secret.Name+strconv.Itoa(i), e.ns.Name)
if err != nil {
b.Fatalf("failed to create a secret: %v", err)
}
}
}
func (e *transformTest) getETCDPath() string {
return fmt.Sprintf("/%s/secrets/%s/%s", e.storageConfig.Prefix, e.ns.Name, e.secret.Name)
}
func (e *transformTest) getRawSecretFromETCD() ([]byte, error) {
secretETCDPath := e.getETCDPath()
etcdResponse, err := e.readRawRecordFromETCD(secretETCDPath)
if err != nil {
return nil, fmt.Errorf("failed to read %s from etcd: %v", secretETCDPath, err)
}
return etcdResponse.Kvs[0].Value, nil
}
func (e *transformTest) getEncryptionOptions() []string {
if e.transformerConfig != "" {
return []string{"--experimental-encryption-provider-config", path.Join(e.configDir, encryptionConfigFileName)}
}
return nil
}
func (e *transformTest) createEncryptionConfig() (string, error) {
tempDir, err := ioutil.TempDir("", "secrets-encryption-test")
if err != nil {
return "", fmt.Errorf("failed to create temp directory: %v", err)
}
encryptionConfig := path.Join(tempDir, encryptionConfigFileName)
if err := ioutil.WriteFile(encryptionConfig, []byte(e.transformerConfig), 0644); err != nil {
os.RemoveAll(tempDir)
return "", fmt.Errorf("error while writing encryption config: %v", err)
}
return tempDir, nil
}
func (e *transformTest) getEncryptionConfig() (*encryptionconfig.ProviderConfig, error) {
var config encryptionconfig.EncryptionConfig
err := yaml.Unmarshal([]byte(e.transformerConfig), &config)
if err != nil {
return nil, fmt.Errorf("failed to extract transformer key: %v", err)
}
return &config.Resources[0].Providers[0], nil
}
func (e *transformTest) createNamespace(name string) (*corev1.Namespace, error) {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
if _, err := e.restClient.CoreV1().Namespaces().Create(ns); err != nil {
return nil, fmt.Errorf("unable to create testing namespace %v", err)
}
return ns, nil
}
func (e *transformTest) createSecret(name, namespace string) (*corev1.Secret, error) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: map[string][]byte{
secretKey: []byte(secretVal),
},
}
if _, err := e.restClient.CoreV1().Secrets(secret.Namespace).Create(secret); err != nil {
return nil, fmt.Errorf("error while writing secret: %v", err)
}
return secret, nil
}
func (e *transformTest) readRawRecordFromETCD(path string) (*clientv3.GetResponse, error) {
etcdClient, err := integration.GetEtcdKVClient(e.kubeAPIServer.ServerOpts.Etcd.StorageConfig)
if err != nil {
return nil, fmt.Errorf("failed to create etcd client: %v", err)
}
response, err := etcdClient.Get(context.Background(), path, clientv3.WithPrefix())
if err != nil {
return nil, fmt.Errorf("failed to retrieve secret from etcd %v", err)
}
return response, nil
}