mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
Enable encryption for custom resources
Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
This commit is contained in:
parent
e287b36cc1
commit
c3df726c7b
@ -83,14 +83,17 @@ func createAPIExtensionsConfig(
|
|||||||
apiextensionsapiserver.Scheme); err != nil {
|
apiextensionsapiserver.Scheme); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
crdRESTOptionsGetter, err := apiextensionsoptions.NewCRDRESTOptionsGetter(etcdOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
apiextensionsConfig := &apiextensionsapiserver.Config{
|
apiextensionsConfig := &apiextensionsapiserver.Config{
|
||||||
GenericConfig: &genericapiserver.RecommendedConfig{
|
GenericConfig: &genericapiserver.RecommendedConfig{
|
||||||
Config: genericConfig,
|
Config: genericConfig,
|
||||||
SharedInformerFactory: externalInformers,
|
SharedInformerFactory: externalInformers,
|
||||||
},
|
},
|
||||||
ExtraConfig: apiextensionsapiserver.ExtraConfig{
|
ExtraConfig: apiextensionsapiserver.ExtraConfig{
|
||||||
CRDRESTOptionsGetter: apiextensionsoptions.NewCRDRESTOptionsGetter(etcdOptions),
|
CRDRESTOptionsGetter: crdRESTOptionsGetter,
|
||||||
MasterCount: masterCount,
|
MasterCount: masterCount,
|
||||||
AuthResolverWrapper: authResolverWrapper,
|
AuthResolverWrapper: authResolverWrapper,
|
||||||
ServiceResolver: serviceResolver,
|
ServiceResolver: serviceResolver,
|
||||||
|
@ -70,10 +70,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||||
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
|
||||||
genericfilters "k8s.io/apiserver/pkg/server/filters"
|
genericfilters "k8s.io/apiserver/pkg/server/filters"
|
||||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
|
||||||
flowcontrolrequest "k8s.io/apiserver/pkg/util/flowcontrol/request"
|
|
||||||
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
|
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/apiserver/pkg/warning"
|
"k8s.io/apiserver/pkg/warning"
|
||||||
@ -1134,33 +1131,6 @@ func (d unstructuredDefaulter) Default(in runtime.Object) {
|
|||||||
structuraldefaulting.Default(u.UnstructuredContent(), d.structuralSchemas[u.GetObjectKind().GroupVersionKind().Version])
|
structuraldefaulting.Default(u.UnstructuredContent(), d.structuralSchemas[u.GetObjectKind().GroupVersionKind().Version])
|
||||||
}
|
}
|
||||||
|
|
||||||
type CRDRESTOptionsGetter struct {
|
|
||||||
StorageConfig storagebackend.Config
|
|
||||||
StoragePrefix string
|
|
||||||
EnableWatchCache bool
|
|
||||||
DefaultWatchCacheSize int
|
|
||||||
EnableGarbageCollection bool
|
|
||||||
DeleteCollectionWorkers int
|
|
||||||
CountMetricPollPeriod time.Duration
|
|
||||||
StorageObjectCountTracker flowcontrolrequest.StorageObjectCountTracker
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t CRDRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
|
|
||||||
ret := generic.RESTOptions{
|
|
||||||
StorageConfig: t.StorageConfig.ForResource(resource),
|
|
||||||
Decorator: generic.UndecoratedStorage,
|
|
||||||
EnableGarbageCollection: t.EnableGarbageCollection,
|
|
||||||
DeleteCollectionWorkers: t.DeleteCollectionWorkers,
|
|
||||||
ResourcePrefix: resource.Group + "/" + resource.Resource,
|
|
||||||
CountMetricPollPeriod: t.CountMetricPollPeriod,
|
|
||||||
StorageObjectCountTracker: t.StorageObjectCountTracker,
|
|
||||||
}
|
|
||||||
if t.EnableWatchCache {
|
|
||||||
ret.Decorator = genericregistry.StorageWithCacher()
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// clone returns a clone of the provided crdStorageMap.
|
// clone returns a clone of the provided crdStorageMap.
|
||||||
// The clone is a shallow copy of the map.
|
// The clone is a shallow copy of the map.
|
||||||
func (in crdStorageMap) clone() crdStorageMap {
|
func (in crdStorageMap) clone() crdStorageMap {
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
oteltrace "go.opentelemetry.io/otel/trace"
|
oteltrace "go.opentelemetry.io/otel/trace"
|
||||||
@ -106,11 +107,14 @@ func (o CustomResourceDefinitionsServerOptions) Config() (*apiserver.Config, err
|
|||||||
if err := o.APIEnablement.ApplyTo(&serverConfig.Config, apiserver.DefaultAPIResourceConfigSource(), apiserver.Scheme); err != nil {
|
if err := o.APIEnablement.ApplyTo(&serverConfig.Config, apiserver.DefaultAPIResourceConfigSource(), apiserver.Scheme); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
crdRESTOptionsGetter, err := NewCRDRESTOptionsGetter(*o.RecommendedOptions.Etcd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
config := &apiserver.Config{
|
config := &apiserver.Config{
|
||||||
GenericConfig: serverConfig,
|
GenericConfig: serverConfig,
|
||||||
ExtraConfig: apiserver.ExtraConfig{
|
ExtraConfig: apiserver.ExtraConfig{
|
||||||
CRDRESTOptionsGetter: NewCRDRESTOptionsGetter(*o.RecommendedOptions.Etcd),
|
CRDRESTOptionsGetter: crdRESTOptionsGetter,
|
||||||
ServiceResolver: &serviceResolver{serverConfig.SharedInformerFactory.Core().V1().Services().Lister()},
|
ServiceResolver: &serviceResolver{serverConfig.SharedInformerFactory.Core().V1().Services().Lister()},
|
||||||
AuthResolverWrapper: webhook.NewDefaultAuthenticationInfoResolverWrapper(nil, nil, serverConfig.LoopbackClientConfig, oteltrace.NewNoopTracerProvider()),
|
AuthResolverWrapper: webhook.NewDefaultAuthenticationInfoResolverWrapper(nil, nil, serverConfig.LoopbackClientConfig, oteltrace.NewNoopTracerProvider()),
|
||||||
},
|
},
|
||||||
@ -119,20 +123,30 @@ func (o CustomResourceDefinitionsServerOptions) Config() (*apiserver.Config, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCRDRESTOptionsGetter create a RESTOptionsGetter for CustomResources.
|
// NewCRDRESTOptionsGetter create a RESTOptionsGetter for CustomResources.
|
||||||
func NewCRDRESTOptionsGetter(etcdOptions genericoptions.EtcdOptions) genericregistry.RESTOptionsGetter {
|
// This works on a copy of the etcd options so we don't mutate originals.
|
||||||
ret := apiserver.CRDRESTOptionsGetter{
|
// We assume that the input etcd options have been completed already.
|
||||||
StorageConfig: etcdOptions.StorageConfig,
|
// Avoid messing with anything outside of changes to StorageConfig as that
|
||||||
StoragePrefix: etcdOptions.StorageConfig.Prefix,
|
// may lead to unexpected behavior when the options are applied.
|
||||||
EnableWatchCache: etcdOptions.EnableWatchCache,
|
func NewCRDRESTOptionsGetter(etcdOptions genericoptions.EtcdOptions) (genericregistry.RESTOptionsGetter, error) {
|
||||||
DefaultWatchCacheSize: etcdOptions.DefaultWatchCacheSize,
|
etcdOptions.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
|
||||||
EnableGarbageCollection: etcdOptions.EnableGarbageCollection,
|
etcdOptions.WatchCacheSizes = nil // this control is not provided for custom resources
|
||||||
DeleteCollectionWorkers: etcdOptions.DeleteCollectionWorkers,
|
etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
|
||||||
CountMetricPollPeriod: etcdOptions.StorageConfig.CountMetricPollPeriod,
|
|
||||||
StorageObjectCountTracker: etcdOptions.StorageConfig.StorageObjectCountTracker,
|
|
||||||
}
|
|
||||||
ret.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
|
|
||||||
|
|
||||||
return ret
|
// creates a generic apiserver config for etcdOptions to mutate
|
||||||
|
c := genericapiserver.Config{}
|
||||||
|
if err := etcdOptions.ApplyTo(&c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
restOptionsGetter := c.RESTOptionsGetter
|
||||||
|
if restOptionsGetter == nil {
|
||||||
|
return nil, fmt.Errorf("server.Config RESTOptionsGetter should not be nil")
|
||||||
|
}
|
||||||
|
// sanity check that no other fields are set
|
||||||
|
c.RESTOptionsGetter = nil
|
||||||
|
if !reflect.DeepEqual(c, genericapiserver.Config{}) {
|
||||||
|
return nil, fmt.Errorf("only RESTOptionsGetter should have been mutated in server.Config")
|
||||||
|
}
|
||||||
|
return restOptionsGetter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceResolver struct {
|
type serviceResolver struct {
|
||||||
|
@ -182,7 +182,10 @@ func testWebhookConverter(t *testing.T, watchCache bool) {
|
|||||||
|
|
||||||
crd := multiVersionFixture.DeepCopy()
|
crd := multiVersionFixture.DeepCopy()
|
||||||
|
|
||||||
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
RESTOptionsGetter, err := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
|
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -658,7 +658,10 @@ func TestCustomResourceDefaultingOfMetaFields(t *testing.T) {
|
|||||||
t.Logf("CR created: %#v", returnedFoo.UnstructuredContent())
|
t.Logf("CR created: %#v", returnedFoo.UnstructuredContent())
|
||||||
|
|
||||||
// get persisted object
|
// get persisted object
|
||||||
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
RESTOptionsGetter, err := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
|
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -144,7 +144,10 @@ func StartDefaultServerWithClientsAndEtcd(t servertesting.Logger, extraFlags ...
|
|||||||
return nil, nil, nil, nil, "", err
|
return nil, nil, nil, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
RESTOptionsGetter, err := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, "", err
|
||||||
|
}
|
||||||
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: "hopefully-ignored-group", Resource: "hopefully-ignored-resources"})
|
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: "hopefully-ignored-group", Resource: "hopefully-ignored-resources"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, "", err
|
return nil, nil, nil, nil, "", err
|
||||||
|
@ -132,7 +132,10 @@ func TestInvalidObjectMetaInStorage(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
RESTOptionsGetter, err := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: noxuDefinition.Spec.Group, Resource: noxuDefinition.Spec.Names.Plural})
|
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: noxuDefinition.Spec.Group, Resource: noxuDefinition.Spec.Names.Plural})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package transformation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/kubernetes/test/integration/etcd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createResources(t *testing.T, test *transformTest,
|
||||||
|
group,
|
||||||
|
version,
|
||||||
|
kind,
|
||||||
|
resource,
|
||||||
|
name,
|
||||||
|
namespace string,
|
||||||
|
) {
|
||||||
|
switch resource {
|
||||||
|
case "pods":
|
||||||
|
_, err := test.createPod(namespace, dynamic.NewForConfigOrDie(test.kubeAPIServer.ClientConfig))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test pod, error: %v, name: %s, ns: %s", err, name, namespace)
|
||||||
|
}
|
||||||
|
case "configmaps":
|
||||||
|
_, err := test.createConfigMap(name, namespace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test configmap, error: %v, name: %s, ns: %s", err, name, namespace)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
gvr := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
|
||||||
|
data := etcd.GetEtcdStorageData()[gvr]
|
||||||
|
stub := data.Stub
|
||||||
|
dynamicClient, obj, err := etcd.JSONToUnstructured(stub, namespace, &meta.RESTMapping{
|
||||||
|
Resource: gvr,
|
||||||
|
GroupVersionKind: gvr.GroupVersion().WithKind(kind),
|
||||||
|
Scope: meta.RESTScopeRoot,
|
||||||
|
}, dynamic.NewForConfigOrDie(test.kubeAPIServer.ClientConfig))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = dynamicClient.Create(ctx, obj, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := dynamicClient.Get(ctx, obj.GetName(), metav1.GetOptions{}); err != nil {
|
||||||
|
t.Fatalf("object should exist: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptSupportedForAllResourceTypes(t *testing.T) {
|
||||||
|
// check resources provided by the three servers that we have wired together
|
||||||
|
// - pods and configmaps from KAS
|
||||||
|
// - CRDs and CRs from API extensions
|
||||||
|
// - API services from aggregator
|
||||||
|
encryptionConfig := `
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- pods
|
||||||
|
- configmaps
|
||||||
|
- customresourcedefinitions.apiextensions.k8s.io
|
||||||
|
- pandas.awesome.bears.com
|
||||||
|
- apiservices.apiregistration.k8s.io
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: c2VjcmV0IGlzIHNlY3VyZQ==
|
||||||
|
`
|
||||||
|
|
||||||
|
test, err := newTransformTest(t, encryptionConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start Kube API Server with encryptionConfig\n %s, error: %v", encryptionConfig, err)
|
||||||
|
}
|
||||||
|
t.Cleanup(test.cleanUp)
|
||||||
|
|
||||||
|
// the storage registry for CRs is dynamic so create one to exercise the wiring
|
||||||
|
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(test.kubeAPIServer.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
group string
|
||||||
|
version string
|
||||||
|
kind string
|
||||||
|
resource string
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
}{
|
||||||
|
{"", "v1", "ConfigMap", "configmaps", "cm1", testNamespace},
|
||||||
|
{"apiextensions.k8s.io", "v1", "CustomResourceDefinition", "customresourcedefinitions", "pandas.awesome.bears.com", ""},
|
||||||
|
{"awesome.bears.com", "v1", "Panda", "pandas", "cr3panda", ""},
|
||||||
|
{"apiregistration.k8s.io", "v1", "APIService", "apiservices", "as2.foo.com", ""},
|
||||||
|
{"", "v1", "Pod", "pods", "pod1", testNamespace},
|
||||||
|
} {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.resource, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
createResources(t, test, tt.group, tt.version, tt.kind, tt.resource, tt.name, tt.namespace)
|
||||||
|
test.runResource(t, unSealWithCBCTransformer, aesCBCPrefix, tt.group, tt.version, tt.resource, tt.name, tt.namespace)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -145,7 +145,7 @@ resources:
|
|||||||
// Since Data Encryption Key (DEK) is randomly generated (per encryption operation), we need to ask KMS Mock for it.
|
// Since Data Encryption Key (DEK) is randomly generated (per encryption operation), we need to ask KMS Mock for it.
|
||||||
plainTextDEK := pluginMock.LastEncryptRequest()
|
plainTextDEK := pluginMock.LastEncryptRequest()
|
||||||
|
|
||||||
secretETCDPath := test.getETCDPath()
|
secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace)
|
||||||
rawEnvelope, err := test.getRawSecretFromETCD()
|
rawEnvelope, err := test.getRawSecretFromETCD()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
|
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
|
||||||
|
@ -154,7 +154,7 @@ resources:
|
|||||||
// Since Data Encryption Key (DEK) is randomly generated (per encryption operation), we need to ask KMS Mock for it.
|
// Since Data Encryption Key (DEK) is randomly generated (per encryption operation), we need to ask KMS Mock for it.
|
||||||
plainTextDEK := pluginMock.LastEncryptRequest()
|
plainTextDEK := pluginMock.LastEncryptRequest()
|
||||||
|
|
||||||
secretETCDPath := test.getETCDPath()
|
secretETCDPath := test.getETCDPathForResource(test.storageConfig.Prefix, "", "secrets", test.secret.Name, test.secret.Namespace)
|
||||||
rawEnvelope, err := test.getRawSecretFromETCD()
|
rawEnvelope, err := test.getRawSecretFromETCD()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
|
t.Fatalf("failed to read %s from etcd: %v", secretETCDPath, err)
|
||||||
|
@ -95,7 +95,7 @@ func TestSecretsShouldBeTransformed(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create test secret, error: %v", err)
|
t.Fatalf("Failed to create test secret, error: %v", err)
|
||||||
}
|
}
|
||||||
test.run(tt.unSealFunc, tt.transformerPrefix)
|
test.runResource(test.logger, tt.unSealFunc, tt.transformerPrefix, "", "v1", "secrets", test.secret.Name, test.secret.Namespace)
|
||||||
test.cleanUp()
|
test.cleanUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package transformation
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -34,12 +35,16 @@ import (
|
|||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
apiserverconfigv1 "k8s.io/apiserver/pkg/apis/config/v1"
|
apiserverconfigv1 "k8s.io/apiserver/pkg/apis/config/v1"
|
||||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
"k8s.io/apiserver/pkg/storage/value"
|
"k8s.io/apiserver/pkg/storage/value"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
"k8s.io/kubernetes/test/integration"
|
"k8s.io/kubernetes/test/integration"
|
||||||
|
"k8s.io/kubernetes/test/integration/etcd"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -50,6 +55,8 @@ const (
|
|||||||
testNamespace = "secret-encryption-test"
|
testNamespace = "secret-encryption-test"
|
||||||
testSecret = "test-secret"
|
testSecret = "test-secret"
|
||||||
metricsPrefix = "apiserver_storage_"
|
metricsPrefix = "apiserver_storage_"
|
||||||
|
configMapKey = "foo"
|
||||||
|
configMapVal = "bar"
|
||||||
|
|
||||||
// precomputed key and secret for use with AES CBC
|
// precomputed key and secret for use with AES CBC
|
||||||
// this looks exactly the same as the AES GCM secret but with a different value
|
// this looks exactly the same as the AES GCM secret but with a different value
|
||||||
@ -107,44 +114,88 @@ func (e *transformTest) cleanUp() {
|
|||||||
e.kubeAPIServer.TearDownFn()
|
e.kubeAPIServer.TearDownFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *transformTest) run(unSealSecretFunc unSealSecret, expectedEnvelopePrefix string) {
|
func (e *transformTest) runResource(l kubeapiservertesting.Logger, unSealSecretFunc unSealSecret, expectedEnvelopePrefix,
|
||||||
response, err := e.readRawRecordFromETCD(e.getETCDPath())
|
group,
|
||||||
|
version,
|
||||||
|
resource,
|
||||||
|
name,
|
||||||
|
namespaceName string,
|
||||||
|
) {
|
||||||
|
response, err := e.readRawRecordFromETCD(e.getETCDPathForResource(e.storageConfig.Prefix, group, resource, name, namespaceName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.logger.Errorf("failed to read from etcd: %v", err)
|
l.Errorf("failed to read from etcd: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.HasPrefix(response.Kvs[0].Value, []byte(expectedEnvelopePrefix)) {
|
if !bytes.HasPrefix(response.Kvs[0].Value, []byte(expectedEnvelopePrefix)) {
|
||||||
e.logger.Errorf("expected secret to be prefixed with %s, but got %s",
|
l.Errorf("expected data to be prefixed with %s, but got %s",
|
||||||
expectedEnvelopePrefix, response.Kvs[0].Value)
|
expectedEnvelopePrefix, response.Kvs[0].Value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// etcd path of the key is used as the authenticated context - need to pass it to decrypt
|
// etcd path of the key is used as the authenticated context - need to pass it to decrypt
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dataCtx := value.DefaultContext([]byte(e.getETCDPath()))
|
dataCtx := value.DefaultContext(e.getETCDPathForResource(e.storageConfig.Prefix, group, resource, name, namespaceName))
|
||||||
// Envelope header precedes the cipherTextPayload
|
// Envelope header precedes the cipherTextPayload
|
||||||
sealedData := response.Kvs[0].Value[len(expectedEnvelopePrefix):]
|
sealedData := response.Kvs[0].Value[len(expectedEnvelopePrefix):]
|
||||||
transformerConfig, err := e.getEncryptionConfig()
|
transformerConfig, err := e.getEncryptionConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.logger.Errorf("failed to parse transformer config: %v", err)
|
l.Errorf("failed to parse transformer config: %v", err)
|
||||||
}
|
}
|
||||||
v, err := unSealSecretFunc(ctx, sealedData, dataCtx, *transformerConfig)
|
v, err := unSealSecretFunc(ctx, sealedData, dataCtx, *transformerConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.logger.Errorf("failed to unseal secret: %v", err)
|
l.Errorf("failed to unseal secret: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(v), secretVal) {
|
if resource == "secrets" {
|
||||||
e.logger.Errorf("expected %q after decryption, but got %q", secretVal, string(v))
|
if !strings.Contains(string(v), secretVal) {
|
||||||
|
l.Errorf("expected %q after decryption, but got %q", secretVal, string(v))
|
||||||
|
}
|
||||||
|
} else if resource == "configmaps" {
|
||||||
|
if !strings.Contains(string(v), configMapVal) {
|
||||||
|
l.Errorf("expected %q after decryption, but got %q", configMapVal, string(v))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(string(v), name) {
|
||||||
|
l.Errorf("expected %q after decryption, but got %q", name, string(v))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secrets should be un-enveloped on direct reads from Kube API Server.
|
// Data should be un-enveloped on direct reads from Kube API Server.
|
||||||
s, err := e.restClient.CoreV1().Secrets(testNamespace).Get(context.TODO(), testSecret, metav1.GetOptions{})
|
if resource == "secrets" {
|
||||||
if err != nil {
|
s, err := e.restClient.CoreV1().Secrets(testNamespace).Get(context.TODO(), testSecret, metav1.GetOptions{})
|
||||||
e.logger.Errorf("failed to get Secret from %s, err: %v", testNamespace, err)
|
if err != nil {
|
||||||
}
|
l.Fatalf("failed to get Secret from %s, err: %v", testNamespace, err)
|
||||||
if secretVal != string(s.Data[secretKey]) {
|
}
|
||||||
e.logger.Errorf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
|
if secretVal != string(s.Data[secretKey]) {
|
||||||
|
l.Errorf("expected %s from KubeAPI, but got %s", secretVal, string(s.Data[secretKey]))
|
||||||
|
}
|
||||||
|
} else if resource == "configmaps" {
|
||||||
|
s, err := e.restClient.CoreV1().ConfigMaps(namespaceName).Get(context.TODO(), name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalf("failed to get ConfigMap from %s, err: %v", namespaceName, err)
|
||||||
|
}
|
||||||
|
if configMapVal != string(s.Data[configMapKey]) {
|
||||||
|
l.Errorf("expected %s from KubeAPI, but got %s", configMapVal, string(s.Data[configMapKey]))
|
||||||
|
}
|
||||||
|
} else if resource == "pods" {
|
||||||
|
p, err := e.restClient.CoreV1().Pods(namespaceName).Get(context.TODO(), name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalf("failed to get Pod from %s, err: %v", namespaceName, err)
|
||||||
|
}
|
||||||
|
if p.Name != name {
|
||||||
|
l.Errorf("expected %s from KubeAPI, but got %s", name, p.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.Logf("Get object with dynamic client")
|
||||||
|
fooResource := schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
|
||||||
|
obj, err := dynamic.NewForConfigOrDie(e.kubeAPIServer.ClientConfig).Resource(fooResource).Namespace(namespaceName).Get(context.TODO(), name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalf("Failed to get test instance: %v, name: %s", err, name)
|
||||||
|
}
|
||||||
|
if obj.GetObjectKind().GroupVersionKind().Group == group && obj.GroupVersionKind().Version == version && obj.GetKind() == resource && obj.GetNamespace() == namespaceName && obj.GetName() != name {
|
||||||
|
l.Errorf("expected %s from KubeAPI, but got %s", name, obj.GetName())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,12 +208,19 @@ func (e *transformTest) benchmark(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *transformTest) getETCDPath() string {
|
func (e *transformTest) getETCDPathForResource(storagePrefix, group, resource, name, namespaceName string) string {
|
||||||
return fmt.Sprintf("/%s/secrets/%s/%s", e.storageConfig.Prefix, e.ns.Name, e.secret.Name)
|
groupResource := resource
|
||||||
|
if group != "" {
|
||||||
|
groupResource = fmt.Sprintf("%s/%s", group, resource)
|
||||||
|
}
|
||||||
|
if namespaceName == "" {
|
||||||
|
return fmt.Sprintf("/%s/%s/%s", storagePrefix, groupResource, name)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("/%s/%s/%s/%s", storagePrefix, groupResource, namespaceName, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *transformTest) getRawSecretFromETCD() ([]byte, error) {
|
func (e *transformTest) getRawSecretFromETCD() ([]byte, error) {
|
||||||
secretETCDPath := e.getETCDPath()
|
secretETCDPath := e.getETCDPathForResource(e.storageConfig.Prefix, "", "secrets", e.secret.Name, e.secret.Namespace)
|
||||||
etcdResponse, err := e.readRawRecordFromETCD(secretETCDPath)
|
etcdResponse, err := e.readRawRecordFromETCD(secretETCDPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read %s from etcd: %v", secretETCDPath, err)
|
return nil, fmt.Errorf("failed to read %s from etcd: %v", secretETCDPath, err)
|
||||||
@ -172,7 +230,9 @@ func (e *transformTest) getRawSecretFromETCD() ([]byte, error) {
|
|||||||
|
|
||||||
func (e *transformTest) getEncryptionOptions() []string {
|
func (e *transformTest) getEncryptionOptions() []string {
|
||||||
if e.transformerConfig != "" {
|
if e.transformerConfig != "" {
|
||||||
return []string{"--encryption-provider-config", path.Join(e.configDir, encryptionConfigFileName)}
|
return []string{
|
||||||
|
"--encryption-provider-config", path.Join(e.configDir, encryptionConfigFileName),
|
||||||
|
"--disable-admission-plugins", "ServiceAccount"}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -235,6 +295,60 @@ func (e *transformTest) createSecret(name, namespace string) (*corev1.Secret, er
|
|||||||
return secret, nil
|
return secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *transformTest) createConfigMap(name, namespace string) (*corev1.ConfigMap, error) {
|
||||||
|
cm := &corev1.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Data: map[string]string{
|
||||||
|
configMapKey: configMapVal,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := e.restClient.CoreV1().ConfigMaps(cm.Namespace).Create(context.TODO(), cm, metav1.CreateOptions{}); err != nil {
|
||||||
|
return nil, fmt.Errorf("error while writing configmap: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gvr(group, version, resource string) schema.GroupVersionResource {
|
||||||
|
return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createResource(client dynamic.Interface, gvr schema.GroupVersionResource, ns string) (*unstructured.Unstructured, error) {
|
||||||
|
stubObj, err := getStubObj(gvr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.Resource(gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStubObj(gvr schema.GroupVersionResource) (*unstructured.Unstructured, error) {
|
||||||
|
stub := ""
|
||||||
|
if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok {
|
||||||
|
stub = data.Stub
|
||||||
|
}
|
||||||
|
if len(stub) == 0 {
|
||||||
|
return nil, fmt.Errorf("no stub data for %#v", gvr)
|
||||||
|
}
|
||||||
|
|
||||||
|
stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||||
|
if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err)
|
||||||
|
}
|
||||||
|
return stubObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *transformTest) createPod(namespace string, dynamicInterface dynamic.Interface) (*unstructured.Unstructured, error) {
|
||||||
|
podGVR := gvr("", "v1", "pods")
|
||||||
|
pod, err := createResource(dynamicInterface, podGVR, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error while writing pod: %v", err)
|
||||||
|
}
|
||||||
|
return pod, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *transformTest) readRawRecordFromETCD(path string) (*clientv3.GetResponse, error) {
|
func (e *transformTest) readRawRecordFromETCD(path string) (*clientv3.GetResponse, error) {
|
||||||
rawClient, etcdClient, err := integration.GetEtcdClients(e.kubeAPIServer.ServerOpts.Etcd.StorageConfig.Transport)
|
rawClient, etcdClient, err := integration.GetEtcdClients(e.kubeAPIServer.ServerOpts.Etcd.StorageConfig.Transport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user