github.com/nginxinc/kubernetes-ingress@v1.12.5/pkg/apis/configuration/validation/policy.go (about) 1 package validation 2 3 import ( 4 "fmt" 5 "net" 6 "net/url" 7 "regexp" 8 "strconv" 9 "strings" 10 11 "github.com/nginxinc/kubernetes-ingress/internal/k8s/appprotect" 12 v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" 13 "k8s.io/apimachinery/pkg/util/validation" 14 "k8s.io/apimachinery/pkg/util/validation/field" 15 ) 16 17 // ValidatePolicy validates a Policy. 18 func ValidatePolicy(policy *v1.Policy, isPlus, enablePreviewPolicies, enableAppProtect bool) error { 19 allErrs := validatePolicySpec(&policy.Spec, field.NewPath("spec"), isPlus, enablePreviewPolicies, enableAppProtect) 20 return allErrs.ToAggregate() 21 } 22 23 func validatePolicySpec(spec *v1.PolicySpec, fieldPath *field.Path, isPlus, enablePreviewPolicies, enableAppProtect bool) field.ErrorList { 24 allErrs := field.ErrorList{} 25 26 fieldCount := 0 27 28 if spec.AccessControl != nil { 29 allErrs = append(allErrs, validateAccessControl(spec.AccessControl, fieldPath.Child("accessControl"))...) 30 fieldCount++ 31 } 32 33 if spec.RateLimit != nil { 34 if !enablePreviewPolicies { 35 return append(allErrs, field.Forbidden(fieldPath.Child("rateLimit"), 36 "rateLimit is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies")) 37 } 38 allErrs = append(allErrs, validateRateLimit(spec.RateLimit, fieldPath.Child("rateLimit"), isPlus)...) 39 fieldCount++ 40 } 41 42 if spec.JWTAuth != nil { 43 if !enablePreviewPolicies { 44 allErrs = append(allErrs, field.Forbidden(fieldPath.Child("jwt"), 45 "jwt is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies")) 46 } 47 if !isPlus { 48 return append(allErrs, field.Forbidden(fieldPath.Child("jwt"), "jwt secrets are only supported in NGINX Plus")) 49 } 50 51 allErrs = append(allErrs, validateJWT(spec.JWTAuth, fieldPath.Child("jwt"))...) 52 fieldCount++ 53 } 54 55 if spec.IngressMTLS != nil { 56 if !enablePreviewPolicies { 57 return append(allErrs, field.Forbidden(fieldPath.Child("ingressMTLS"), 58 "ingressMTLS is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies")) 59 } 60 allErrs = append(allErrs, validateIngressMTLS(spec.IngressMTLS, fieldPath.Child("ingressMTLS"))...) 61 fieldCount++ 62 } 63 64 if spec.EgressMTLS != nil { 65 if !enablePreviewPolicies { 66 return append(allErrs, field.Forbidden(fieldPath.Child("egressMTLS"), 67 "egressMTLS is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies")) 68 } 69 allErrs = append(allErrs, validateEgressMTLS(spec.EgressMTLS, fieldPath.Child("egressMTLS"))...) 70 fieldCount++ 71 } 72 73 if spec.OIDC != nil { 74 if !enablePreviewPolicies { 75 allErrs = append(allErrs, field.Forbidden(fieldPath.Child("oidc"), 76 "oidc is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies")) 77 } 78 if !isPlus { 79 return append(allErrs, field.Forbidden(fieldPath.Child("oidc"), "OIDC is only supported in NGINX Plus")) 80 } 81 82 allErrs = append(allErrs, validateOIDC(spec.OIDC, fieldPath.Child("oidc"))...) 83 fieldCount++ 84 } 85 86 if spec.WAF != nil { 87 if !enablePreviewPolicies { 88 allErrs = append(allErrs, field.Forbidden(fieldPath.Child("waf"), 89 "waf is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies")) 90 } 91 if !isPlus { 92 allErrs = append(allErrs, field.Forbidden(fieldPath.Child("waf"), "WAF is only supported in NGINX Plus")) 93 } 94 if !enableAppProtect { 95 allErrs = append(allErrs, field.Forbidden(fieldPath.Child("waf"), 96 "App Protect must be enabled via cli argument -enable-appprotect to use WAF policy")) 97 } 98 99 allErrs = append(allErrs, validateWAF(spec.WAF, fieldPath.Child("waf"))...) 100 fieldCount++ 101 } 102 103 if fieldCount != 1 { 104 msg := "must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`" 105 if isPlus { 106 msg = fmt.Sprint(msg, ", `jwt`, `oidc`, `waf`") 107 } 108 allErrs = append(allErrs, field.Invalid(fieldPath, "", msg)) 109 } 110 111 return allErrs 112 } 113 114 func validateAccessControl(accessControl *v1.AccessControl, fieldPath *field.Path) field.ErrorList { 115 allErrs := field.ErrorList{} 116 117 fieldCount := 0 118 119 if accessControl.Allow != nil { 120 for i, ipOrCIDR := range accessControl.Allow { 121 allErrs = append(allErrs, validateIPorCIDR(ipOrCIDR, fieldPath.Child("allow").Index(i))...) 122 } 123 fieldCount++ 124 } 125 126 if accessControl.Deny != nil { 127 for i, ipOrCIDR := range accessControl.Deny { 128 allErrs = append(allErrs, validateIPorCIDR(ipOrCIDR, fieldPath.Child("deny").Index(i))...) 129 } 130 fieldCount++ 131 } 132 133 if fieldCount != 1 { 134 allErrs = append(allErrs, field.Invalid(fieldPath, "", "must specify exactly one of: `allow` or `deny`")) 135 } 136 137 return allErrs 138 } 139 140 func validateRateLimit(rateLimit *v1.RateLimit, fieldPath *field.Path, isPlus bool) field.ErrorList { 141 allErrs := field.ErrorList{} 142 143 allErrs = append(allErrs, validateRateLimitZoneSize(rateLimit.ZoneSize, fieldPath.Child("zoneSize"))...) 144 allErrs = append(allErrs, validateRate(rateLimit.Rate, fieldPath.Child("rate"))...) 145 allErrs = append(allErrs, validateRateLimitKey(rateLimit.Key, fieldPath.Child("key"), isPlus)...) 146 147 if rateLimit.Delay != nil { 148 allErrs = append(allErrs, validatePositiveInt(*rateLimit.Delay, fieldPath.Child("delay"))...) 149 } 150 151 if rateLimit.Burst != nil { 152 allErrs = append(allErrs, validatePositiveInt(*rateLimit.Burst, fieldPath.Child("burst"))...) 153 } 154 155 if rateLimit.LogLevel != "" { 156 allErrs = append(allErrs, validateRateLimitLogLevel(rateLimit.LogLevel, fieldPath.Child("logLevel"))...) 157 } 158 159 if rateLimit.RejectCode != nil { 160 if *rateLimit.RejectCode < 400 || *rateLimit.RejectCode > 599 { 161 allErrs = append(allErrs, field.Invalid(fieldPath.Child("rejectCode"), rateLimit.RejectCode, 162 "must be within the range [400-599]")) 163 } 164 } 165 166 return allErrs 167 } 168 169 func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList { 170 allErrs := field.ErrorList{} 171 172 allErrs = append(allErrs, validateJWTRealm(jwt.Realm, fieldPath.Child("realm"))...) 173 174 if jwt.Secret == "" { 175 return append(allErrs, field.Required(fieldPath.Child("secret"), "")) 176 } 177 allErrs = append(allErrs, validateSecretName(jwt.Secret, fieldPath.Child("secret"))...) 178 179 allErrs = append(allErrs, validateJWTToken(jwt.Token, fieldPath.Child("token"))...) 180 181 return allErrs 182 } 183 184 func validateIngressMTLS(ingressMTLS *v1.IngressMTLS, fieldPath *field.Path) field.ErrorList { 185 allErrs := field.ErrorList{} 186 187 if ingressMTLS.ClientCertSecret == "" { 188 return append(allErrs, field.Required(fieldPath.Child("clientCertSecret"), "")) 189 } 190 allErrs = append(allErrs, validateSecretName(ingressMTLS.ClientCertSecret, fieldPath.Child("clientCertSecret"))...) 191 192 allErrs = append(allErrs, validateIngressMTLSVerifyClient(ingressMTLS.VerifyClient, fieldPath.Child("verifyClient"))...) 193 194 if ingressMTLS.VerifyDepth != nil { 195 allErrs = append(allErrs, validatePositiveIntOrZero(*ingressMTLS.VerifyDepth, fieldPath.Child("verifyDepth"))...) 196 } 197 return allErrs 198 } 199 200 func validateEgressMTLS(egressMTLS *v1.EgressMTLS, fieldPath *field.Path) field.ErrorList { 201 allErrs := field.ErrorList{} 202 203 allErrs = append(allErrs, validateSecretName(egressMTLS.TLSSecret, fieldPath.Child("tlsSecret"))...) 204 205 if egressMTLS.VerifyServer && egressMTLS.TrustedCertSecret == "" { 206 return append(allErrs, field.Required(fieldPath.Child("trustedCertSecret"), "must be set when verifyServer is 'true'")) 207 } 208 allErrs = append(allErrs, validateSecretName(egressMTLS.TrustedCertSecret, fieldPath.Child("trustedCertSecret"))...) 209 210 if egressMTLS.VerifyDepth != nil { 211 allErrs = append(allErrs, validatePositiveIntOrZero(*egressMTLS.VerifyDepth, fieldPath.Child("verifyDepth"))...) 212 } 213 214 allErrs = append(allErrs, validateSSLName(egressMTLS.SSLName, fieldPath.Child("sslName"))...) 215 216 return allErrs 217 } 218 219 func validateOIDC(oidc *v1.OIDC, fieldPath *field.Path) field.ErrorList { 220 allErrs := field.ErrorList{} 221 222 if oidc.AuthEndpoint == "" { 223 return append(allErrs, field.Required(fieldPath.Child("authEndpoint"), "")) 224 } 225 if oidc.TokenEndpoint == "" { 226 return append(allErrs, field.Required(fieldPath.Child("tokenEndpoint"), "")) 227 } 228 if oidc.JWKSURI == "" { 229 return append(allErrs, field.Required(fieldPath.Child("jwksURI"), "")) 230 } 231 if oidc.ClientID == "" { 232 return append(allErrs, field.Required(fieldPath.Child("clientID"), "")) 233 } 234 if oidc.ClientSecret == "" { 235 return append(allErrs, field.Required(fieldPath.Child("clientSecret"), "")) 236 } 237 238 if oidc.Scope != "" { 239 allErrs = append(allErrs, validateOIDCScope(oidc.Scope, fieldPath.Child("scope"))...) 240 } 241 242 if oidc.RedirectURI != "" { 243 allErrs = append(allErrs, validatePath(oidc.RedirectURI, fieldPath.Child("redirectURI"))...) 244 } 245 246 allErrs = append(allErrs, validateURL(oidc.AuthEndpoint, fieldPath.Child("authEndpoint"))...) 247 allErrs = append(allErrs, validateURL(oidc.TokenEndpoint, fieldPath.Child("tokenEndpoint"))...) 248 allErrs = append(allErrs, validateURL(oidc.JWKSURI, fieldPath.Child("jwksURI"))...) 249 allErrs = append(allErrs, validateSecretName(oidc.ClientSecret, fieldPath.Child("clientSecret"))...) 250 allErrs = append(allErrs, validateClientID(oidc.ClientID, fieldPath.Child("clientID"))...) 251 252 return allErrs 253 } 254 255 func validateWAF(waf *v1.WAF, fieldPath *field.Path) field.ErrorList { 256 allErrs := field.ErrorList{} 257 258 if waf.ApPolicy != "" { 259 for _, msg := range validation.IsQualifiedName(waf.ApPolicy) { 260 allErrs = append(allErrs, field.Invalid(fieldPath.Child("apPolicy"), waf.ApPolicy, msg)) 261 } 262 } 263 264 if waf.SecurityLog != nil { 265 allErrs = append(allErrs, validateLogConf(waf.SecurityLog.ApLogConf, waf.SecurityLog.LogDest, fieldPath.Child("securityLog"))...) 266 } 267 268 return allErrs 269 } 270 271 func validateLogConf(logConf, logDest string, fieldPath *field.Path) field.ErrorList { 272 allErrs := field.ErrorList{} 273 274 if logConf != "" { 275 for _, msg := range validation.IsQualifiedName(logConf) { 276 allErrs = append(allErrs, field.Invalid(fieldPath.Child("apLogConf"), logConf, msg)) 277 } 278 } 279 280 err := appprotect.ValidateAppProtectLogDestination(logDest) 281 if err != nil { 282 allErrs = append(allErrs, field.Invalid(fieldPath.Child("logDest"), logDest, err.Error())) 283 } 284 return allErrs 285 } 286 287 func validateClientID(client string, fieldPath *field.Path) field.ErrorList { 288 allErrs := field.ErrorList{} 289 290 // isValidHeaderValue checks for $ and " in the string 291 if isValidHeaderValue(client) != nil { 292 allErrs = append(allErrs, field.Invalid( 293 fieldPath, 294 client, 295 `invalid string. String must contain valid ASCII characters, must have all '"' escaped and must not contain any '$' or end with an unescaped '\' 296 `)) 297 } 298 299 return allErrs 300 } 301 302 var validScopes = map[string]bool{ 303 "openid": true, 304 "profile": true, 305 "email": true, 306 "address": true, 307 "phone": true, 308 } 309 310 // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims 311 func validateOIDCScope(scope string, fieldPath *field.Path) field.ErrorList { 312 allErrs := field.ErrorList{} 313 314 if !strings.Contains(scope, "openid") { 315 return append(allErrs, field.Required(fieldPath, "openid scope")) 316 } 317 318 s := strings.Split(scope, "+") 319 for _, v := range s { 320 if !validScopes[v] { 321 msg := fmt.Sprintf("invalid Scope. Accepted scopes are: %v", mapToPrettyString(validScopes)) 322 allErrs = append(allErrs, field.Invalid(fieldPath, v, msg)) 323 } 324 325 } 326 327 return allErrs 328 } 329 330 func validateURL(name string, fieldPath *field.Path) field.ErrorList { 331 allErrs := field.ErrorList{} 332 333 u, err := url.Parse(name) 334 if err != nil { 335 return append(allErrs, field.Invalid(fieldPath, name, err.Error())) 336 } 337 var msg string 338 if u.Scheme == "" { 339 msg = "scheme required, please use the prefix http(s)://" 340 return append(allErrs, field.Invalid(fieldPath, name, msg)) 341 } 342 if u.Host == "" { 343 msg = "hostname required" 344 return append(allErrs, field.Invalid(fieldPath, name, msg)) 345 } 346 if u.Path == "" { 347 msg = "path required" 348 return append(allErrs, field.Invalid(fieldPath, name, msg)) 349 } 350 351 host, port, err := net.SplitHostPort(u.Host) 352 if err != nil { 353 host = u.Host 354 } 355 356 allErrs = append(allErrs, validateSSLName(host, fieldPath)...) 357 if port != "" { 358 allErrs = append(allErrs, validatePortNumber(port, fieldPath)...) 359 } 360 361 return allErrs 362 } 363 364 func validatePortNumber(port string, fieldPath *field.Path) field.ErrorList { 365 allErrs := field.ErrorList{} 366 portInt, _ := strconv.Atoi(port) 367 msg := validation.IsValidPortNum(portInt) 368 if msg != nil { 369 allErrs = append(allErrs, field.Invalid(fieldPath, port, msg[0])) 370 } 371 return allErrs 372 } 373 374 func validateSSLName(name string, fieldPath *field.Path) field.ErrorList { 375 allErrs := field.ErrorList{} 376 377 if name != "" { 378 for _, msg := range validation.IsDNS1123Subdomain(name) { 379 allErrs = append(allErrs, field.Invalid(fieldPath, name, msg)) 380 } 381 } 382 return allErrs 383 } 384 385 var validateVerifyClientKeyParameters = map[string]bool{ 386 "on": true, 387 "off": true, 388 "optional": true, 389 "optional_no_ca": true, 390 } 391 392 func validateIngressMTLSVerifyClient(verifyClient string, fieldPath *field.Path) field.ErrorList { 393 allErrs := field.ErrorList{} 394 if verifyClient != "" { 395 allErrs = append(allErrs, validateParameter(verifyClient, validateVerifyClientKeyParameters, fieldPath)...) 396 } 397 return allErrs 398 } 399 400 const ( 401 rateFmt = `[1-9]\d*r/[sSmM]` 402 rateErrMsg = "must consist of numeric characters followed by a valid rate suffix. 'r/s|r/m" 403 ) 404 405 var rateRegexp = regexp.MustCompile("^" + rateFmt + "$") 406 407 func validateRate(rate string, fieldPath *field.Path) field.ErrorList { 408 allErrs := field.ErrorList{} 409 410 if rate == "" { 411 return append(allErrs, field.Required(fieldPath, "")) 412 } 413 414 if !rateRegexp.MatchString(rate) { 415 msg := validation.RegexError(rateErrMsg, rateFmt, "16r/s", "32r/m", "64r/s") 416 return append(allErrs, field.Invalid(fieldPath, rate, msg)) 417 } 418 return allErrs 419 } 420 421 func validateRateLimitZoneSize(zoneSize string, fieldPath *field.Path) field.ErrorList { 422 allErrs := field.ErrorList{} 423 424 if zoneSize == "" { 425 return append(allErrs, field.Required(fieldPath, "")) 426 } 427 428 allErrs = append(allErrs, validateSize(zoneSize, fieldPath)...) 429 430 kbZoneSize := strings.TrimSuffix(strings.ToLower(zoneSize), "k") 431 kbZoneSizeNum, err := strconv.Atoi(kbZoneSize) 432 433 mbZoneSize := strings.TrimSuffix(strings.ToLower(zoneSize), "m") 434 mbZoneSizeNum, mbErr := strconv.Atoi(mbZoneSize) 435 436 if err == nil && kbZoneSizeNum < 32 || mbErr == nil && mbZoneSizeNum == 0 { 437 allErrs = append(allErrs, field.Invalid(fieldPath, zoneSize, "must be greater than 31k")) 438 } 439 440 return allErrs 441 } 442 443 var rateLimitKeySpecialVariables = []string{"arg_", "http_", "cookie_"} 444 445 // rateLimitKeyVariables includes NGINX variables allowed to be used in a rateLimit policy key. 446 var rateLimitKeyVariables = map[string]bool{ 447 "binary_remote_addr": true, 448 "request_uri": true, 449 "uri": true, 450 "args": true, 451 } 452 453 func validateRateLimitKey(key string, fieldPath *field.Path, isPlus bool) field.ErrorList { 454 allErrs := field.ErrorList{} 455 456 if key == "" { 457 return append(allErrs, field.Required(fieldPath, "")) 458 } 459 460 if !escapedStringsFmtRegexp.MatchString(key) { 461 msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, `Hello World! \n`, `\"${request_uri}\" is unavailable. \n`) 462 allErrs = append(allErrs, field.Invalid(fieldPath, key, msg)) 463 } 464 465 allErrs = append(allErrs, validateStringWithVariables(key, fieldPath, rateLimitKeySpecialVariables, rateLimitKeyVariables, isPlus)...) 466 467 return allErrs 468 } 469 470 var jwtTokenSpecialVariables = []string{"arg_", "http_", "cookie_"} 471 472 func validateJWTToken(token string, fieldPath *field.Path) field.ErrorList { 473 allErrs := field.ErrorList{} 474 475 if token == "" { 476 return allErrs 477 } 478 479 nginxVars := strings.Split(token, "$") 480 if len(nginxVars) != 2 { 481 return append(allErrs, field.Invalid(fieldPath, token, "must have 1 var")) 482 } 483 nVar := token[1:] 484 485 special := false 486 for _, specialVar := range jwtTokenSpecialVariables { 487 if strings.HasPrefix(nVar, specialVar) { 488 special = true 489 break 490 } 491 } 492 493 if special { 494 // validateJWTToken is called only when NGINX Plus is running 495 isPlus := true 496 allErrs = append(allErrs, validateSpecialVariable(nVar, fieldPath, isPlus)...) 497 } else { 498 return append(allErrs, field.Invalid(fieldPath, token, "must only have special vars")) 499 } 500 501 return allErrs 502 } 503 504 var validLogLevels = map[string]bool{ 505 "info": true, 506 "notice": true, 507 "warn": true, 508 "error": true, 509 } 510 511 func validateRateLimitLogLevel(logLevel string, fieldPath *field.Path) field.ErrorList { 512 allErrs := field.ErrorList{} 513 514 if !validLogLevels[logLevel] { 515 allErrs = append(allErrs, field.Invalid(fieldPath, logLevel, fmt.Sprintf("Accepted values: %s", 516 mapToPrettyString(validLogLevels)))) 517 } 518 519 return allErrs 520 } 521 522 const ( 523 jwtRealmFmt = `([^"$\\]|\\[^$])*` 524 jwtRealmFmtErrMsg string = `a valid realm must have all '"' escaped and must not contain any '$' or end with an unescaped '\'` 525 ) 526 527 var jwtRealmFmtRegexp = regexp.MustCompile("^" + jwtRealmFmt + "$") 528 529 func validateJWTRealm(realm string, fieldPath *field.Path) field.ErrorList { 530 allErrs := field.ErrorList{} 531 532 if realm == "" { 533 return append(allErrs, field.Required(fieldPath, "")) 534 } 535 536 if !jwtRealmFmtRegexp.MatchString(realm) { 537 msg := validation.RegexError(jwtRealmFmtErrMsg, jwtRealmFmt, "MyAPI", "My Product API") 538 allErrs = append(allErrs, field.Invalid(fieldPath, realm, msg)) 539 } 540 541 return allErrs 542 } 543 544 func validateIPorCIDR(ipOrCIDR string, fieldPath *field.Path) field.ErrorList { 545 allErrs := field.ErrorList{} 546 547 _, _, err := net.ParseCIDR(ipOrCIDR) 548 if err == nil { 549 // valid CIDR 550 return allErrs 551 } 552 553 ip := net.ParseIP(ipOrCIDR) 554 if ip != nil { 555 // valid IP 556 return allErrs 557 } 558 559 return append(allErrs, field.Invalid(fieldPath, ipOrCIDR, "must be a CIDR or IP")) 560 } 561 562 func validatePositiveInt(n int, fieldPath *field.Path) field.ErrorList { 563 allErrs := field.ErrorList{} 564 565 if n <= 0 { 566 return append(allErrs, field.Invalid(fieldPath, n, "must be positive")) 567 } 568 569 return allErrs 570 }