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 }