k8s.io/apiserver@v0.31.1/pkg/apis/apiserver/validation/validation_encryption.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package validation validates EncryptionConfiguration.
    18  package validation
    19  
    20  import (
    21  	"encoding/base64"
    22  	"fmt"
    23  	"net/url"
    24  	"strings"
    25  
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"k8s.io/apimachinery/pkg/util/validation/field"
    29  	"k8s.io/apiserver/pkg/apis/apiserver"
    30  )
    31  
    32  const (
    33  	moreThanOneElementErr          = "more than one provider specified in a single element, should split into different list elements"
    34  	keyLenErrFmt                   = "secret is not of the expected length, got %d, expected one of %v"
    35  	unsupportedSchemeErrFmt        = "unsupported scheme %q for KMS provider, only unix is supported"
    36  	unsupportedKMSAPIVersionErrFmt = "unsupported apiVersion %s for KMS provider, only v1 and v2 are supported"
    37  	atLeastOneRequiredErrFmt       = "at least one %s is required"
    38  	invalidURLErrFmt               = "invalid endpoint for kms provider, error: %v"
    39  	mandatoryFieldErrFmt           = "%s is a mandatory field for a %s"
    40  	base64EncodingErr              = "secrets must be base64 encoded"
    41  	zeroOrNegativeErrFmt           = "%s should be a positive value"
    42  	nonZeroErrFmt                  = "%s should be a positive value, or negative to disable"
    43  	encryptionConfigNilErr         = "EncryptionConfiguration can't be nil"
    44  	invalidKMSConfigNameErrFmt     = "invalid KMS provider name %s, must not contain ':'"
    45  	duplicateKMSConfigNameErrFmt   = "duplicate KMS provider name %s, names must be unique"
    46  	eventsGroupErr                 = "'*.events.k8s.io' objects are stored using the 'events' API group in etcd. Use 'events' instead in the config file"
    47  	extensionsGroupErr             = "'extensions' group has been removed and cannot be used for encryption"
    48  	starResourceErr                = "use '*.' to encrypt all the resources from core API group or *.* to encrypt all resources"
    49  	overlapErr                     = "using overlapping resources such as 'secrets' and '*.' in the same resource list is not allowed as they will be masked"
    50  	nonRESTAPIResourceErr          = "resources which do not have REST API/s cannot be encrypted"
    51  	resourceNameErr                = "resource name should not contain capital letters"
    52  	resourceAcrossGroupErr         = "encrypting the same resource across groups is not supported"
    53  	duplicateResourceErr           = "the same resource cannot be specified multiple times"
    54  )
    55  
    56  var (
    57  	// See https://golang.org/pkg/crypto/aes/#NewCipher for details on supported key sizes for AES.
    58  	aesKeySizes = []int{16, 24, 32}
    59  
    60  	// See https://godoc.org/golang.org/x/crypto/nacl/secretbox#Open for details on the supported key sizes for Secretbox.
    61  	secretBoxKeySizes = []int{32}
    62  )
    63  
    64  // ValidateEncryptionConfiguration validates a v1.EncryptionConfiguration.
    65  func ValidateEncryptionConfiguration(c *apiserver.EncryptionConfiguration, reload bool) field.ErrorList {
    66  	root := field.NewPath("resources")
    67  	allErrs := field.ErrorList{}
    68  
    69  	if c == nil {
    70  		allErrs = append(allErrs, field.Required(root, encryptionConfigNilErr))
    71  		return allErrs
    72  	}
    73  
    74  	if len(c.Resources) == 0 {
    75  		allErrs = append(allErrs, field.Required(root, fmt.Sprintf(atLeastOneRequiredErrFmt, root)))
    76  		return allErrs
    77  	}
    78  
    79  	// kmsProviderNames is used to track config names to ensure they are unique.
    80  	kmsProviderNames := sets.New[string]()
    81  	for i, conf := range c.Resources {
    82  		r := root.Index(i).Child("resources")
    83  		p := root.Index(i).Child("providers")
    84  
    85  		if len(conf.Resources) == 0 {
    86  			allErrs = append(allErrs, field.Required(r, fmt.Sprintf(atLeastOneRequiredErrFmt, r)))
    87  		}
    88  
    89  		allErrs = append(allErrs, validateResourceOverlap(conf.Resources, r)...)
    90  		allErrs = append(allErrs, validateResourceNames(conf.Resources, r)...)
    91  
    92  		if len(conf.Providers) == 0 {
    93  			allErrs = append(allErrs, field.Required(p, fmt.Sprintf(atLeastOneRequiredErrFmt, p)))
    94  		}
    95  
    96  		for j, provider := range conf.Providers {
    97  			path := p.Index(j)
    98  			allErrs = append(allErrs, validateSingleProvider(provider, path)...)
    99  
   100  			switch {
   101  			case provider.KMS != nil:
   102  				allErrs = append(allErrs, validateKMSConfiguration(provider.KMS, path.Child("kms"), kmsProviderNames, reload)...)
   103  				kmsProviderNames.Insert(provider.KMS.Name)
   104  			case provider.AESGCM != nil:
   105  				allErrs = append(allErrs, validateKeys(provider.AESGCM.Keys, path.Child("aesgcm").Child("keys"), aesKeySizes)...)
   106  			case provider.AESCBC != nil:
   107  				allErrs = append(allErrs, validateKeys(provider.AESCBC.Keys, path.Child("aescbc").Child("keys"), aesKeySizes)...)
   108  			case provider.Secretbox != nil:
   109  				allErrs = append(allErrs, validateKeys(provider.Secretbox.Keys, path.Child("secretbox").Child("keys"), secretBoxKeySizes)...)
   110  			}
   111  		}
   112  	}
   113  
   114  	return allErrs
   115  }
   116  
   117  var anyGroupAnyResource = schema.GroupResource{
   118  	Group:    "*",
   119  	Resource: "*",
   120  }
   121  
   122  func validateResourceOverlap(resources []string, fieldPath *field.Path) field.ErrorList {
   123  	if len(resources) < 2 { // cannot have overlap with a single resource
   124  		return nil
   125  	}
   126  
   127  	var allErrs field.ErrorList
   128  
   129  	r := make([]schema.GroupResource, 0, len(resources))
   130  	for _, resource := range resources {
   131  		r = append(r, schema.ParseGroupResource(resource))
   132  	}
   133  
   134  	var hasOverlap, hasDuplicate bool
   135  
   136  	for i, r1 := range r {
   137  		for j, r2 := range r {
   138  			if i == j {
   139  				continue
   140  			}
   141  
   142  			if r1 == r2 && !hasDuplicate {
   143  				hasDuplicate = true
   144  				continue
   145  			}
   146  
   147  			if hasOverlap {
   148  				continue
   149  			}
   150  
   151  			if r1 == anyGroupAnyResource {
   152  				hasOverlap = true
   153  				continue
   154  			}
   155  
   156  			if r1.Group != r2.Group {
   157  				continue
   158  			}
   159  
   160  			if r1.Resource == "*" || r2.Resource == "*" {
   161  				hasOverlap = true
   162  				continue
   163  			}
   164  		}
   165  	}
   166  
   167  	if hasDuplicate {
   168  		allErrs = append(
   169  			allErrs,
   170  			field.Invalid(
   171  				fieldPath,
   172  				resources,
   173  				duplicateResourceErr,
   174  			),
   175  		)
   176  	}
   177  
   178  	if hasOverlap {
   179  		allErrs = append(
   180  			allErrs,
   181  			field.Invalid(
   182  				fieldPath,
   183  				resources,
   184  				overlapErr,
   185  			),
   186  		)
   187  	}
   188  
   189  	return allErrs
   190  }
   191  
   192  func validateResourceNames(resources []string, fieldPath *field.Path) field.ErrorList {
   193  	var allErrs field.ErrorList
   194  
   195  	for j, res := range resources {
   196  		jj := fieldPath.Index(j)
   197  
   198  		// check if resource name has capital letters
   199  		if hasCapital(res) {
   200  			allErrs = append(
   201  				allErrs,
   202  				field.Invalid(
   203  					jj,
   204  					resources[j],
   205  					resourceNameErr,
   206  				),
   207  			)
   208  			continue
   209  		}
   210  
   211  		// check if resource is '*'
   212  		if res == "*" {
   213  			allErrs = append(
   214  				allErrs,
   215  				field.Invalid(
   216  					jj,
   217  					resources[j],
   218  					starResourceErr,
   219  				),
   220  			)
   221  			continue
   222  		}
   223  
   224  		// check if resource is:
   225  		// 'apiserveripinfo' OR
   226  		// 'serviceipallocations' OR
   227  		// 'servicenodeportallocations' OR
   228  		if res == "apiserveripinfo" ||
   229  			res == "serviceipallocations" ||
   230  			res == "servicenodeportallocations" {
   231  			allErrs = append(
   232  				allErrs,
   233  				field.Invalid(
   234  					jj,
   235  					resources[j],
   236  					nonRESTAPIResourceErr,
   237  				),
   238  			)
   239  			continue
   240  		}
   241  
   242  		// check if group is 'events.k8s.io'
   243  		gr := schema.ParseGroupResource(res)
   244  		if gr.Group == "events.k8s.io" {
   245  			allErrs = append(
   246  				allErrs,
   247  				field.Invalid(
   248  					jj,
   249  					resources[j],
   250  					eventsGroupErr,
   251  				),
   252  			)
   253  			continue
   254  		}
   255  
   256  		// check if group is 'extensions'
   257  		if gr.Group == "extensions" {
   258  			allErrs = append(
   259  				allErrs,
   260  				field.Invalid(
   261  					jj,
   262  					resources[j],
   263  					extensionsGroupErr,
   264  				),
   265  			)
   266  			continue
   267  		}
   268  
   269  		// disallow resource.* as encrypting the same resource across groups does not make sense
   270  		if gr.Group == "*" && gr.Resource != "*" {
   271  			allErrs = append(
   272  				allErrs,
   273  				field.Invalid(
   274  					jj,
   275  					resources[j],
   276  					resourceAcrossGroupErr,
   277  				),
   278  			)
   279  			continue
   280  		}
   281  	}
   282  
   283  	return allErrs
   284  }
   285  
   286  func validateSingleProvider(provider apiserver.ProviderConfiguration, fieldPath *field.Path) field.ErrorList {
   287  	allErrs := field.ErrorList{}
   288  	found := 0
   289  
   290  	if provider.KMS != nil {
   291  		found++
   292  	}
   293  	if provider.AESGCM != nil {
   294  		found++
   295  	}
   296  	if provider.AESCBC != nil {
   297  		found++
   298  	}
   299  	if provider.Secretbox != nil {
   300  		found++
   301  	}
   302  	if provider.Identity != nil {
   303  		found++
   304  	}
   305  
   306  	if found == 0 {
   307  		return append(allErrs, field.Invalid(fieldPath, provider, "provider does not contain any of the expected providers: KMS, AESGCM, AESCBC, Secretbox, Identity"))
   308  	}
   309  
   310  	if found > 1 {
   311  		return append(allErrs, field.Invalid(fieldPath, provider, moreThanOneElementErr))
   312  	}
   313  
   314  	return allErrs
   315  }
   316  
   317  func validateKeys(keys []apiserver.Key, fieldPath *field.Path, expectedLen []int) field.ErrorList {
   318  	allErrs := field.ErrorList{}
   319  
   320  	if len(keys) == 0 {
   321  		allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, "keys")))
   322  		return allErrs
   323  	}
   324  
   325  	for i, key := range keys {
   326  		allErrs = append(allErrs, validateKey(key, fieldPath.Index(i), expectedLen)...)
   327  	}
   328  
   329  	return allErrs
   330  }
   331  
   332  func validateKey(key apiserver.Key, fieldPath *field.Path, expectedLen []int) field.ErrorList {
   333  	allErrs := field.ErrorList{}
   334  
   335  	if key.Name == "" {
   336  		allErrs = append(allErrs, field.Required(fieldPath.Child("name"), fmt.Sprintf(mandatoryFieldErrFmt, "name", "key")))
   337  	}
   338  
   339  	if key.Secret == "" {
   340  		allErrs = append(allErrs, field.Required(fieldPath.Child("secret"), fmt.Sprintf(mandatoryFieldErrFmt, "secret", "key")))
   341  		return allErrs
   342  	}
   343  
   344  	secret, err := base64.StdEncoding.DecodeString(key.Secret)
   345  	if err != nil {
   346  		allErrs = append(allErrs, field.Invalid(fieldPath.Child("secret"), "REDACTED", base64EncodingErr))
   347  		return allErrs
   348  	}
   349  
   350  	lenMatched := false
   351  	for _, l := range expectedLen {
   352  		if len(secret) == l {
   353  			lenMatched = true
   354  			break
   355  		}
   356  	}
   357  
   358  	if !lenMatched {
   359  		allErrs = append(allErrs, field.Invalid(fieldPath.Child("secret"), "REDACTED", fmt.Sprintf(keyLenErrFmt, len(secret), expectedLen)))
   360  	}
   361  
   362  	return allErrs
   363  }
   364  
   365  func validateKMSConfiguration(c *apiserver.KMSConfiguration, fieldPath *field.Path, kmsProviderNames sets.Set[string], reload bool) field.ErrorList {
   366  	allErrs := field.ErrorList{}
   367  
   368  	allErrs = append(allErrs, validateKMSConfigName(c, fieldPath.Child("name"), kmsProviderNames, reload)...)
   369  	allErrs = append(allErrs, validateKMSTimeout(c, fieldPath.Child("timeout"))...)
   370  	allErrs = append(allErrs, validateKMSEndpoint(c, fieldPath.Child("endpoint"))...)
   371  	allErrs = append(allErrs, validateKMSCacheSize(c, fieldPath.Child("cachesize"))...)
   372  	allErrs = append(allErrs, validateKMSAPIVersion(c, fieldPath.Child("apiVersion"))...)
   373  	return allErrs
   374  }
   375  
   376  func validateKMSCacheSize(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
   377  	allErrs := field.ErrorList{}
   378  
   379  	// In defaulting, we set the cache size to the default value only when API version is v1.
   380  	// So, for v2 API version, we expect the cache size field to be nil.
   381  	if c.APIVersion != "v1" && c.CacheSize != nil {
   382  		allErrs = append(allErrs, field.Invalid(fieldPath, *c.CacheSize, "cachesize is not supported in v2"))
   383  	}
   384  	if c.APIVersion == "v1" && *c.CacheSize == 0 {
   385  		allErrs = append(allErrs, field.Invalid(fieldPath, *c.CacheSize, fmt.Sprintf(nonZeroErrFmt, "cachesize")))
   386  	}
   387  
   388  	return allErrs
   389  }
   390  
   391  func validateKMSTimeout(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
   392  	allErrs := field.ErrorList{}
   393  	if c.Timeout.Duration <= 0 {
   394  		allErrs = append(allErrs, field.Invalid(fieldPath, c.Timeout, fmt.Sprintf(zeroOrNegativeErrFmt, "timeout")))
   395  	}
   396  
   397  	return allErrs
   398  }
   399  
   400  func validateKMSEndpoint(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
   401  	allErrs := field.ErrorList{}
   402  	if len(c.Endpoint) == 0 {
   403  		return append(allErrs, field.Invalid(fieldPath, "", fmt.Sprintf(mandatoryFieldErrFmt, "endpoint", "kms")))
   404  	}
   405  
   406  	u, err := url.Parse(c.Endpoint)
   407  	if err != nil {
   408  		return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf(invalidURLErrFmt, err)))
   409  	}
   410  
   411  	if u.Scheme != "unix" {
   412  		return append(allErrs, field.Invalid(fieldPath, c.Endpoint, fmt.Sprintf(unsupportedSchemeErrFmt, u.Scheme)))
   413  	}
   414  
   415  	return allErrs
   416  }
   417  
   418  func validateKMSAPIVersion(c *apiserver.KMSConfiguration, fieldPath *field.Path) field.ErrorList {
   419  	allErrs := field.ErrorList{}
   420  	if c.APIVersion != "v1" && c.APIVersion != "v2" {
   421  		allErrs = append(allErrs, field.Invalid(fieldPath, c.APIVersion, fmt.Sprintf(unsupportedKMSAPIVersionErrFmt, "apiVersion")))
   422  	}
   423  
   424  	return allErrs
   425  }
   426  
   427  func validateKMSConfigName(c *apiserver.KMSConfiguration, fieldPath *field.Path, kmsProviderNames sets.Set[string], reload bool) field.ErrorList {
   428  	allErrs := field.ErrorList{}
   429  	if c.Name == "" {
   430  		allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf(mandatoryFieldErrFmt, "name", "provider")))
   431  	}
   432  
   433  	// kms v2 providers are not allowed to have a ":" in their name
   434  	if c.APIVersion != "v1" && strings.Contains(c.Name, ":") {
   435  		allErrs = append(allErrs, field.Invalid(fieldPath, c.Name, fmt.Sprintf(invalidKMSConfigNameErrFmt, c.Name)))
   436  	}
   437  
   438  	// kms v2 providers name must always be unique across all kms providers (v1 and v2)
   439  	// kms v1 provider names must be unique across all kms providers (v1 and v2) when hot reloading of encryption configuration is enabled (reload=true)
   440  	if reload || c.APIVersion != "v1" {
   441  		if kmsProviderNames.Has(c.Name) {
   442  			allErrs = append(allErrs, field.Invalid(fieldPath, c.Name, fmt.Sprintf(duplicateKMSConfigNameErrFmt, c.Name)))
   443  		}
   444  	}
   445  
   446  	return allErrs
   447  }
   448  
   449  func hasCapital(input string) bool {
   450  	return strings.ToLower(input) != input
   451  }