Merge pull request #113697 from aramase/kms-duplication-name-validation-part-2

[KMS]: add validation for duplicate kms config name when auto reload is enabled
This commit is contained in:
Kubernetes Prow Robot 2022-11-07 16:02:07 -08:00 committed by GitHub
commit a236e4ca6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 290 additions and 22 deletions

View File

@ -23,6 +23,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/config" "k8s.io/apiserver/pkg/apis/config"
) )
@ -40,6 +41,7 @@ const (
nonZeroErrFmt = "%s should be a positive value, or negative to disable" nonZeroErrFmt = "%s should be a positive value, or negative to disable"
encryptionConfigNilErr = "EncryptionConfiguration can't be nil" encryptionConfigNilErr = "EncryptionConfiguration can't be nil"
invalidKMSConfigNameErrFmt = "invalid KMS provider name %s, must not contain ':'" invalidKMSConfigNameErrFmt = "invalid KMS provider name %s, must not contain ':'"
duplicateKMSConfigNameErrFmt = "duplicate KMS provider name %s, names must be unique"
) )
var ( var (
@ -53,7 +55,7 @@ var (
) )
// ValidateEncryptionConfiguration validates a v1.EncryptionConfiguration. // ValidateEncryptionConfiguration validates a v1.EncryptionConfiguration.
func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration) field.ErrorList { func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration, reload bool) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if c == nil { if c == nil {
@ -66,6 +68,8 @@ func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration) field.Er
return allErrs return allErrs
} }
// kmsProviderNames is used to track config names to ensure they are unique.
kmsProviderNames := sets.NewString()
for i, conf := range c.Resources { for i, conf := range c.Resources {
r := root.Index(i).Child("resources") r := root.Index(i).Child("resources")
p := root.Index(i).Child("providers") p := root.Index(i).Child("providers")
@ -84,7 +88,8 @@ func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration) field.Er
switch { switch {
case provider.KMS != nil: case provider.KMS != nil:
allErrs = append(allErrs, validateKMSConfiguration(provider.KMS, path.Child("kms"))...) allErrs = append(allErrs, validateKMSConfiguration(provider.KMS, path.Child("kms"), kmsProviderNames, reload)...)
kmsProviderNames.Insert(provider.KMS.Name)
case provider.AESGCM != nil: case provider.AESGCM != nil:
allErrs = append(allErrs, validateKeys(provider.AESGCM.Keys, path.Child("aesgcm").Child("keys"), aesKeySizes)...) allErrs = append(allErrs, validateKeys(provider.AESGCM.Keys, path.Child("aesgcm").Child("keys"), aesKeySizes)...)
case provider.AESCBC != nil: case provider.AESCBC != nil:
@ -98,7 +103,7 @@ func ValidateEncryptionConfiguration(c *config.EncryptionConfiguration) field.Er
return allErrs return allErrs
} }
func validateSingleProvider(provider config.ProviderConfiguration, filedPath *field.Path) field.ErrorList { func validateSingleProvider(provider config.ProviderConfiguration, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
found := 0 found := 0
@ -119,11 +124,11 @@ func validateSingleProvider(provider config.ProviderConfiguration, filedPath *fi
} }
if found == 0 { if found == 0 {
return append(allErrs, field.Invalid(filedPath, provider, "provider does not contain any of the expected providers: KMS, AESGCM, AESCBC, Secretbox, Identity")) return append(allErrs, field.Invalid(fieldPath, provider, "provider does not contain any of the expected providers: KMS, AESGCM, AESCBC, Secretbox, Identity"))
} }
if found > 1 { if found > 1 {
return append(allErrs, field.Invalid(filedPath, provider, moreThanOneElementErr)) return append(allErrs, field.Invalid(fieldPath, provider, moreThanOneElementErr))
} }
return allErrs return allErrs
@ -177,10 +182,10 @@ func validateKey(key config.Key, fieldPath *field.Path, expectedLen []int) field
return allErrs return allErrs
} }
func validateKMSConfiguration(c *config.KMSConfiguration, fieldPath *field.Path) field.ErrorList { func validateKMSConfiguration(c *config.KMSConfiguration, fieldPath *field.Path, kmsProviderNames sets.String, reload bool) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
allErrs = append(allErrs, validateKMSConfigName(c, fieldPath.Child("name"))...) allErrs = append(allErrs, validateKMSConfigName(c, fieldPath.Child("name"), kmsProviderNames, reload)...)
allErrs = append(allErrs, validateKMSTimeout(c, fieldPath.Child("timeout"))...) allErrs = append(allErrs, validateKMSTimeout(c, fieldPath.Child("timeout"))...)
allErrs = append(allErrs, validateKMSEndpoint(c, fieldPath.Child("endpoint"))...) allErrs = append(allErrs, validateKMSEndpoint(c, fieldPath.Child("endpoint"))...)
allErrs = append(allErrs, validateKMSCacheSize(c, fieldPath.Child("cachesize"))...) allErrs = append(allErrs, validateKMSCacheSize(c, fieldPath.Child("cachesize"))...)
@ -233,15 +238,24 @@ func validateKMSAPIVersion(c *config.KMSConfiguration, fieldPath *field.Path) fi
return allErrs return allErrs
} }
func validateKMSConfigName(c *config.KMSConfiguration, fieldPath *field.Path) field.ErrorList { func validateKMSConfigName(c *config.KMSConfiguration, fieldPath *field.Path, kmsProviderNames sets.String, reload bool) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if c.Name == "" { if c.Name == "" {
allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf(mandatoryFieldErrFmt, "name", "provider"))) allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf(mandatoryFieldErrFmt, "name", "provider")))
} }
// kms v2 providers are not allowed to have a ":" in their name
if c.APIVersion != "v1" && strings.Contains(c.Name, ":") { if c.APIVersion != "v1" && strings.Contains(c.Name, ":") {
allErrs = append(allErrs, field.Invalid(fieldPath, c.Name, fmt.Sprintf(invalidKMSConfigNameErrFmt, c.Name))) allErrs = append(allErrs, field.Invalid(fieldPath, c.Name, fmt.Sprintf(invalidKMSConfigNameErrFmt, c.Name)))
} }
// kms v2 providers name must always be unique across all kms providers (v1 and v2)
// kms v1 provider names must be unique across all kms providers (v1 and v2) when hot reloading of encryption configuration is enabled (reload=true)
if reload || c.APIVersion != "v1" {
if kmsProviderNames.Has(c.Name) {
allErrs = append(allErrs, field.Invalid(fieldPath, c.Name, fmt.Sprintf(duplicateKMSConfigNameErrFmt, c.Name)))
}
}
return allErrs return allErrs
} }

View File

@ -23,16 +23,19 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/config" "k8s.io/apiserver/pkg/apis/config"
) )
func TestStructure(t *testing.T) { func TestStructure(t *testing.T) {
firstResourcePath := root.Index(0) firstResourcePath := root.Index(0)
cacheSize := int32(1)
testCases := []struct { testCases := []struct {
desc string desc string
in *config.EncryptionConfiguration in *config.EncryptionConfiguration
want field.ErrorList reload bool
want field.ErrorList
}{ }{
{ {
desc: "nil encryption config", desc: "nil encryption config",
@ -161,13 +164,225 @@ func TestStructure(t *testing.T) {
}, },
want: field.ErrorList{}, want: field.ErrorList{},
}, },
{
desc: "duplicate kms v2 config name with kms v1 config",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-1.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v1",
},
},
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-2.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v2",
},
},
},
},
},
},
want: field.ErrorList{
field.Invalid(firstResourcePath.Child("providers").Index(1).Child("kms").Child("name"),
"foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
{
desc: "duplicate kms v2 config names",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-1.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v2",
},
},
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-2.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v2",
},
},
},
},
},
},
want: field.ErrorList{
field.Invalid(firstResourcePath.Child("providers").Index(1).Child("kms").Child("name"),
"foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
{
desc: "duplicate kms v2 config name across providers",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-1.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v2",
},
},
},
},
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-2.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v2",
},
},
},
},
},
},
want: field.ErrorList{
field.Invalid(root.Index(1).Child("providers").Index(0).Child("kms").Child("name"),
"foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
{
desc: "duplicate kms config name with v1 and v2 across providers",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-1.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v1",
},
},
},
},
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-2.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v2",
},
},
},
},
},
},
want: field.ErrorList{
field.Invalid(root.Index(1).Child("providers").Index(0).Child("kms").Child("name"),
"foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
{
desc: "duplicate kms v1 config names shouldn't error",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-1.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v1",
},
},
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-2.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v1",
},
},
},
},
},
},
want: field.ErrorList{},
},
{
desc: "duplicate kms v1 config names should error when reload=true",
in: &config.EncryptionConfiguration{
Resources: []config.ResourceConfiguration{
{
Resources: []string{"secrets"},
Providers: []config.ProviderConfiguration{
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-1.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v1",
},
},
{
KMS: &config.KMSConfiguration{
Name: "foo",
Endpoint: "unix:///tmp/kms-provider-2.socket",
Timeout: &metav1.Duration{Duration: 3 * time.Second},
CacheSize: &cacheSize,
APIVersion: "v1",
},
},
},
},
},
},
reload: true,
want: field.ErrorList{
field.Invalid(root.Index(0).Child("providers").Index(1).Child("kms").Child("name"),
"foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
} }
for _, tt := range testCases { for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
got := ValidateEncryptionConfiguration(tt.in) got := ValidateEncryptionConfiguration(tt.in, tt.reload)
if d := cmp.Diff(tt.want, got); d != "" { if d := cmp.Diff(tt.want, got); d != "" {
t.Fatalf("EncryptionConfiguratoin validation results mismatch (-want +got):\n%s", d) t.Fatalf("EncryptionConfiguration validation results mismatch (-want +got):\n%s", d)
} }
}) })
} }
@ -392,9 +607,11 @@ func TestKMSProviderName(t *testing.T) {
nameField := field.NewPath("Resource").Index(0).Child("Provider").Index(0).Child("KMS").Child("name") nameField := field.NewPath("Resource").Index(0).Child("Provider").Index(0).Child("KMS").Child("name")
testCases := []struct { testCases := []struct {
desc string desc string
in *config.KMSConfiguration in *config.KMSConfiguration
want field.ErrorList reload bool
kmsProviderNames sets.String
want field.ErrorList
}{ }{
{ {
desc: "valid name", desc: "valid name",
@ -415,11 +632,48 @@ func TestKMSProviderName(t *testing.T) {
field.Invalid(nameField, "foo:bar", fmt.Sprintf(invalidKMSConfigNameErrFmt, "foo:bar")), field.Invalid(nameField, "foo:bar", fmt.Sprintf(invalidKMSConfigNameErrFmt, "foo:bar")),
}, },
}, },
{
desc: "invalid name with : but api version is v1",
in: &config.KMSConfiguration{Name: "foo:bar", APIVersion: "v1"},
want: field.ErrorList{},
},
{
desc: "duplicate name, kms v2, reload=false",
in: &config.KMSConfiguration{APIVersion: "v2", Name: "foo"},
kmsProviderNames: sets.NewString("foo"),
want: field.ErrorList{
field.Invalid(nameField, "foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
{
desc: "duplicate name, kms v2, reload=true",
in: &config.KMSConfiguration{APIVersion: "v2", Name: "foo"},
reload: true,
kmsProviderNames: sets.NewString("foo"),
want: field.ErrorList{
field.Invalid(nameField, "foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
{
desc: "duplicate name, kms v1, reload=false",
in: &config.KMSConfiguration{APIVersion: "v1", Name: "foo"},
kmsProviderNames: sets.NewString("foo"),
want: field.ErrorList{},
},
{
desc: "duplicate name, kms v1, reload=true",
in: &config.KMSConfiguration{APIVersion: "v1", Name: "foo"},
reload: true,
kmsProviderNames: sets.NewString("foo"),
want: field.ErrorList{
field.Invalid(nameField, "foo", fmt.Sprintf(duplicateKMSConfigNameErrFmt, "foo")),
},
},
} }
for _, tt := range testCases { for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
got := validateKMSConfigName(tt.in, nameField) got := validateKMSConfigName(tt.in, nameField, tt.kmsProviderNames, tt.reload)
if d := cmp.Diff(tt.want, got); d != "" { if d := cmp.Diff(tt.want, got); d != "" {
t.Fatalf("KMS Provider validation mismatch (-want +got):\n%s", d) t.Fatalf("KMS Provider validation mismatch (-want +got):\n%s", d)
} }

View File

@ -117,7 +117,7 @@ func (h *kmsv2PluginProbe) toHealthzCheck(idx int) healthz.HealthChecker {
// It may launch multiple go routines whose lifecycle is controlled by stopCh. // It may launch multiple go routines whose lifecycle is controlled by stopCh.
// If reload is true, or KMS v2 plugins are used with no KMS v1 plugins, the returned slice of health checkers will always be of length 1. // If reload is true, or KMS v2 plugins are used with no KMS v1 plugins, the returned slice of health checkers will always be of length 1.
func LoadEncryptionConfig(filepath string, reload bool, stopCh <-chan struct{}) (map[schema.GroupResource]value.Transformer, []healthz.HealthChecker, error) { func LoadEncryptionConfig(filepath string, reload bool, stopCh <-chan struct{}) (map[schema.GroupResource]value.Transformer, []healthz.HealthChecker, error) {
config, err := loadConfig(filepath) config, err := loadConfig(filepath, reload)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error while parsing file: %w", err) return nil, nil, fmt.Errorf("error while parsing file: %w", err)
} }
@ -262,7 +262,7 @@ func isKMSv2ProviderHealthy(name string, response *envelopekmsv2.StatusResponse)
return nil return nil
} }
func loadConfig(filepath string) (*apiserverconfig.EncryptionConfiguration, error) { func loadConfig(filepath string, reload bool) (*apiserverconfig.EncryptionConfiguration, error) {
f, err := os.Open(filepath) f, err := os.Open(filepath)
if err != nil { if err != nil {
return nil, fmt.Errorf("error opening encryption provider configuration file %q: %w", filepath, err) return nil, fmt.Errorf("error opening encryption provider configuration file %q: %w", filepath, err)
@ -291,7 +291,7 @@ func loadConfig(filepath string) (*apiserverconfig.EncryptionConfiguration, erro
return nil, fmt.Errorf("got unexpected config type: %v", gvk) return nil, fmt.Errorf("got unexpected config type: %v", gvk)
} }
return config, validation.ValidateEncryptionConfiguration(config).ToAggregate() return config, validation.ValidateEncryptionConfiguration(config, reload).ToAggregate()
} }
func prefixTransformersAndProbes(config apiserverconfig.ResourceConfiguration, stopCh <-chan struct{}) ([]value.PrefixTransformer, []healthChecker, *kmsState, error) { func prefixTransformersAndProbes(config apiserverconfig.ResourceConfiguration, stopCh <-chan struct{}) ([]value.PrefixTransformer, []healthChecker, *kmsState, error) {

View File

@ -114,7 +114,7 @@ func newMockErrorEnvelopeKMSv2Service(endpoint string, timeout time.Duration) (e
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(legacyV1Config) legacyConfigObject, err := loadConfig(legacyV1Config, false)
cacheSize := int32(10) 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)
@ -323,7 +323,7 @@ func TestKMSPluginHealthz(t *testing.T) {
for _, tt := range testCases { for _, tt := range testCases {
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
config, err := loadConfig(tt.config) config, err := loadConfig(tt.config, false)
if errStr := errString(err); errStr != tt.wantErr { if errStr := errString(err); errStr != tt.wantErr {
t.Fatalf("unexpected error state got=%s want=%s", errStr, tt.wantErr) t.Fatalf("unexpected error state got=%s want=%s", errStr, tt.wantErr)
} }