diff --git a/cmd/kube-apiserver/app/BUILD b/cmd/kube-apiserver/app/BUILD index e5012da611c..c746ba89194 100644 --- a/cmd/kube-apiserver/app/BUILD +++ b/cmd/kube-apiserver/app/BUILD @@ -89,6 +89,7 @@ go_library( "//vendor/k8s.io/apiserver/pkg/server/filters:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/healthz:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/options:go_default_library", + "//vendor/k8s.io/apiserver/pkg/server/options/encryptionconfig:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration:go_default_library", diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index ae52ed0f62b..1f12cf27e57 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -50,6 +50,7 @@ import ( genericregistry "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/filters" + "k8s.io/apiserver/pkg/server/options/encryptionconfig" serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/kubernetes/cmd/kube-apiserver/app/options" @@ -518,6 +519,16 @@ func BuildStorageFactory(s *options.ServerRunOptions) (*serverstorage.DefaultSto storageFactory.SetEtcdLocation(groupResource, servers) } + if s.Etcd.EncryptionProviderConfigFilepath != "" { + transformerOverrides, err := encryptionconfig.GetTransformerOverrides(s.Etcd.EncryptionProviderConfigFilepath) + if err != nil { + return nil, err + } + for groupResource, transformer := range transformerOverrides { + storageFactory.SetTransformer(groupResource, transformer) + } + } + return storageFactory, nil } diff --git a/hack/.linted_packages b/hack/.linted_packages index 59299ce5536..abf236ea6ce 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -358,6 +358,7 @@ staging/src/k8s.io/apiserver/pkg/storage/names staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes +staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity staging/src/k8s.io/apiserver/pkg/util/flushwriter staging/src/k8s.io/apiserver/pkg/util/logs staging/src/k8s.io/apiserver/plugin/pkg/audit/webhook diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/BUILD b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/BUILD new file mode 100644 index 00000000000..de403c56180 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/BUILD @@ -0,0 +1,36 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = [ + "config.go", + "types.go", + ], + tags = ["automanaged"], + deps = [ + "//vendor/github.com/ghodss/yaml:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema: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/storage/value/encrypt/identity:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["encryptionconfig_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library", + ], +) diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/config.go b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/config.go new file mode 100644 index 00000000000..4986e3c1b05 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/config.go @@ -0,0 +1,178 @@ +/* +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 encryptionconfig + +import ( + "crypto/aes" + "encoding/base64" + "fmt" + "io" + "io/ioutil" + "os" + + yaml "github.com/ghodss/yaml" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/storage/value" + aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes" + "k8s.io/apiserver/pkg/storage/value/encrypt/identity" +) + +const ( + aesTransformerPrefixV1 = "k8s:enc:aes:v1:" +) + +// GetTransformerOverrides returns the transformer overrides by reading and parsing the encryption provider configuration file +func GetTransformerOverrides(filepath string) (map[schema.GroupResource]value.Transformer, error) { + f, err := os.Open(filepath) + if err != nil { + return nil, fmt.Errorf("error opening encryption provider configuration file %q: %v", filepath, err) + } + defer f.Close() + + result, err := ParseEncryptionConfiguration(f) + if err != nil { + return nil, fmt.Errorf("error while parsing encryption provider configuration file %q: %v", filepath, err) + } + return result, nil +} + +// ParseEncryptionConfiguration parses configuration data and returns the transformer overrides +func ParseEncryptionConfiguration(f io.Reader) (map[schema.GroupResource]value.Transformer, error) { + configFileContents, err := ioutil.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("could not read contents: %v", err) + } + + var config EncryptionConfig + err = yaml.Unmarshal(configFileContents, &config) + if err != nil { + return nil, fmt.Errorf("error while parsing file: %v", err) + } + + if config.Kind != "EncryptionConfig" && config.Kind != "" { + return nil, fmt.Errorf("invalid configuration kind %q provided", config.Kind) + } + if config.Kind == "" { + return nil, fmt.Errorf("invalid configuration file, missing Kind") + } + // TODO config.APIVersion is unchecked + + resourceToPrefixTransformer := map[schema.GroupResource][]value.PrefixTransformer{} + + // For each entry in the configuration + for _, resourceConfig := range config.Resources { + transformers, err := GetPrefixTransformers(&resourceConfig) + if err != nil { + return nil, err + } + + // For each resource, create a list of providers to use + for _, resource := range resourceConfig.Resources { + gr := schema.ParseGroupResource(resource) + resourceToPrefixTransformer[gr] = append( + resourceToPrefixTransformer[gr], transformers...) + } + } + + result := map[schema.GroupResource]value.Transformer{} + for gr, transList := range resourceToPrefixTransformer { + result[gr] = value.NewMutableTransformer(value.NewPrefixTransformers(fmt.Errorf("no matching prefix found"), transList...)) + } + return result, nil +} + +// GetPrefixTransformer constructs and returns the appropriate prefix transformers for the passed resource using its configuration +func GetPrefixTransformers(config *ResourceConfig) ([]value.PrefixTransformer, error) { + var result []value.PrefixTransformer + for _, provider := range config.Providers { + found := false + + if provider.AES != nil { + transformer, err := GetAESPrefixTransformer(provider.AES) + found = true + if err != nil { + return result, err + } + result = append(result, transformer) + } + + if provider.Identity != nil { + if found == true { + return result, fmt.Errorf("more than one provider specified in a single element, should split into different list elements") + } + found = true + result = append(result, value.PrefixTransformer{ + Transformer: identity.NewEncryptCheckTransformer(), + Prefix: []byte{}, + }) + } + + if found == false { + return result, fmt.Errorf("invalid provider configuration provided") + } + } + return result, nil +} + +// GetAESPrefixTransformer returns a prefix transformer from the provided configuration +func GetAESPrefixTransformer(config *AESConfig) (value.PrefixTransformer, error) { + var result value.PrefixTransformer + + if len(config.Keys) == 0 { + return result, fmt.Errorf("aes provider has no valid keys") + } + for _, key := range config.Keys { + if key.Name == "" { + return result, fmt.Errorf("key with invalid name provided") + } + if key.Secret == "" { + return result, fmt.Errorf("key %v has no provided secret", key.Name) + } + } + + keyTransformers := []value.PrefixTransformer{} + + for _, keyData := range config.Keys { + key, err := base64.StdEncoding.DecodeString(keyData.Secret) + if err != nil { + return result, fmt.Errorf("could not obtain secret for named key %s: %s", keyData.Name, err) + } + block, err := aes.NewCipher(key) + if err != nil { + return result, fmt.Errorf("error while creating cipher for named key %s: %s", keyData.Name, err) + } + + // Create a new PrefixTransformer for this key + keyTransformers = append(keyTransformers, + value.PrefixTransformer{ + Transformer: aestransformer.NewGCMTransformer(block), + Prefix: []byte(keyData.Name + ":"), + }) + } + + // Create a prefixTransformer which can choose between these keys + keyTransformer := value.NewPrefixTransformers( + fmt.Errorf("no matching key was found for the provided AES transformer"), keyTransformers...) + + // Create a PrefixTransformer which shall later be put in a list with other providers + result = value.PrefixTransformer{ + Transformer: keyTransformer, + Prefix: []byte(aesTransformerPrefixV1), + } + return result, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/encryptionconfig_test.go b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/encryptionconfig_test.go new file mode 100644 index 00000000000..ae7f10c140c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/encryptionconfig_test.go @@ -0,0 +1,172 @@ +/* +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 encryptionconfig + +import ( + "bytes" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/storage/value" +) + +const ( + sampleText = "abcdefghijklmnopqrstuvwxyz" + + sampleContextText = "0123456789" + + correctConfigWithIdentityFirst = ` +kind: EncryptionConfig +apiVersion: v1 +resources: + - resources: + - secrets + - namespaces + providers: + - identity: {} + - aes: + keys: + - name: key1 + secret: c2VjcmV0IGlzIHNlY3VyZQ== + - name: key2 + secret: dGhpcyBpcyBwYXNzd29yZA== +` + + correctConfigWithAesFirst = ` +kind: EncryptionConfig +apiVersion: v1 +resources: + - resources: + - secrets + providers: + - aes: + keys: + - name: key1 + secret: c2VjcmV0IGlzIHNlY3VyZQ== + - name: key2 + secret: dGhpcyBpcyBwYXNzd29yZA== + - identity: {} +` + + incorrectConfigNoSecretForKey = ` +kind: EncryptionConfig +apiVersion: v1 +resources: + - resources: + - namespaces + - secrets + providers: + - aes: + keys: + - name: key1 +` + + incorrectConfigInvalidKey = ` +kind: EncryptionConfig +apiVersion: v1 +resources: + - resources: + - namespaces + - secrets + providers: + - aes: + keys: + - name: key1 + secret: c2VjcmV0IGlzIHNlY3VyZQ== + - name: key2 + secret: YSBzZWNyZXQgYSBzZWNyZXQ= +` +) + +func TestEncryptionProviderConfigCorrect(t *testing.T) { + // Creates two transformers with different ordering of identity and AES transformers. + // Transforms data using one of them, and tries to untransform using both of them. + // Repeats this for both the possible combinations. + + identityFirstTransformerOverrides, err := ParseEncryptionConfiguration(strings.NewReader(correctConfigWithIdentityFirst)) + if err != nil { + t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithIdentityFirst) + } + + aesFirstTransformerOverrides, err := ParseEncryptionConfiguration(strings.NewReader(correctConfigWithAesFirst)) + if err != nil { + t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, correctConfigWithAesFirst) + } + + // Pick the transformer for any of the returned resources. + identityFirstTransformer := identityFirstTransformerOverrides[schema.ParseGroupResource("secrets")] + aesFirstTransformer := aesFirstTransformerOverrides[schema.ParseGroupResource("secrets")] + + context := value.DefaultContext([]byte(sampleContextText)) + originalText := []byte(sampleText) + + testCases := []struct { + WritingTransformer value.Transformer + Name string + AesStale bool + IdentityStale bool + }{ + {aesFirstTransformer, "aesFirst", false, true}, + {identityFirstTransformer, "identityFirst", true, false}, + } + + for _, testCase := range testCases { + transformedData, err := testCase.WritingTransformer.TransformToStorage(originalText, context) + if err != nil { + t.Fatalf("%s: error while transforming data to storage: %s", testCase.Name, err) + } + + aesUntransformedData, stale, err := aesFirstTransformer.TransformFromStorage(transformedData, context) + if err != nil { + t.Fatalf("%s: error while reading using aesFirst transformer: %s", testCase.Name, err) + } + if stale != testCase.AesStale { + t.Fatalf("%s: wrong stale information on reading using aesFirst transformer, should be %v", testCase.Name, testCase.AesStale) + } + + identityUntransformedData, stale, err := identityFirstTransformer.TransformFromStorage(transformedData, context) + if err != nil { + t.Fatalf("%s: error while reading using identityFirst transformer: %s", testCase.Name, err) + } + if stale != testCase.IdentityStale { + t.Fatalf("%s: wrong stale information on reading using identityFirst transformer, should be %v", testCase.Name, testCase.IdentityStale) + } + + if bytes.Compare(aesUntransformedData, originalText) != 0 { + t.Fatalf("%s: aesFirst transformer transformed data incorrectly. Expected: %v, got %v", testCase.Name, originalText, aesUntransformedData) + } + + if bytes.Compare(identityUntransformedData, originalText) != 0 { + t.Fatalf("%s: identityFirst transformer transformed data incorrectly. Expected: %v, got %v", testCase.Name, originalText, aesUntransformedData) + } + } +} + +// Throw error if key has no secret +func TestEncryptionProviderConfigNoSecretForKey(t *testing.T) { + if _, err := ParseEncryptionConfiguration(strings.NewReader(incorrectConfigNoSecretForKey)); err == nil { + t.Fatalf("invalid configuration file (one key has no secret) got parsed:\n%s", incorrectConfigNoSecretForKey) + } +} + +// Throw error if invalid key for AES +func TestEncryptionProviderConfigInvalidKey(t *testing.T) { + if _, err := ParseEncryptionConfiguration(strings.NewReader(incorrectConfigInvalidKey)); err == nil { + t.Fatalf("invalid configuration file (bad AES key) got parsed:\n%s", incorrectConfigInvalidKey) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/types.go b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/types.go new file mode 100644 index 00000000000..ea23ac40539 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/options/encryptionconfig/types.go @@ -0,0 +1,61 @@ +/* +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 encryptionconfig + +// EncryptionConfig stores the complete configuration for encryption providers. +type EncryptionConfig struct { + // kind is the type of configuration file. + Kind string `json:"kind"` + // apiVersion is the API version this file has to be parsed as. + APIVersion string `json:"apiVersion"` + // resources is a list containing resources, and their corresponding encryption providers. + Resources []ResourceConfig `json:"resources"` +} + +// ResourceConfig stores per resource configuration. +type ResourceConfig struct { + // resources is a list of kubernetes resources which have to be encrypted. + Resources []string `json:"resources"` + // providers is a list of transformers to be used for reading and writing the resources to disk. + // eg: aes, identity. + Providers []ProviderConfig `json:"providers"` +} + +// ProviderConfig stores the provided configuration for an encryption provider. +type ProviderConfig struct { + // aes is the configuration for the AEAD-GCM transformer. + AES *AESConfig `json:"aes,omitempty"` + // identity is the (empty) configuration for the identity transformer. + Identity *IdentityConfig `json:"identity,omitempty"` +} + +// AESConfig contains the API configuration for an AES transformer. +type AESConfig struct { + // keys is a list of keys to be used for creating the AES transformer. + Keys []Key `json:"keys"` +} + +// Key contains name and secret of the provided key for AES transformer. +type Key struct { + // name is the name of the key to be used while storing data to disk. + Name string `json:"name"` + // secret is the actual AES key, encoded in base64. It has to be 16, 24 or 32 bytes long. + Secret string `json:"secret"` +} + +// IdentityConfig is an empty struct to allow identity transformer in provider configuration. +type IdentityConfig struct{} diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/etcd.go b/staging/src/k8s.io/apiserver/pkg/server/options/etcd.go index 5bfa27e60ac..e255b636050 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/etcd.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/etcd.go @@ -30,7 +30,8 @@ import ( ) type EtcdOptions struct { - StorageConfig storagebackend.Config + StorageConfig storagebackend.Config + EncryptionProviderConfigFilepath string EtcdServersOverrides []string @@ -109,6 +110,9 @@ func (s *EtcdOptions) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&s.StorageConfig.Quorum, "etcd-quorum-read", s.StorageConfig.Quorum, "If true, enable quorum read.") + + fs.StringVar(&s.EncryptionProviderConfigFilepath, "experimental-encryption-provider-config", s.EncryptionProviderConfigFilepath, + "The file containing configuration for encryption providers to be used for storing secrets in etcd") } func (s *EtcdOptions) ApplyTo(c *server.Config) error { diff --git a/staging/src/k8s.io/apiserver/pkg/server/storage/BUILD b/staging/src/k8s.io/apiserver/pkg/server/storage/BUILD index 324e82c6ef9..eec4c23df61 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/storage/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/storage/BUILD @@ -48,5 +48,6 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/runtime/serializer/recognizer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library", + "//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/server/storage/storage_factory.go b/staging/src/k8s.io/apiserver/pkg/server/storage/storage_factory.go index 790d9ca0cd8..2ceff3b6121 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/storage/storage_factory.go +++ b/staging/src/k8s.io/apiserver/pkg/server/storage/storage_factory.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/storage/storagebackend" + "k8s.io/apiserver/pkg/storage/value" ) // Backend describes the storage servers, the information here should be enough @@ -109,6 +110,8 @@ type groupResourceOverrides struct { // decoderDecoratorFn is optional and may wrap the provided decoders (can add new decoders). The order of // returned decoders will be priority for attempt to decode. decoderDecoratorFn func([]runtime.Decoder) []runtime.Decoder + // transformer is optional and shall encrypt that resource at rest. + transformer value.Transformer } // Apply overrides the provided config and options if the override has a value in that position @@ -132,6 +135,9 @@ func (o groupResourceOverrides) Apply(config *storagebackend.Config, options *St if o.decoderDecoratorFn != nil { options.DecoderDecoratorFn = o.decoderDecoratorFn } + if o.transformer != nil { + config.Transformer = o.transformer + } } var _ StorageFactory = &DefaultStorageFactory{} @@ -193,6 +199,12 @@ func (s *DefaultStorageFactory) SetSerializer(groupResource schema.GroupResource s.Overrides[groupResource] = overrides } +func (s *DefaultStorageFactory) SetTransformer(groupResource schema.GroupResource, transformer value.Transformer) { + overrides := s.Overrides[groupResource] + overrides.transformer = transformer + s.Overrides[groupResource] = overrides +} + // AddCohabitatingResources links resources together the order of the slice matters! its the priority order of lookup for finding a storage location func (s *DefaultStorageFactory) AddCohabitatingResources(groupResources ...schema.GroupResource) { for _, groupResource := range groupResources { diff --git a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go index 1e9fbed8ed1..210b67084c4 100644 --- a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go +++ b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go @@ -388,7 +388,7 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor elems := make([]*elemForDecode, 0, len(getResp.Kvs)) for _, kv := range getResp.Kvs { - data, _, err := s.transformer.TransformFromStorage(kv.Value, authenticatedDataString(key)) + data, _, err := s.transformer.TransformFromStorage(kv.Value, authenticatedDataString(kv.Key)) if err != nil { utilruntime.HandleError(fmt.Errorf("unable to transform key %q: %v", key, err)) continue diff --git a/staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory/etcd3.go b/staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory/etcd3.go index 2c838a09ce7..ff6753a0110 100644 --- a/staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory/etcd3.go +++ b/staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory/etcd3.go @@ -58,7 +58,7 @@ func newETCD3Storage(c storagebackend.Config) (storage.Interface, DestroyFunc, e } transformer := c.Transformer if transformer == nil { - transformer = value.IdentityTransformer + transformer = value.NewMutableTransformer(value.IdentityTransformer) } if c.Quorum { return etcd3.New(client, c.Codec, c.Prefix, transformer), destroyFunc, nil diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity/BUILD b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity/BUILD new file mode 100644 index 00000000000..3043fe2065b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity/BUILD @@ -0,0 +1,15 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["identity.go"], + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity/identity.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity/identity.go new file mode 100644 index 00000000000..0f7f1a70393 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity/identity.go @@ -0,0 +1,50 @@ +/* +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 identity + +import ( + "bytes" + "fmt" + + "k8s.io/apiserver/pkg/storage/value" +) + +// encryptIdentityTransformer performs no transformation on provided data, but validates +// that the data is not encrypted data during TransformFromStorage +type identityTransformer struct{} + +// NewEncryptCheckTransformer returns an identityTransformer which returns an error +// on attempts to read encrypted data +func NewEncryptCheckTransformer() value.Transformer { + return identityTransformer{} +} + +// TransformFromStorage returns the input bytes if the data is not encrypted +func (identityTransformer) TransformFromStorage(b []byte, context value.Context) ([]byte, bool, error) { + // EncryptIdentityTransformer has to return an error if the data is encoded using another transformer. + // JSON data starts with '{'. Protobuf data has a prefix 'k8s[\x00-\xFF]'. + // Prefix 'k8s:enc:' is reserved for encrypted data on disk. + if bytes.HasPrefix(b, []byte("k8s:enc:")) { + return []byte{}, false, fmt.Errorf("identity transformer tried to read encrypted data") + } + return b, false, nil +} + +// TransformToStorage implements the Transformer interface for encryptIdentityTransformer +func (identityTransformer) TransformToStorage(b []byte, context value.Context) ([]byte, error) { + return b, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go b/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go index ab5d4af959b..04af5d6db7b 100644 --- a/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go @@ -126,6 +126,13 @@ func (t *prefixTransformers) TransformFromStorage(data []byte, context Context) for i, transformer := range t.transformers { if bytes.HasPrefix(data, transformer.Prefix) { result, stale, err := transformer.Transformer.TransformFromStorage(data[len(transformer.Prefix):], context) + // To migrate away from encryption, user can specify an identity transformer higher up + // (in the config file) than the encryption transformer. In that scenario, the identity transformer needs to + // identify (during reads from disk) whether the data being read is encrypted or not. If the data is encrypted, + // it shall throw an error, but that error should not prevent subsequent transformers from being tried. + if len(transformer.Prefix) == 0 && err != nil { + continue + } return result, stale || i != 0, err } }