Merge pull request #85363 from immutableT/encryption-config-defaulter

Add defaulting and validation logic for EncryptionConfiguration type.
This commit is contained in:
Kubernetes Prow Robot 2019-12-02 17:06:57 -08:00 committed by GitHub
commit 0810bc3386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 907 additions and 258 deletions

View File

@ -353,6 +353,7 @@ staging/src/k8s.io/apiserver/pkg/apis/audit
staging/src/k8s.io/apiserver/pkg/apis/audit/v1 staging/src/k8s.io/apiserver/pkg/apis/audit/v1
staging/src/k8s.io/apiserver/pkg/apis/audit/v1alpha1 staging/src/k8s.io/apiserver/pkg/apis/audit/v1alpha1
staging/src/k8s.io/apiserver/pkg/apis/audit/v1beta1 staging/src/k8s.io/apiserver/pkg/apis/audit/v1beta1
staging/src/k8s.io/apiserver/pkg/apis/config/v1
staging/src/k8s.io/apiserver/pkg/apis/example staging/src/k8s.io/apiserver/pkg/apis/example
staging/src/k8s.io/apiserver/pkg/apis/example/v1 staging/src/k8s.io/apiserver/pkg/apis/example/v1
staging/src/k8s.io/apiserver/pkg/apis/example2 staging/src/k8s.io/apiserver/pkg/apis/example2

View File

@ -30,6 +30,7 @@ filegroup(
srcs = [ srcs = [
":package-srcs", ":package-srcs",
"//staging/src/k8s.io/apiserver/pkg/apis/config/v1:all-srcs", "//staging/src/k8s.io/apiserver/pkg/apis/config/v1:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/apis/config/validation:all-srcs",
], ],
tags = ["automanaged"], tags = ["automanaged"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],

View File

@ -17,6 +17,8 @@ limitations under the License.
package config package config
import ( import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -74,6 +76,11 @@ type Key struct {
Secret string Secret string
} }
// String implements Stringer interface in a log safe way.
func (k Key) String() string {
return fmt.Sprintf("Name: %s, Secret: [REDACTED]", k.Name)
}
// IdentityConfiguration is an empty struct to allow identity transformer in provider configuration. // IdentityConfiguration is an empty struct to allow identity transformer in provider configuration.
type IdentityConfiguration struct{} type IdentityConfiguration struct{}
@ -83,7 +90,7 @@ type KMSConfiguration struct {
Name string Name string
// cacheSize is the maximum number of secrets which are cached in memory. The default value is 1000. // cacheSize is the maximum number of secrets which are cached in memory. The default value is 1000.
// +optional // +optional
CacheSize int32 CacheSize *int32
// endpoint is the gRPC server listening address, for example "unix:///var/run/kms-provider.sock". // endpoint is the gRPC server listening address, for example "unix:///var/run/kms-provider.sock".
Endpoint string Endpoint string
// Timeout for gRPC calls to kms-plugin (ex. 5s). The default is 3 seconds. // Timeout for gRPC calls to kms-plugin (ex. 5s). The default is 3 seconds.

View File

@ -1,8 +1,9 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"defaults.go",
"doc.go", "doc.go",
"register.go", "register.go",
"types.go", "types.go",
@ -35,3 +36,13 @@ filegroup(
tags = ["automanaged"], tags = ["automanaged"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )
go_test(
name = "go_default_test",
srcs = ["defaults_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
],
)

View File

@ -0,0 +1,44 @@
/*
Copyright 2019 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 v1
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
var (
defaultTimeout = &metav1.Duration{Duration: 3 * time.Second}
defaultCacheSize int32 = 1000
)
func addDefaultingFuncs(scheme *runtime.Scheme) error {
return RegisterDefaults(scheme)
}
// SetDefaults_KMSConfiguration applies defaults to KMSConfiguration.
func SetDefaults_KMSConfiguration(obj *KMSConfiguration) {
if obj.Timeout == nil {
obj.Timeout = defaultTimeout
}
if obj.CacheSize == nil {
obj.CacheSize = &defaultCacheSize
}
}

View File

@ -0,0 +1,92 @@
/*
Copyright 2019 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 v1
import (
"testing"
"time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/google/go-cmp/cmp"
)
func TestKMSProviderTimeoutDefaults(t *testing.T) {
testCases := []struct {
desc string
in *KMSConfiguration
want *KMSConfiguration
}{
{
desc: "timeout not supplied",
in: &KMSConfiguration{},
want: &KMSConfiguration{Timeout: defaultTimeout, CacheSize: &defaultCacheSize},
},
{
desc: "timeout supplied",
in: &KMSConfiguration{Timeout: &v1.Duration{Duration: 1 * time.Minute}},
want: &KMSConfiguration{Timeout: &v1.Duration{Duration: 1 * time.Minute}, CacheSize: &defaultCacheSize},
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
SetDefaults_KMSConfiguration(tt.in)
if d := cmp.Diff(tt.want, tt.in); d != "" {
t.Fatalf("KMS Provider mismatch (-want +got):\n%s", d)
}
})
}
}
func TestKMSProviderCacheDefaults(t *testing.T) {
var (
zero int32 = 0
ten int32 = 10
)
testCases := []struct {
desc string
in *KMSConfiguration
want *KMSConfiguration
}{
{
desc: "cache size not supplied",
in: &KMSConfiguration{},
want: &KMSConfiguration{Timeout: defaultTimeout, CacheSize: &defaultCacheSize},
},
{
desc: "cache of zero size supplied",
in: &KMSConfiguration{CacheSize: &zero},
want: &KMSConfiguration{Timeout: defaultTimeout, CacheSize: &zero},
},
{
desc: "positive cache size supplied",
in: &KMSConfiguration{CacheSize: &ten},
want: &KMSConfiguration{Timeout: defaultTimeout, CacheSize: &ten},
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
SetDefaults_KMSConfiguration(tt.in)
if d := cmp.Diff(tt.want, tt.in); d != "" {
t.Fatalf("KMS Provider mismatch (-want +got):\n%s", d)
}
})
}
}

View File

@ -40,6 +40,7 @@ func init() {
// generated functions takes place in the generated files. The separation // generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing. // makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(addKnownTypes) localSchemeBuilder.Register(addKnownTypes)
localSchemeBuilder.Register(addDefaultingFuncs)
} }
func addKnownTypes(scheme *runtime.Scheme) error { func addKnownTypes(scheme *runtime.Scheme) error {

View File

@ -17,6 +17,8 @@ limitations under the License.
package v1 package v1
import ( import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -74,6 +76,11 @@ type Key struct {
Secret string `json:"secret"` Secret string `json:"secret"`
} }
// String implements Stringer interface in a log safe way.
func (k Key) String() string {
return fmt.Sprintf("Name: %s, Secret: [REDACTED]", k.Name)
}
// IdentityConfiguration is an empty struct to allow identity transformer in provider configuration. // IdentityConfiguration is an empty struct to allow identity transformer in provider configuration.
type IdentityConfiguration struct{} type IdentityConfiguration struct{}
@ -83,7 +90,7 @@ type KMSConfiguration struct {
Name string `json:"name"` Name string `json:"name"`
// cacheSize is the maximum number of secrets which are cached in memory. The default value is 1000. // cacheSize is the maximum number of secrets which are cached in memory. The default value is 1000.
// +optional // +optional
CacheSize int32 `json:"cachesize,omitempty"` CacheSize *int32 `json:"cachesize,omitempty"`
// endpoint is the gRPC server listening address, for example "unix:///var/run/kms-provider.sock". // endpoint is the gRPC server listening address, for example "unix:///var/run/kms-provider.sock".
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
// Timeout for gRPC calls to kms-plugin (ex. 5s). The default is 3 seconds. // Timeout for gRPC calls to kms-plugin (ex. 5s). The default is 3 seconds.

View File

@ -179,7 +179,7 @@ func Convert_config_IdentityConfiguration_To_v1_IdentityConfiguration(in *config
func autoConvert_v1_KMSConfiguration_To_config_KMSConfiguration(in *KMSConfiguration, out *config.KMSConfiguration, s conversion.Scope) error { func autoConvert_v1_KMSConfiguration_To_config_KMSConfiguration(in *KMSConfiguration, out *config.KMSConfiguration, s conversion.Scope) error {
out.Name = in.Name out.Name = in.Name
out.CacheSize = in.CacheSize out.CacheSize = (*int32)(unsafe.Pointer(in.CacheSize))
out.Endpoint = in.Endpoint out.Endpoint = in.Endpoint
out.Timeout = (*metav1.Duration)(unsafe.Pointer(in.Timeout)) out.Timeout = (*metav1.Duration)(unsafe.Pointer(in.Timeout))
return nil return nil
@ -192,7 +192,7 @@ func Convert_v1_KMSConfiguration_To_config_KMSConfiguration(in *KMSConfiguration
func autoConvert_config_KMSConfiguration_To_v1_KMSConfiguration(in *config.KMSConfiguration, out *KMSConfiguration, s conversion.Scope) error { func autoConvert_config_KMSConfiguration_To_v1_KMSConfiguration(in *config.KMSConfiguration, out *KMSConfiguration, s conversion.Scope) error {
out.Name = in.Name out.Name = in.Name
out.CacheSize = in.CacheSize out.CacheSize = (*int32)(unsafe.Pointer(in.CacheSize))
out.Endpoint = in.Endpoint out.Endpoint = in.Endpoint
out.Timeout = (*metav1.Duration)(unsafe.Pointer(in.Timeout)) out.Timeout = (*metav1.Duration)(unsafe.Pointer(in.Timeout))
return nil return nil

View File

@ -97,6 +97,11 @@ func (in *IdentityConfiguration) DeepCopy() *IdentityConfiguration {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KMSConfiguration) DeepCopyInto(out *KMSConfiguration) { func (in *KMSConfiguration) DeepCopyInto(out *KMSConfiguration) {
*out = *in *out = *in
if in.CacheSize != nil {
in, out := &in.CacheSize, &out.CacheSize
*out = new(int32)
**out = **in
}
if in.Timeout != nil { if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout in, out := &in.Timeout, &out.Timeout
*out = new(metav1.Duration) *out = new(metav1.Duration)

View File

@ -28,5 +28,18 @@ import (
// Public to allow building arbitrary schemes. // Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters. // All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error { func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&EncryptionConfiguration{}, func(obj interface{}) { SetObjectDefaults_EncryptionConfiguration(obj.(*EncryptionConfiguration)) })
return nil return nil
} }
func SetObjectDefaults_EncryptionConfiguration(in *EncryptionConfiguration) {
for i := range in.Resources {
a := &in.Resources[i]
for j := range a.Providers {
b := &a.Providers[j]
if b.KMS != nil {
SetDefaults_KMSConfiguration(b.KMS)
}
}
}
}

View File

@ -0,0 +1,39 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["validation.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/apis/config/validation",
importpath = "k8s.io/apiserver/pkg/apis/config/validation",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["validation_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library",
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,219 @@
/*
Copyright 2019 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 validation validates EncryptionConfiguration.
package validation
import (
"encoding/base64"
"fmt"
"net/url"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/config"
)
const (
moreThanOneElementErr = "more than one provider specified in a single element, should split into different list elements"
keyLenErrFmt = "secret is not of the expected length, got %d, expected one of %v"
unsupportedSchemeErrFmt = "unsupported scheme %q for KMS provider, only unix is supported"
atLeastOneRequiredErrFmt = "at least one %s is required"
mandatoryFieldErrFmt = "%s is a mandatory field for a %s"
base64EncodingErr = "secrets must be base64 encoded"
zeroOrNegativeErrFmt = "%s should be a positive value"
negativeValueErrFmt = "%s can't be negative"
encryptionConfigNilErr = "EncryptionConfiguration can't be nil"
)
var (
aesKeySizes = []int{16, 24, 32}
// See https://golang.org/pkg/crypto/aes/#NewCipher for details on supported key sizes for AES.
secretBoxKeySizes = []int{32}
// See https://godoc.org/golang.org/x/crypto/nacl/secretbox#Open for details on the supported key sizes for Secretbox.
root = field.NewPath("resources")
)
// ValidateEncryptionConfiguration validates a v1.EncryptionConfiguration.
func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration) field.ErrorList {
allErrs := field.ErrorList{}
if c == nil {
allErrs = append(allErrs, field.Required(root, "EncryptionConfiguration can't be nil"))
return allErrs
}
if len(c.Resources) == 0 {
allErrs = append(allErrs, field.Required(root, fmt.Sprintf(atLeastOneRequiredErrFmt, root)))
return allErrs
}
for i, conf := range c.Resources {
r := root.Index(i).Child("resources")
p := root.Index(i).Child("providers")
if len(conf.Resources) == 0 {
allErrs = append(allErrs, field.Required(r, fmt.Sprintf(atLeastOneRequiredErrFmt, r)))
}
if len(conf.Providers) == 0 {
allErrs = append(allErrs, field.Required(p, fmt.Sprintf(atLeastOneRequiredErrFmt, p)))
}
for j, provider := range conf.Providers {
path := p.Index(j)
allErrs = append(allErrs, validateSingleProvider(provider, path)...)
switch {
case provider.KMS != nil:
allErrs = append(allErrs, validateKMSConfiguration(provider.KMS, path.Child("kms"))...)
case provider.AESGCM != nil:
allErrs = append(allErrs, validateKeys(provider.AESGCM.Keys, path.Child("aesgcm").Child("keys"), aesKeySizes)...)
case provider.AESCBC != nil:
allErrs = append(allErrs, validateKeys(provider.AESCBC.Keys, path.Child("aescbc").Child("keys"), aesKeySizes)...)
case provider.Secretbox != nil:
allErrs = append(allErrs, validateKeys(provider.Secretbox.Keys, path.Child("secretbox").Child("keys"), secretBoxKeySizes)...)
}
}
}
return allErrs
}
func validateSingleProvider(provider config.ProviderConfiguration, filedPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
found := 0
if provider.KMS != nil {
found++
}
if provider.AESGCM != nil {
found++
}
if provider.AESCBC != nil {
found++
}
if provider.Secretbox != nil {
found++
}
if provider.Identity != nil {
found++
}
if found == 0 {
return append(allErrs, field.Invalid(filedPath, provider, "provider does not contain any of the expected providers: KMS, AESGCM, AESCBC, Secretbox, Identity"))
}
if found > 1 {
return append(allErrs, field.Invalid(filedPath, provider, moreThanOneElementErr))
}
return allErrs
}
func validateKeys(keys []config.Key, fieldPath *field.Path, expectedLen []int) field.ErrorList {
allErrs := field.ErrorList{}
if len(keys) == 0 {
allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, "keys")))
return allErrs
}
for i, key := range keys {
allErrs = append(allErrs, validateKey(key, fieldPath.Index(i), expectedLen)...)
}
return allErrs
}
func validateKey(key config.Key, fieldPath *field.Path, expectedLen []int) field.ErrorList {
allErrs := field.ErrorList{}
if key.Name == "" {
allErrs = append(allErrs, field.Required(fieldPath.Child("name"), fmt.Sprintf(mandatoryFieldErrFmt, "name", "key")))
}
if key.Secret == "" {
allErrs = append(allErrs, field.Required(fieldPath.Child("secret"), fmt.Sprintf(mandatoryFieldErrFmt, "secret", "key")))
return allErrs
}
secret, err := base64.StdEncoding.DecodeString(key.Secret)
if err != nil {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("secret"), "REDACTED", base64EncodingErr))
return allErrs
}
lenMatched := false
for _, l := range expectedLen {
if len(secret) == l {
lenMatched = true
break
}
}
if !lenMatched {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("secret"), "REDACTED", fmt.Sprintf(keyLenErrFmt, len(secret), expectedLen)))
}
return allErrs
}
func validateKMSConfiguration(c *config.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if c.Name == "" {
allErrs = append(allErrs, field.Required(fieldPath.Child("name"), fmt.Sprintf(mandatoryFieldErrFmt, "name", "provider")))
}
allErrs = append(allErrs, validateKMSTimeout(c, fieldPath.Child("timeout"))...)
allErrs = append(allErrs, validateKMSEndpoint(c, fieldPath.Child("endpoint"))...)
allErrs = append(allErrs, validateKMSCacheSize(c, fieldPath.Child("cachesize"))...)
return allErrs
}
func validateKMSCacheSize(c *config.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if *c.CacheSize <= 0 {
allErrs = append(allErrs, field.Invalid(fieldPath, *c.CacheSize, fmt.Sprintf(zeroOrNegativeErrFmt, "cachesize")))
}
return allErrs
}
func validateKMSTimeout(c *config.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if c.Timeout.Duration <= 0 {
allErrs = append(allErrs, field.Invalid(fieldPath, c.Timeout, fmt.Sprintf(zeroOrNegativeErrFmt, "timeout")))
}
return allErrs
}
func validateKMSEndpoint(c *config.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(c.Endpoint) == 0 {
return append(allErrs, field.Invalid(fieldPath, "", fmt.Sprintf(mandatoryFieldErrFmt, "endpoint", "kms")))
}
u, err := url.Parse(c.Endpoint)
if err != nil {
return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf("invalid endpoint for kms provider, error: %v", err)))
}
if u.Scheme != "unix" {
return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf(unsupportedSchemeErrFmt, u.Scheme)))
}
return allErrs
}

View File

@ -0,0 +1,354 @@
/*
Copyright 2019 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 validation
import (
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/config"
)
func TestStructure(t *testing.T) {
firstResourcePath := root.Index(0)
testCases := []struct {
desc string
in *config.EncryptionConfiguration
want field.ErrorList
}{
{
desc: "nil encryption config",
in: nil,
want: field.ErrorList{
field.Required(root, encryptionConfigNilErr),
},
},
{
desc: "empty encryption config",
in: &config.EncryptionConfiguration{},
want: field.ErrorList{
field.Required(root, fmt.Sprintf(atLeastOneRequiredErrFmt, root)),
},
},
{
desc: "no k8s resources",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Providers: []config.ProviderConfiguration{
{
AESCBC: &config.AESConfiguration{
Keys: []config.Key{
{
Name: "foo",
Secret: "A/j5CnrWGB83ylcPkuUhm/6TSyrQtsNJtDPwPHNOj4Q=",
},
},
},
},
},
},
},
},
want: field.ErrorList{
field.Required(firstResourcePath.Child("resources"), fmt.Sprintf(atLeastOneRequiredErrFmt, root.Index(0).Child("resources"))),
},
},
{
desc: "no providers",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
},
},
},
want: field.ErrorList{
field.Required(firstResourcePath.Child("providers"), fmt.Sprintf(atLeastOneRequiredErrFmt, root.Index(0).Child("providers"))),
},
},
{
desc: "multiple providers",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
AESGCM: &config.AESConfiguration{
Keys: []config.Key{
{
Name: "foo",
Secret: "A/j5CnrWGB83ylcPkuUhm/6TSyrQtsNJtDPwPHNOj4Q=",
},
},
},
AESCBC: &config.AESConfiguration{
Keys: []config.Key{
{
Name: "foo",
Secret: "A/j5CnrWGB83ylcPkuUhm/6TSyrQtsNJtDPwPHNOj4Q=",
},
},
},
},
},
},
},
},
want: field.ErrorList{
field.Invalid(
firstResourcePath.Child("providers").Index(0),
config.ProviderConfiguration{
AESGCM: &config.AESConfiguration{
Keys: []config.Key{
{
Name: "foo",
Secret: "A/j5CnrWGB83ylcPkuUhm/6TSyrQtsNJtDPwPHNOj4Q=",
},
},
},
AESCBC: &config.AESConfiguration{
Keys: []config.Key{
{
Name: "foo",
Secret: "A/j5CnrWGB83ylcPkuUhm/6TSyrQtsNJtDPwPHNOj4Q=",
},
},
},
},
moreThanOneElementErr),
},
},
{
desc: "valid config",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
AESGCM: &config.AESConfiguration{
Keys: []config.Key{
{
Name: "foo",
Secret: "A/j5CnrWGB83ylcPkuUhm/6TSyrQtsNJtDPwPHNOj4Q=",
},
},
},
},
},
},
},
},
want: field.ErrorList{},
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
got := ValidateEncryptionConfiguration(tt.in)
if d := cmp.Diff(tt.want, got); d != "" {
t.Fatalf("EncryptionConfiguratoin validation results mismatch (-want +got):\n%s", d)
}
})
}
}
func TestKey(t *testing.T) {
path := root.Index(0).Child("provider").Index(0).Child("key").Index(0)
testCases := []struct {
desc string
in config.Key
want field.ErrorList
}{
{
desc: "valid key",
in: config.Key{Name: "foo", Secret: "c2VjcmV0IGlzIHNlY3VyZQ=="},
want: field.ErrorList{},
},
{
desc: "key without name",
in: config.Key{Secret: "c2VjcmV0IGlzIHNlY3VyZQ=="},
want: field.ErrorList{
field.Required(path.Child("name"), fmt.Sprintf(mandatoryFieldErrFmt, "name", "key")),
},
},
{
desc: "key without secret",
in: config.Key{Name: "foo"},
want: field.ErrorList{
field.Required(path.Child("secret"), fmt.Sprintf(mandatoryFieldErrFmt, "secret", "key")),
},
},
{
desc: "key is not base64 encoded",
in: config.Key{Name: "foo", Secret: "P@ssword"},
want: field.ErrorList{
field.Invalid(path.Child("secret"), "REDACTED", base64EncodingErr),
},
},
{
desc: "key is not of expected length",
in: config.Key{Name: "foo", Secret: "cGFzc3dvcmQK"},
want: field.ErrorList{
field.Invalid(path.Child("secret"), "REDACTED", fmt.Sprintf(keyLenErrFmt, 9, aesKeySizes)),
},
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
got := validateKey(tt.in, path, aesKeySizes)
if d := cmp.Diff(tt.want, got); d != "" {
t.Fatalf("Key validation results mismatch (-want +got):\n%s", d)
}
})
}
}
func TestKMSProviderTimeout(t *testing.T) {
timeoutField := field.NewPath("Resource").Index(0).Child("Provider").Index(0).Child("KMS").Child("Timeout")
negativeTimeout := &metav1.Duration{Duration: -1 * time.Minute}
zeroTimeout := &metav1.Duration{Duration: 0 * time.Minute}
testCases := []struct {
desc string
in *config.KMSConfiguration
want field.ErrorList
}{
{
desc: "valid timeout",
in: &config.KMSConfiguration{Timeout: &metav1.Duration{Duration: 1 * time.Minute}},
want: field.ErrorList{},
},
{
desc: "negative timeout",
in: &config.KMSConfiguration{Timeout: negativeTimeout},
want: field.ErrorList{
field.Invalid(timeoutField, negativeTimeout, fmt.Sprintf(zeroOrNegativeErrFmt, "timeout")),
},
},
{
desc: "zero timeout",
in: &config.KMSConfiguration{Timeout: zeroTimeout},
want: field.ErrorList{
field.Invalid(timeoutField, zeroTimeout, fmt.Sprintf(zeroOrNegativeErrFmt, "timeout")),
},
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
got := validateKMSTimeout(tt.in, timeoutField)
if d := cmp.Diff(tt.want, got); d != "" {
t.Fatalf("KMS Provider validation mismatch (-want +got):\n%s", d)
}
})
}
}
func TestKMSEndpoint(t *testing.T) {
endpointField := field.NewPath("Resource").Index(0).Child("Provider").Index(0).Child("kms").Child("endpoint")
testCases := []struct {
desc string
in *config.KMSConfiguration
want field.ErrorList
}{
{
desc: "valid endpoint",
in: &config.KMSConfiguration{Endpoint: "unix:///socket.sock"},
want: field.ErrorList{},
},
{
desc: "empty endpoint",
in: &config.KMSConfiguration{},
want: field.ErrorList{
field.Invalid(endpointField, "", fmt.Sprintf(mandatoryFieldErrFmt, "endpoint", "kms")),
},
},
{
desc: "non unix endpoint",
in: &config.KMSConfiguration{Endpoint: "https://www.foo.com"},
want: field.ErrorList{
field.Invalid(endpointField, "https://www.foo.com", fmt.Sprintf(unsupportedSchemeErrFmt, "https")),
},
},
{
desc: "invalid url",
in: &config.KMSConfiguration{Endpoint: "unix:///foo\n.socket"},
want: field.ErrorList{
field.Invalid(endpointField, "unix:///foo\n.socket", "invalid endpoint for kms provider, error: parse unix:///foo\n.socket: net/url: invalid control character in URL"),
},
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
got := validateKMSEndpoint(tt.in, endpointField)
if d := cmp.Diff(tt.want, got); d != "" {
t.Fatalf("KMS Provider validation mismatch (-want +got):\n%s", d)
}
})
}
}
func TestKMSProviderCacheSize(t *testing.T) {
cacheField := root.Index(0).Child("kms").Child("cachesize")
negativeCacheSize := int32(-1)
positiveCacheSize := int32(10)
zeroCacheSize := int32(0)
testCases := []struct {
desc string
in *config.KMSConfiguration
want field.ErrorList
}{
{
desc: "valid positive cache size",
in: &config.KMSConfiguration{CacheSize: &positiveCacheSize},
want: field.ErrorList{},
},
{
desc: "invalid zero cache size",
in: &config.KMSConfiguration{CacheSize: &zeroCacheSize},
want: field.ErrorList{
field.Invalid(cacheField, int32(0), fmt.Sprintf(zeroOrNegativeErrFmt, "cachesize")),
},
},
{
desc: "negative caches size",
in: &config.KMSConfiguration{CacheSize: &negativeCacheSize},
want: field.ErrorList{
field.Invalid(cacheField, negativeCacheSize, fmt.Sprintf(zeroOrNegativeErrFmt, "cachesize")),
},
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
got := validateKMSCacheSize(tt.in, cacheField)
if d := cmp.Diff(tt.want, got); d != "" {
t.Fatalf("KMS Provider validation mismatch (-want +got):\n%s", d)
}
})
}
}

View File

@ -97,6 +97,11 @@ func (in *IdentityConfiguration) DeepCopy() *IdentityConfiguration {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KMSConfiguration) DeepCopyInto(out *KMSConfiguration) { func (in *KMSConfiguration) DeepCopyInto(out *KMSConfiguration) {
*out = *in *out = *in
if in.CacheSize != nil {
in, out := &in.CacheSize, &out.CacheSize
*out = new(int32)
**out = **in
}
if in.Timeout != nil { if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout in, out := &in.Timeout, &out.Timeout
*out = new(v1.Duration) *out = new(v1.Duration)

View File

@ -17,6 +17,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/config/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/config/v1:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/config/validation:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/server/healthz:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/healthz:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/value:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes:go_default_library",
@ -32,8 +33,8 @@ go_test(
data = glob(["testdata/**"]), data = glob(["testdata/**"]),
embed = [":go_default_library"], embed = [":go_default_library"],
deps = [ deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/value:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/value:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope:go_default_library",

View File

@ -20,6 +20,7 @@ import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -33,6 +34,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
apiserverconfig "k8s.io/apiserver/pkg/apis/config" apiserverconfig "k8s.io/apiserver/pkg/apis/config"
apiserverconfigv1 "k8s.io/apiserver/pkg/apis/config/v1" apiserverconfigv1 "k8s.io/apiserver/pkg/apis/config/v1"
"k8s.io/apiserver/pkg/apis/config/validation"
"k8s.io/apiserver/pkg/server/healthz" "k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/storage/value" "k8s.io/apiserver/pkg/storage/value"
aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes" aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes"
@ -46,7 +48,6 @@ const (
aesGCMTransformerPrefixV1 = "k8s:enc:aesgcm:v1:" aesGCMTransformerPrefixV1 = "k8s:enc:aesgcm:v1:"
secretboxTransformerPrefixV1 = "k8s:enc:secretbox:v1:" secretboxTransformerPrefixV1 = "k8s:enc:secretbox:v1:"
kmsTransformerPrefixV1 = "k8s:enc:kms:v1:" kmsTransformerPrefixV1 = "k8s:enc:kms:v1:"
kmsPluginConnectionTimeout = 3 * time.Second
kmsPluginHealthzTTL = 3 * time.Second kmsPluginHealthzTTL = 3 * time.Second
) )
@ -104,15 +105,7 @@ func getKMSPluginProbes(reader io.Reader) ([]*kmsPluginProbe, error) {
for _, r := range config.Resources { for _, r := range config.Resources {
for _, p := range r.Providers { for _, p := range r.Providers {
if p.KMS != nil { if p.KMS != nil {
timeout := kmsPluginConnectionTimeout s, err := envelope.NewGRPCService(p.KMS.Endpoint, p.KMS.Timeout.Duration)
if p.KMS.Timeout != nil {
if p.KMS.Timeout.Duration <= 0 {
return nil, fmt.Errorf("could not configure KMS-Plugin's probe %q, timeout should be a positive value", p.KMS.Name)
}
timeout = p.KMS.Timeout.Duration
}
s, err := envelope.NewGRPCService(p.KMS.Endpoint, timeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not configure KMS-Plugin's probe %q, error: %v", p.KMS.Name, err) return nil, fmt.Errorf("could not configure KMS-Plugin's probe %q, error: %v", p.KMS.Name, err)
} }
@ -221,7 +214,8 @@ func loadConfig(data []byte) (*apiserverconfig.EncryptionConfiguration, error) {
if !ok { if !ok {
return nil, fmt.Errorf("got unexpected config type: %v", gvk) return nil, fmt.Errorf("got unexpected config type: %v", gvk)
} }
return config, nil
return config, validation.ValidateEncryptionConfiguration(config).ToAggregate()
} }
// The factory to create kms service. This is to make writing test easier. // The factory to create kms service. This is to make writing test easier.
@ -231,82 +225,38 @@ var envelopeServiceFactory = envelope.NewGRPCService
func GetPrefixTransformers(config *apiserverconfig.ResourceConfiguration) ([]value.PrefixTransformer, error) { func GetPrefixTransformers(config *apiserverconfig.ResourceConfiguration) ([]value.PrefixTransformer, error) {
var result []value.PrefixTransformer var result []value.PrefixTransformer
for _, provider := range config.Providers { for _, provider := range config.Providers {
found := false var (
transformer value.PrefixTransformer
err error
)
var transformer value.PrefixTransformer switch {
var err error case provider.AESGCM != nil:
if provider.AESGCM != nil {
transformer, err = GetAESPrefixTransformer(provider.AESGCM, aestransformer.NewGCMTransformer, aesGCMTransformerPrefixV1) transformer, err = GetAESPrefixTransformer(provider.AESGCM, aestransformer.NewGCMTransformer, aesGCMTransformerPrefixV1)
if err != nil { case provider.AESCBC != nil:
return result, err
}
found = true
}
if provider.AESCBC != nil {
if found == true {
return result, fmt.Errorf("more than one provider specified in a single element, should split into different list elements")
}
transformer, err = GetAESPrefixTransformer(provider.AESCBC, aestransformer.NewCBCTransformer, aesCBCTransformerPrefixV1) transformer, err = GetAESPrefixTransformer(provider.AESCBC, aestransformer.NewCBCTransformer, aesCBCTransformerPrefixV1)
found = true case provider.Secretbox != nil:
}
if provider.Secretbox != nil {
if found == true {
return result, fmt.Errorf("more than one provider specified in a single element, should split into different list elements")
}
transformer, err = GetSecretboxPrefixTransformer(provider.Secretbox) transformer, err = GetSecretboxPrefixTransformer(provider.Secretbox)
found = true case provider.KMS != nil:
} envelopeService, err := envelopeServiceFactory(provider.KMS.Endpoint, provider.KMS.Timeout.Duration)
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")
}
transformer = value.PrefixTransformer{
Transformer: identity.NewEncryptCheckTransformer(),
Prefix: []byte{},
}
found = true
}
if provider.KMS != nil {
if found == true {
return nil, fmt.Errorf("more than one provider specified in a single element, should split into different list elements")
}
// Ensure the endpoint is provided.
if len(provider.KMS.Endpoint) == 0 {
return nil, fmt.Errorf("remote KMS provider can't use empty string as endpoint")
}
timeout := kmsPluginConnectionTimeout
if provider.KMS.Timeout != nil {
if provider.KMS.Timeout.Duration <= 0 {
return nil, fmt.Errorf("could not configure KMS plugin %q, timeout should be a positive value", provider.KMS.Name)
}
timeout = provider.KMS.Timeout.Duration
}
// Get gRPC client service with endpoint.
envelopeService, err := envelopeServiceFactory(provider.KMS.Endpoint, timeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not configure KMS plugin %q, error: %v", provider.KMS.Name, err) return nil, fmt.Errorf("could not configure KMS plugin %q, error: %v", provider.KMS.Name, err)
} }
transformer, err = getEnvelopePrefixTransformer(provider.KMS, envelopeService, kmsTransformerPrefixV1) transformer, err = getEnvelopePrefixTransformer(provider.KMS, envelopeService, kmsTransformerPrefixV1)
found = true case provider.Identity != nil:
transformer = value.PrefixTransformer{
Transformer: identity.NewEncryptCheckTransformer(),
Prefix: []byte{},
}
default:
return nil, errors.New("provider does not contain any of the expected providers: KMS, AESGCM, AESCBC, Secretbox, Identity")
} }
if err != nil { if err != nil {
return result, err return result, err
} }
result = append(result, transformer) result = append(result, transformer)
if found == false {
return result, fmt.Errorf("invalid provider configuration: at least one provider must be specified")
}
} }
return result, nil return result, nil
} }
@ -417,7 +367,7 @@ func GetSecretboxPrefixTransformer(config *apiserverconfig.SecretboxConfiguratio
// getEnvelopePrefixTransformer returns a prefix transformer from the provided config. // getEnvelopePrefixTransformer returns a prefix transformer from the provided config.
// envelopeService is used as the root of trust. // envelopeService is used as the root of trust.
func getEnvelopePrefixTransformer(config *apiserverconfig.KMSConfiguration, envelopeService envelope.Service, prefix string) (value.PrefixTransformer, error) { func getEnvelopePrefixTransformer(config *apiserverconfig.KMSConfiguration, envelopeService envelope.Service, prefix string) (value.PrefixTransformer, error) {
envelopeTransformer, err := envelope.NewEnvelopeTransformer(envelopeService, int(config.CacheSize), aestransformer.NewCBCTransformer) envelopeTransformer, err := envelope.NewEnvelopeTransformer(envelopeService, int(*config.CacheSize), aestransformer.NewCBCTransformer)
if err != nil { if err != nil {
return value.PrefixTransformer{}, err return value.PrefixTransformer{}, err
} }

View File

@ -22,15 +22,12 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect"
"strings"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
apiserverconfig "k8s.io/apiserver/pkg/apis/config" apiserverconfig "k8s.io/apiserver/pkg/apis/config"
"k8s.io/apiserver/pkg/storage/value" "k8s.io/apiserver/pkg/storage/value"
"k8s.io/apiserver/pkg/storage/value/encrypt/envelope" "k8s.io/apiserver/pkg/storage/value/encrypt/envelope"
@ -82,6 +79,7 @@ func newMockEnvelopeService(endpoint string, timeout time.Duration) (envelope.Se
func TestLegacyConfig(t *testing.T) { func TestLegacyConfig(t *testing.T) {
legacyV1Config := "testdata/valid-configs/legacy.yaml" legacyV1Config := "testdata/valid-configs/legacy.yaml"
legacyConfigObject, err := loadConfig(mustReadConfig(t, legacyV1Config)) legacyConfigObject, err := loadConfig(mustReadConfig(t, legacyV1Config))
cacheSize := int32(10)
if err != nil { if err != nil {
t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, legacyV1Config) t.Fatalf("error while parsing configuration file: %s.\nThe file was:\n%s", err, legacyV1Config)
} }
@ -101,7 +99,8 @@ func TestLegacyConfig(t *testing.T) {
{KMS: &apiserverconfig.KMSConfiguration{ {KMS: &apiserverconfig.KMSConfiguration{
Name: "testprovider", Name: "testprovider",
Endpoint: "unix:///tmp/testprovider.sock", Endpoint: "unix:///tmp/testprovider.sock",
CacheSize: 10, CacheSize: &cacheSize,
Timeout: &metav1.Duration{Duration: 3 * time.Second},
}}, }},
{AESCBC: &apiserverconfig.AESConfiguration{ {AESCBC: &apiserverconfig.AESConfiguration{
Keys: []apiserverconfig.Key{ Keys: []apiserverconfig.Key{
@ -118,8 +117,8 @@ func TestLegacyConfig(t *testing.T) {
}, },
}, },
} }
if !reflect.DeepEqual(legacyConfigObject, expected) { if d := cmp.Diff(expected, legacyConfigObject); d != "" {
t.Fatal(diff.ObjectReflectDiff(expected, legacyConfigObject)) t.Fatalf("EncryptionConfig mismatch (-want +got):\n%s", d)
} }
} }
@ -206,80 +205,8 @@ func TestEncryptionProviderConfigCorrect(t *testing.T) {
} }
} }
// Throw error if key has no secret
func TestEncryptionProviderConfigNoSecretForKey(t *testing.T) {
incorrectConfigNoSecretForKey := "testdata/invalid-configs/aes/no-key.yaml"
if _, err := ParseEncryptionConfiguration(mustConfigReader(t, 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) {
incorrectConfigInvalidKey := "testdata/invalid-configs/aes/invalid-key.yaml"
if _, err := ParseEncryptionConfiguration(mustConfigReader(t, incorrectConfigInvalidKey)); err == nil {
t.Fatalf("invalid configuration file (bad AES key) got parsed:\n%s", incorrectConfigInvalidKey)
}
}
// Throw error if kms has no endpoint
func TestEncryptionProviderConfigNoEndpointForKMS(t *testing.T) {
incorrectConfigNoEndpointForKMS := "testdata/invalid-configs/kms/no-endpoint.yaml"
if _, err := ParseEncryptionConfiguration(mustConfigReader(t, incorrectConfigNoEndpointForKMS)); err == nil {
t.Fatalf("invalid configuration file (kms has no endpoint) got parsed:\n%s", incorrectConfigNoEndpointForKMS)
}
}
func TestKMSConfigTimeout(t *testing.T) {
testCases := []struct {
desc string
config string
want time.Duration
wantErr string
}{
{
desc: "duration explicitly provided",
config: "testdata/valid-configs/kms/valid-timeout.yaml",
want: 15 * time.Second,
},
{
desc: "duration explicitly provided as 0 which is an invalid value, error should be returned",
config: "testdata/invalid-configs/kms/zero-timeout.yaml",
wantErr: "timeout should be a positive value",
},
{
desc: "duration is not provided, default will be supplied",
config: "testdata/valid-configs/kms/default-timeout.yaml",
want: kmsPluginConnectionTimeout,
},
{
desc: "duration is invalid (negative), error should be returned",
config: "testdata/invalid-configs/kms/negative-timeout.yaml",
wantErr: "timeout should be a positive value",
},
}
for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) {
// mocking envelopeServiceFactory to sense the value of the supplied timeout.
envelopeServiceFactory = func(endpoint string, callTimeout time.Duration) (envelope.Service, error) {
if callTimeout != tt.want {
t.Fatalf("got timeout: %v, want %v", callTimeout, tt.want)
}
return newMockEnvelopeService(endpoint, callTimeout)
}
// mocked envelopeServiceFactory is called during ParseEncryptionConfiguration.
if _, err := ParseEncryptionConfiguration(mustConfigReader(t, tt.config)); err != nil && !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("unable to parse yaml\n%s\nerror: %v", tt.config, err)
}
})
}
}
func TestKMSPluginHealthz(t *testing.T) { func TestKMSPluginHealthz(t *testing.T) {
service, err := envelope.NewGRPCService("unix:///tmp/testprovider.sock", kmsPluginConnectionTimeout) service, err := envelope.NewGRPCService("unix:///tmp/testprovider.sock", 3*time.Second)
if err != nil { if err != nil {
t.Fatalf("Could not initialize envelopeService, error: %v", err) t.Fatalf("Could not initialize envelopeService, error: %v", err)
} }

View File

@ -1,13 +0,0 @@
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- namespaces
- secrets
providers:
- aesgcm:
keys:
- name: key1
secret: c2VjcmV0IGlzIHNlY3VyZQ==
- name: key2
secret: YSBzZWNyZXQgYSBzZWNyZXQ=

View File

@ -1,10 +0,0 @@
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- namespaces
- secrets
providers:
- aesgcm:
keys:
- name: key1

View File

@ -1,10 +0,0 @@
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- secrets
providers:
- kms:
name: foo
endpoint: unix:///tmp/testprovider.sock
timeout: -15s

View File

@ -1,9 +0,0 @@
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- secrets
providers:
- kms:
name: testprovider
cachesize: 10

View File

@ -1,10 +0,0 @@
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- secrets
providers:
- kms:
name: foo
endpoint: unix:///tmp/testprovider.sock
timeout: 0s

View File

@ -1,10 +0,0 @@
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
- resources:
- secrets
providers:
- kms:
name: foo
endpoint: unix:///tmp/testprovider.sock
timeout: 15s

View File

@ -31,9 +31,6 @@ import (
"golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte"
) )
// defaultCacheSize is the number of decrypted DEKs which would be cached by the transformer.
const defaultCacheSize = 1000
func init() { func init() {
value.RegisterMetrics() value.RegisterMetrics()
} }
@ -54,6 +51,8 @@ type envelopeTransformer struct {
// baseTransformerFunc creates a new transformer for encrypting the data with the DEK. // baseTransformerFunc creates a new transformer for encrypting the data with the DEK.
baseTransformerFunc func(cipher.Block) value.Transformer baseTransformerFunc func(cipher.Block) value.Transformer
cacheEnabled bool
} }
// NewEnvelopeTransformer returns a transformer which implements a KEK-DEK based envelope encryption scheme. // NewEnvelopeTransformer returns a transformer which implements a KEK-DEK based envelope encryption scheme.
@ -61,17 +60,22 @@ type envelopeTransformer struct {
// the data items they encrypt. A cache (of size cacheSize) is maintained to store the most recently // the data items they encrypt. A cache (of size cacheSize) is maintained to store the most recently
// used decrypted DEKs in memory. // used decrypted DEKs in memory.
func NewEnvelopeTransformer(envelopeService Service, cacheSize int, baseTransformerFunc func(cipher.Block) value.Transformer) (value.Transformer, error) { func NewEnvelopeTransformer(envelopeService Service, cacheSize int, baseTransformerFunc func(cipher.Block) value.Transformer) (value.Transformer, error) {
if cacheSize == 0 { var (
cacheSize = defaultCacheSize cache *lru.Cache
} err error
cache, err := lru.New(cacheSize) )
if err != nil {
return nil, err if cacheSize > 0 {
cache, err = lru.New(cacheSize)
if err != nil {
return nil, err
}
} }
return &envelopeTransformer{ return &envelopeTransformer{
envelopeService: envelopeService, envelopeService: envelopeService,
transformers: cache, transformers: cache,
baseTransformerFunc: baseTransformerFunc, baseTransformerFunc: baseTransformerFunc,
cacheEnabled: cacheSize > 0,
}, nil }, nil
} }
@ -91,7 +95,9 @@ func (t *envelopeTransformer) TransformFromStorage(data []byte, context value.Co
// Look up the decrypted DEK from cache or Envelope. // Look up the decrypted DEK from cache or Envelope.
transformer := t.getTransformer(encKey) transformer := t.getTransformer(encKey)
if transformer == nil { if transformer == nil {
value.RecordCacheMiss() if t.cacheEnabled {
value.RecordCacheMiss()
}
key, err := t.envelopeService.Decrypt(encKey) key, err := t.envelopeService.Decrypt(encKey)
if err != nil { if err != nil {
// Do NOT wrap this err using fmt.Errorf() or similar functions // Do NOT wrap this err using fmt.Errorf() or similar functions
@ -99,6 +105,7 @@ func (t *envelopeTransformer) TransformFromStorage(data []byte, context value.Co
// record the metric. // record the metric.
return nil, false, err return nil, false, err
} }
transformer, err = t.addTransformer(encKey, key) transformer, err = t.addTransformer(encKey, key)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@ -153,12 +160,18 @@ func (t *envelopeTransformer) addTransformer(encKey []byte, key []byte) (value.T
transformer := t.baseTransformerFunc(block) transformer := t.baseTransformerFunc(block)
// Use base64 of encKey as the key into the cache because hashicorp/golang-lru // Use base64 of encKey as the key into the cache because hashicorp/golang-lru
// cannot hash []uint8. // cannot hash []uint8.
t.transformers.Add(base64.StdEncoding.EncodeToString(encKey), transformer) if t.cacheEnabled {
t.transformers.Add(base64.StdEncoding.EncodeToString(encKey), transformer)
}
return transformer, nil return transformer, nil
} }
// getTransformer fetches the transformer corresponding to encKey from cache, if it exists. // getTransformer fetches the transformer corresponding to encKey from cache, if it exists.
func (t *envelopeTransformer) getTransformer(encKey []byte) value.Transformer { func (t *envelopeTransformer) getTransformer(encKey []byte) value.Transformer {
if !t.cacheEnabled {
return nil
}
_transformer, found := t.transformers.Get(base64.StdEncoding.EncodeToString(encKey)) _transformer, found := t.transformers.Get(base64.StdEncoding.EncodeToString(encKey))
if found { if found {
return _transformer.(value.Transformer) return _transformer.(value.Transformer)

View File

@ -78,34 +78,54 @@ func newTestEnvelopeService() *testEnvelopeService {
// Throw error if Envelope transformer tries to contact Envelope without hitting cache. // Throw error if Envelope transformer tries to contact Envelope without hitting cache.
func TestEnvelopeCaching(t *testing.T) { func TestEnvelopeCaching(t *testing.T) {
envelopeService := newTestEnvelopeService() testCases := []struct {
envelopeTransformer, err := NewEnvelopeTransformer(envelopeService, testEnvelopeCacheSize, aestransformer.NewCBCTransformer) desc string
if err != nil { cacheSize int
t.Fatalf("failed to initialize envelope transformer: %v", err) simulateKMSPluginFailure bool
} }{
context := value.DefaultContext([]byte(testContextText)) {
originalText := []byte(testText) desc: "positive cache size should withstand plugin failure",
cacheSize: 1000,
transformedData, err := envelopeTransformer.TransformToStorage(originalText, context) simulateKMSPluginFailure: true,
if err != nil { },
t.Fatalf("envelopeTransformer: error while transforming data to storage: %s", err) {
} desc: "cache disabled size should not withstand plugin failure",
untransformedData, _, err := envelopeTransformer.TransformFromStorage(transformedData, context) cacheSize: 0,
if err != nil { },
t.Fatalf("could not decrypt Envelope transformer's encrypted data even once: %v", err)
}
if !bytes.Equal(untransformedData, originalText) {
t.Fatalf("envelopeTransformer transformed data incorrectly. Expected: %v, got %v", originalText, untransformedData)
} }
envelopeService.SetDisabledStatus(true) for _, tt := range testCases {
// Subsequent read for the same data should work fine due to caching. t.Run(tt.desc, func(t *testing.T) {
untransformedData, _, err = envelopeTransformer.TransformFromStorage(transformedData, context) envelopeService := newTestEnvelopeService()
if err != nil { envelopeTransformer, err := NewEnvelopeTransformer(envelopeService, tt.cacheSize, aestransformer.NewCBCTransformer)
t.Fatalf("could not decrypt Envelope transformer's encrypted data using just cache: %v", err) if err != nil {
} t.Fatalf("failed to initialize envelope transformer: %v", err)
if !bytes.Equal(untransformedData, originalText) { }
t.Fatalf("envelopeTransformer transformed data incorrectly using cache. Expected: %v, got %v", originalText, untransformedData) context := value.DefaultContext([]byte(testContextText))
originalText := []byte(testText)
transformedData, err := envelopeTransformer.TransformToStorage(originalText, context)
if err != nil {
t.Fatalf("envelopeTransformer: error while transforming data to storage: %s", err)
}
untransformedData, _, err := envelopeTransformer.TransformFromStorage(transformedData, context)
if err != nil {
t.Fatalf("could not decrypt Envelope transformer's encrypted data even once: %v", err)
}
if !bytes.Equal(untransformedData, originalText) {
t.Fatalf("envelopeTransformer transformed data incorrectly. Expected: %v, got %v", originalText, untransformedData)
}
envelopeService.SetDisabledStatus(tt.simulateKMSPluginFailure)
// Subsequent read for the same data should work fine due to caching.
untransformedData, _, err = envelopeTransformer.TransformFromStorage(transformedData, context)
if err != nil {
t.Fatalf("could not decrypt Envelope transformer's encrypted data using just cache: %v", err)
}
if !bytes.Equal(untransformedData, originalText) {
t.Fatalf("envelopeTransformer transformed data incorrectly using cache. Got: %v, want %v", untransformedData, originalText)
}
})
} }
} }

1
vendor/modules.txt vendored
View File

@ -1289,6 +1289,7 @@ k8s.io/apiserver/pkg/apis/audit/v1beta1
k8s.io/apiserver/pkg/apis/audit/validation k8s.io/apiserver/pkg/apis/audit/validation
k8s.io/apiserver/pkg/apis/config k8s.io/apiserver/pkg/apis/config
k8s.io/apiserver/pkg/apis/config/v1 k8s.io/apiserver/pkg/apis/config/v1
k8s.io/apiserver/pkg/apis/config/validation
k8s.io/apiserver/pkg/apis/example k8s.io/apiserver/pkg/apis/example
k8s.io/apiserver/pkg/apis/example/v1 k8s.io/apiserver/pkg/apis/example/v1
k8s.io/apiserver/pkg/audit k8s.io/apiserver/pkg/audit