github.com/nginxinc/kubernetes-ingress@v1.12.5/internal/k8s/validation.go (about) 1 package k8s 2 3 import ( 4 "errors" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/nginxinc/kubernetes-ingress/internal/configs" 10 networking "k8s.io/api/networking/v1beta1" 11 "k8s.io/apimachinery/pkg/util/sets" 12 "k8s.io/apimachinery/pkg/util/validation/field" 13 ) 14 15 const ( 16 mergeableIngressTypeAnnotation = "nginx.org/mergeable-ingress-type" 17 lbMethodAnnotation = "nginx.org/lb-method" 18 healthChecksAnnotation = "nginx.com/health-checks" 19 healthChecksMandatoryAnnotation = "nginx.com/health-checks-mandatory" 20 healthChecksMandatoryQueueAnnotation = "nginx.com/health-checks-mandatory-queue" 21 slowStartAnnotation = "nginx.com/slow-start" 22 serverTokensAnnotation = "nginx.org/server-tokens" // #nosec G101 23 serverSnippetsAnnotation = "nginx.org/server-snippets" 24 locationSnippetsAnnotation = "nginx.org/location-snippets" 25 proxyConnectTimeoutAnnotation = "nginx.org/proxy-connect-timeout" 26 proxyReadTimeoutAnnotation = "nginx.org/proxy-read-timeout" 27 proxySendTimeoutAnnotation = "nginx.org/proxy-send-timeout" 28 proxyHideHeadersAnnotation = "nginx.org/proxy-hide-headers" 29 proxyPassHeadersAnnotation = "nginx.org/proxy-pass-headers" // #nosec G101 30 clientMaxBodySizeAnnotation = "nginx.org/client-max-body-size" 31 redirectToHTTPSAnnotation = "nginx.org/redirect-to-https" 32 sslRedirectAnnotation = "ingress.kubernetes.io/ssl-redirect" 33 proxyBufferingAnnotation = "nginx.org/proxy-buffering" 34 hstsAnnotation = "nginx.org/hsts" 35 hstsMaxAgeAnnotation = "nginx.org/hsts-max-age" 36 hstsIncludeSubdomainsAnnotation = "nginx.org/hsts-include-subdomains" 37 hstsBehindProxyAnnotation = "nginx.org/hsts-behind-proxy" 38 proxyBuffersAnnotation = "nginx.org/proxy-buffers" 39 proxyBufferSizeAnnotation = "nginx.org/proxy-buffer-size" 40 proxyMaxTempFileSizeAnnotation = "nginx.org/proxy-max-temp-file-size" 41 upstreamZoneSizeAnnotation = "nginx.org/upstream-zone-size" 42 jwtRealmAnnotation = "nginx.com/jwt-realm" 43 jwtKeyAnnotation = "nginx.com/jwt-key" 44 jwtTokenAnnotation = "nginx.com/jwt-token" // #nosec G101 45 jwtLoginURLAnnotation = "nginx.com/jwt-login-url" 46 listenPortsAnnotation = "nginx.org/listen-ports" 47 listenPortsSSLAnnotation = "nginx.org/listen-ports-ssl" 48 keepaliveAnnotation = "nginx.org/keepalive" 49 maxFailsAnnotation = "nginx.org/max-fails" 50 maxConnsAnnotation = "nginx.org/max-conns" 51 failTimeoutAnnotation = "nginx.org/fail-timeout" 52 appProtectEnableAnnotation = "appprotect.f5.com/app-protect-enable" 53 appProtectSecurityLogEnableAnnotation = "appprotect.f5.com/app-protect-security-log-enable" 54 internalRouteAnnotation = "nsm.nginx.com/internal-route" 55 websocketServicesAnnotation = "nginx.org/websocket-services" 56 sslServicesAnnotation = "nginx.org/ssl-services" 57 grpcServicesAnnotation = "nginx.org/grpc-services" 58 rewritesAnnotation = "nginx.org/rewrites" 59 stickyCookieServicesAnnotation = "nginx.com/sticky-cookie-services" 60 ) 61 62 type annotationValidationContext struct { 63 annotations map[string]string 64 specServices map[string]bool 65 name string 66 value string 67 isPlus bool 68 appProtectEnabled bool 69 internalRoutesEnabled bool 70 fieldPath *field.Path 71 snippetsEnabled bool 72 } 73 74 type ( 75 annotationValidationFunc func(context *annotationValidationContext) field.ErrorList 76 annotationValidationConfig map[string][]annotationValidationFunc 77 validatorFunc func(val string) error 78 ) 79 80 var ( 81 // annotationValidations defines the various validations which will be applied in order to each ingress annotation. 82 // If any specified validation fails, the remaining validations for that annotation will not be run. 83 annotationValidations = annotationValidationConfig{ 84 mergeableIngressTypeAnnotation: { 85 validateRequiredAnnotation, 86 validateMergeableIngressTypeAnnotation, 87 }, 88 lbMethodAnnotation: { 89 validateRequiredAnnotation, 90 validateLBMethodAnnotation, 91 }, 92 healthChecksAnnotation: { 93 validatePlusOnlyAnnotation, 94 validateRequiredAnnotation, 95 validateBoolAnnotation, 96 }, 97 healthChecksMandatoryAnnotation: { 98 validatePlusOnlyAnnotation, 99 validateRelatedAnnotation(healthChecksAnnotation, validateIsTrue), 100 validateRequiredAnnotation, 101 validateBoolAnnotation, 102 }, 103 healthChecksMandatoryQueueAnnotation: { 104 validatePlusOnlyAnnotation, 105 validateRelatedAnnotation(healthChecksMandatoryAnnotation, validateIsTrue), 106 validateRequiredAnnotation, 107 validateUint64Annotation, 108 }, 109 slowStartAnnotation: { 110 validatePlusOnlyAnnotation, 111 validateRequiredAnnotation, 112 validateTimeAnnotation, 113 }, 114 serverTokensAnnotation: { 115 validateRequiredAnnotation, 116 validateServerTokensAnnotation, 117 }, 118 serverSnippetsAnnotation: { 119 validateSnippetsAnnotation, 120 }, 121 locationSnippetsAnnotation: { 122 validateSnippetsAnnotation, 123 }, 124 proxyConnectTimeoutAnnotation: { 125 validateRequiredAnnotation, 126 validateTimeAnnotation, 127 }, 128 proxyReadTimeoutAnnotation: { 129 validateRequiredAnnotation, 130 validateTimeAnnotation, 131 }, 132 proxySendTimeoutAnnotation: { 133 validateRequiredAnnotation, 134 validateTimeAnnotation, 135 }, 136 proxyHideHeadersAnnotation: {}, 137 proxyPassHeadersAnnotation: {}, 138 clientMaxBodySizeAnnotation: { 139 validateRequiredAnnotation, 140 validateOffsetAnnotation, 141 }, 142 redirectToHTTPSAnnotation: { 143 validateRequiredAnnotation, 144 validateBoolAnnotation, 145 }, 146 sslRedirectAnnotation: { 147 validateRequiredAnnotation, 148 validateBoolAnnotation, 149 }, 150 proxyBufferingAnnotation: { 151 validateRequiredAnnotation, 152 validateBoolAnnotation, 153 }, 154 hstsAnnotation: { 155 validateRequiredAnnotation, 156 validateBoolAnnotation, 157 }, 158 hstsMaxAgeAnnotation: { 159 validateRelatedAnnotation(hstsAnnotation, validateIsBool), 160 validateRequiredAnnotation, 161 validateInt64Annotation, 162 }, 163 hstsIncludeSubdomainsAnnotation: { 164 validateRelatedAnnotation(hstsAnnotation, validateIsBool), 165 validateRequiredAnnotation, 166 validateBoolAnnotation, 167 }, 168 hstsBehindProxyAnnotation: { 169 validateRelatedAnnotation(hstsAnnotation, validateIsBool), 170 validateRequiredAnnotation, 171 validateBoolAnnotation, 172 }, 173 proxyBuffersAnnotation: { 174 validateRequiredAnnotation, 175 validateProxyBuffersAnnotation, 176 }, 177 proxyBufferSizeAnnotation: { 178 validateRequiredAnnotation, 179 validateSizeAnnotation, 180 }, 181 proxyMaxTempFileSizeAnnotation: { 182 validateRequiredAnnotation, 183 validateSizeAnnotation, 184 }, 185 upstreamZoneSizeAnnotation: { 186 validateRequiredAnnotation, 187 validateSizeAnnotation, 188 }, 189 jwtRealmAnnotation: { 190 validatePlusOnlyAnnotation, 191 }, 192 jwtKeyAnnotation: { 193 validatePlusOnlyAnnotation, 194 }, 195 jwtTokenAnnotation: { 196 validatePlusOnlyAnnotation, 197 }, 198 jwtLoginURLAnnotation: { 199 validatePlusOnlyAnnotation, 200 }, 201 listenPortsAnnotation: { 202 validateRequiredAnnotation, 203 validatePortListAnnotation, 204 }, 205 listenPortsSSLAnnotation: { 206 validateRequiredAnnotation, 207 validatePortListAnnotation, 208 }, 209 keepaliveAnnotation: { 210 validateRequiredAnnotation, 211 validateIntAnnotation, 212 }, 213 maxFailsAnnotation: { 214 validateRequiredAnnotation, 215 validateUint64Annotation, 216 }, 217 maxConnsAnnotation: { 218 validateRequiredAnnotation, 219 validateUint64Annotation, 220 }, 221 failTimeoutAnnotation: { 222 validateRequiredAnnotation, 223 validateTimeAnnotation, 224 }, 225 appProtectEnableAnnotation: { 226 validateAppProtectOnlyAnnotation, 227 validateRequiredAnnotation, 228 validateBoolAnnotation, 229 }, 230 appProtectSecurityLogEnableAnnotation: { 231 validateAppProtectOnlyAnnotation, 232 validateRequiredAnnotation, 233 validateBoolAnnotation, 234 }, 235 internalRouteAnnotation: { 236 validateInternalRoutesOnlyAnnotation, 237 validateRequiredAnnotation, 238 validateBoolAnnotation, 239 }, 240 websocketServicesAnnotation: { 241 validateRequiredAnnotation, 242 validateServiceListAnnotation, 243 }, 244 sslServicesAnnotation: { 245 validateRequiredAnnotation, 246 validateServiceListAnnotation, 247 }, 248 grpcServicesAnnotation: { 249 validateRequiredAnnotation, 250 validateServiceListAnnotation, 251 }, 252 rewritesAnnotation: { 253 validateRequiredAnnotation, 254 validateRewriteListAnnotation, 255 }, 256 stickyCookieServicesAnnotation: { 257 validatePlusOnlyAnnotation, 258 validateRequiredAnnotation, 259 validateStickyServiceListAnnotation, 260 }, 261 } 262 annotationNames = sortedAnnotationNames(annotationValidations) 263 ) 264 265 func sortedAnnotationNames(annotationValidations annotationValidationConfig) []string { 266 sortedNames := make([]string, 0) 267 for annotationName := range annotationValidations { 268 sortedNames = append(sortedNames, annotationName) 269 } 270 sort.Strings(sortedNames) 271 return sortedNames 272 } 273 274 // validateIngress validate an Ingress resource with rules that our Ingress Controller enforces. 275 // Note that the full validation of Ingress resources is done by Kubernetes. 276 func validateIngress( 277 ing *networking.Ingress, 278 isPlus bool, 279 appProtectEnabled bool, 280 internalRoutesEnabled bool, 281 snippetsEnabled bool, 282 ) field.ErrorList { 283 allErrs := field.ErrorList{} 284 allErrs = append(allErrs, validateIngressAnnotations( 285 ing.Annotations, 286 getSpecServices(ing.Spec), 287 isPlus, 288 appProtectEnabled, 289 internalRoutesEnabled, 290 field.NewPath("annotations"), 291 snippetsEnabled, 292 )...) 293 294 allErrs = append(allErrs, validateIngressSpec(&ing.Spec, field.NewPath("spec"))...) 295 296 if isMaster(ing) { 297 allErrs = append(allErrs, validateMasterSpec(&ing.Spec, field.NewPath("spec"))...) 298 } else if isMinion(ing) { 299 allErrs = append(allErrs, validateMinionSpec(&ing.Spec, field.NewPath("spec"))...) 300 } 301 302 return allErrs 303 } 304 305 func validateIngressAnnotations( 306 annotations map[string]string, 307 specServices map[string]bool, 308 isPlus bool, 309 appProtectEnabled bool, 310 internalRoutesEnabled bool, 311 fieldPath *field.Path, 312 snippetsEnabled bool, 313 ) field.ErrorList { 314 allErrs := field.ErrorList{} 315 316 for _, name := range annotationNames { 317 if value, exists := annotations[name]; exists { 318 context := &annotationValidationContext{ 319 annotations: annotations, 320 specServices: specServices, 321 name: name, 322 value: value, 323 isPlus: isPlus, 324 appProtectEnabled: appProtectEnabled, 325 internalRoutesEnabled: internalRoutesEnabled, 326 fieldPath: fieldPath.Child(name), 327 snippetsEnabled: snippetsEnabled, 328 } 329 allErrs = append(allErrs, validateIngressAnnotation(context)...) 330 } 331 } 332 333 return allErrs 334 } 335 336 func validateIngressAnnotation(context *annotationValidationContext) field.ErrorList { 337 allErrs := field.ErrorList{} 338 if validationFuncs, exists := annotationValidations[context.name]; exists { 339 for _, validationFunc := range validationFuncs { 340 valErrors := validationFunc(context) 341 if len(valErrors) > 0 { 342 allErrs = append(allErrs, valErrors...) 343 break 344 } 345 } 346 } 347 return allErrs 348 } 349 350 func validateRelatedAnnotation(name string, validator validatorFunc) annotationValidationFunc { 351 return func(context *annotationValidationContext) field.ErrorList { 352 allErrs := field.ErrorList{} 353 val, exists := context.annotations[name] 354 if !exists { 355 return append(allErrs, field.Forbidden(context.fieldPath, fmt.Sprintf("related annotation %s: must be set", name))) 356 } 357 358 if err := validator(val); err != nil { 359 return append(allErrs, field.Forbidden(context.fieldPath, fmt.Sprintf("related annotation %s: %s", name, err.Error()))) 360 } 361 return allErrs 362 } 363 } 364 365 func validateMergeableIngressTypeAnnotation(context *annotationValidationContext) field.ErrorList { 366 allErrs := field.ErrorList{} 367 if context.value != "master" && context.value != "minion" { 368 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be one of: 'master' or 'minion'")) 369 } 370 return allErrs 371 } 372 373 func validateLBMethodAnnotation(context *annotationValidationContext) field.ErrorList { 374 allErrs := field.ErrorList{} 375 376 parseFunc := configs.ParseLBMethod 377 if context.isPlus { 378 parseFunc = configs.ParseLBMethodForPlus 379 } 380 381 if _, err := parseFunc(context.value); err != nil { 382 return append(allErrs, field.Invalid(context.fieldPath, context.value, err.Error())) 383 } 384 return allErrs 385 } 386 387 func validateServerTokensAnnotation(context *annotationValidationContext) field.ErrorList { 388 allErrs := field.ErrorList{} 389 if !context.isPlus { 390 if _, err := configs.ParseBool(context.value); err != nil { 391 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a boolean")) 392 } 393 } 394 return allErrs 395 } 396 397 func validateRequiredAnnotation(context *annotationValidationContext) field.ErrorList { 398 allErrs := field.ErrorList{} 399 if context.value == "" { 400 return append(allErrs, field.Required(context.fieldPath, "")) 401 } 402 return allErrs 403 } 404 405 func validatePlusOnlyAnnotation(context *annotationValidationContext) field.ErrorList { 406 allErrs := field.ErrorList{} 407 if !context.isPlus { 408 return append(allErrs, field.Forbidden(context.fieldPath, "annotation requires NGINX Plus")) 409 } 410 return allErrs 411 } 412 413 func validateAppProtectOnlyAnnotation(context *annotationValidationContext) field.ErrorList { 414 allErrs := field.ErrorList{} 415 if !context.appProtectEnabled { 416 return append(allErrs, field.Forbidden(context.fieldPath, "annotation requires AppProtect")) 417 } 418 return allErrs 419 } 420 421 func validateInternalRoutesOnlyAnnotation(context *annotationValidationContext) field.ErrorList { 422 allErrs := field.ErrorList{} 423 if !context.internalRoutesEnabled { 424 return append(allErrs, field.Forbidden(context.fieldPath, "annotation requires Internal Routes enabled")) 425 } 426 return allErrs 427 } 428 429 func validateBoolAnnotation(context *annotationValidationContext) field.ErrorList { 430 allErrs := field.ErrorList{} 431 if _, err := configs.ParseBool(context.value); err != nil { 432 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a boolean")) 433 } 434 return allErrs 435 } 436 437 func validateTimeAnnotation(context *annotationValidationContext) field.ErrorList { 438 allErrs := field.ErrorList{} 439 if _, err := configs.ParseTime(context.value); err != nil { 440 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a time")) 441 } 442 return allErrs 443 } 444 445 func validateOffsetAnnotation(context *annotationValidationContext) field.ErrorList { 446 allErrs := field.ErrorList{} 447 if _, err := configs.ParseOffset(context.value); err != nil { 448 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be an offset")) 449 } 450 return allErrs 451 } 452 453 func validateSizeAnnotation(context *annotationValidationContext) field.ErrorList { 454 allErrs := field.ErrorList{} 455 if _, err := configs.ParseSize(context.value); err != nil { 456 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a size")) 457 } 458 return allErrs 459 } 460 461 func validateProxyBuffersAnnotation(context *annotationValidationContext) field.ErrorList { 462 allErrs := field.ErrorList{} 463 if _, err := configs.ParseProxyBuffersSpec(context.value); err != nil { 464 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a proxy buffer spec")) 465 } 466 return allErrs 467 } 468 469 func validateUint64Annotation(context *annotationValidationContext) field.ErrorList { 470 allErrs := field.ErrorList{} 471 if _, err := configs.ParseUint64(context.value); err != nil { 472 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a non-negative integer")) 473 } 474 return allErrs 475 } 476 477 func validateInt64Annotation(context *annotationValidationContext) field.ErrorList { 478 allErrs := field.ErrorList{} 479 if _, err := configs.ParseInt64(context.value); err != nil { 480 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be an integer")) 481 } 482 return allErrs 483 } 484 485 func validateIntAnnotation(context *annotationValidationContext) field.ErrorList { 486 allErrs := field.ErrorList{} 487 if _, err := configs.ParseInt(context.value); err != nil { 488 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be an integer")) 489 } 490 return allErrs 491 } 492 493 func validatePortListAnnotation(context *annotationValidationContext) field.ErrorList { 494 allErrs := field.ErrorList{} 495 if _, err := configs.ParsePortList(context.value); err != nil { 496 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a comma-separated list of port numbers")) 497 } 498 return allErrs 499 } 500 501 func validateServiceListAnnotation(context *annotationValidationContext) field.ErrorList { 502 allErrs := field.ErrorList{} 503 var unknownServices []string 504 annotationServices := configs.ParseServiceList(context.value) 505 for svc := range annotationServices { 506 if _, exists := context.specServices[svc]; !exists { 507 unknownServices = append(unknownServices, svc) 508 } 509 } 510 if len(unknownServices) > 0 { 511 errorMsg := fmt.Sprintf( 512 "must be a comma-separated list of services. The following services were not found: %s", 513 strings.Join(unknownServices, ","), 514 ) 515 return append(allErrs, field.Invalid(context.fieldPath, context.value, errorMsg)) 516 } 517 return allErrs 518 } 519 520 func validateStickyServiceListAnnotation(context *annotationValidationContext) field.ErrorList { 521 allErrs := field.ErrorList{} 522 if _, err := configs.ParseStickyServiceList(context.value); err != nil { 523 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a semicolon-separated list of sticky services")) 524 } 525 return allErrs 526 } 527 528 func validateRewriteListAnnotation(context *annotationValidationContext) field.ErrorList { 529 allErrs := field.ErrorList{} 530 if _, err := configs.ParseRewriteList(context.value); err != nil { 531 return append(allErrs, field.Invalid(context.fieldPath, context.value, "must be a semicolon-separated list of rewrites")) 532 } 533 return allErrs 534 } 535 536 func validateSnippetsAnnotation(context *annotationValidationContext) field.ErrorList { 537 allErrs := field.ErrorList{} 538 539 if !context.snippetsEnabled { 540 return append(allErrs, field.Forbidden(context.fieldPath, "snippet specified but snippets feature is not enabled")) 541 } 542 return allErrs 543 } 544 545 func validateIsBool(v string) error { 546 _, err := configs.ParseBool(v) 547 return err 548 } 549 550 func validateIsTrue(v string) error { 551 b, err := configs.ParseBool(v) 552 if err != nil { 553 return err 554 } 555 if !b { 556 return errors.New("must be true") 557 } 558 return nil 559 } 560 561 func validateIngressSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList { 562 allErrs := field.ErrorList{} 563 564 allHosts := sets.String{} 565 566 if len(spec.Rules) == 0 { 567 return append(allErrs, field.Required(fieldPath.Child("rules"), "")) 568 } 569 570 for i, r := range spec.Rules { 571 idxPath := fieldPath.Child("rules").Index(i) 572 573 if r.Host == "" { 574 allErrs = append(allErrs, field.Required(idxPath.Child("host"), "")) 575 } else if allHosts.Has(r.Host) { 576 allErrs = append(allErrs, field.Duplicate(idxPath.Child("host"), r.Host)) 577 } else { 578 allHosts.Insert(r.Host) 579 } 580 } 581 582 return allErrs 583 } 584 585 func validateMasterSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList { 586 allErrs := field.ErrorList{} 587 588 if len(spec.Rules) != 1 { 589 return append(allErrs, field.TooMany(fieldPath.Child("rules"), len(spec.Rules), 1)) 590 } 591 592 // the number of paths of the first rule of the spec must be 0 593 if spec.Rules[0].HTTP != nil && len(spec.Rules[0].HTTP.Paths) > 0 { 594 pathsField := fieldPath.Child("rules").Index(0).Child("http").Child("paths") 595 return append(allErrs, field.TooMany(pathsField, len(spec.Rules[0].HTTP.Paths), 0)) 596 } 597 598 return allErrs 599 } 600 601 func validateMinionSpec(spec *networking.IngressSpec, fieldPath *field.Path) field.ErrorList { 602 allErrs := field.ErrorList{} 603 604 if len(spec.TLS) > 0 { 605 allErrs = append(allErrs, field.TooMany(fieldPath.Child("tls"), len(spec.TLS), 0)) 606 } 607 608 if len(spec.Rules) != 1 { 609 return append(allErrs, field.TooMany(fieldPath.Child("rules"), len(spec.Rules), 1)) 610 } 611 612 // the number of paths of the first rule of the spec must be greater than 0 613 if spec.Rules[0].HTTP == nil || len(spec.Rules[0].HTTP.Paths) == 0 { 614 pathsField := fieldPath.Child("rules").Index(0).Child("http").Child("paths") 615 return append(allErrs, field.Required(pathsField, "must include at least one path")) 616 } 617 618 return allErrs 619 } 620 621 func getSpecServices(ingressSpec networking.IngressSpec) map[string]bool { 622 services := make(map[string]bool) 623 if ingressSpec.Backend != nil { 624 services[ingressSpec.Backend.ServiceName] = true 625 } 626 for _, rule := range ingressSpec.Rules { 627 if rule.HTTP != nil { 628 for _, path := range rule.HTTP.Paths { 629 services[path.Backend.ServiceName] = true 630 } 631 } 632 } 633 return services 634 }