Add configuration options for encryption providers

Add location transformer, config for transformers

Location transformer helps choose the most specific transformer for
read/write operations depending on the path of resource being accessed.

Configuration allows use of --experimental-encryption-provider-config
to set up encryption providers. Only AEAD is supported at the moment.

Add new files to BUILD, AEAD => k8s-aes-gcm

Use group resources to select encryption provider

Update tests for configuration parsing

Remove location transformer

Allow specifying providers per resource group in configuration

Add IdentityTransformer configuration option

Fix minor issues with initial AEAD implementation

Unified parsing of all configurations

Parse configuration using a union struct

Run configuration parsing in APIserver, refactor parsing

More gdoc, fix minor bugs

Add test coverage for combined transformers

Use table driven tests for encryptionconfig
pull/6/head
Saksham Sharma 2017-05-23 17:30:49 -07:00 committed by Saksham Sharma
parent 68dd748ba1
commit 9760d00d08
15 changed files with 552 additions and 3 deletions

View File

@ -87,6 +87,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",

View File

@ -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"
@ -496,6 +497,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
}

View File

@ -357,6 +357,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

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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{}

View File

@ -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 {

View File

@ -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",
],
)

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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"],
)

View File

@ -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
}

View File

@ -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
}
}