From f8eb6b187911a2b479bbd92d4bc8c8b1d44c7249 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Sun, 26 Feb 2023 21:51:38 -0300 Subject: [PATCH 01/14] Add validation to all annotations --- internal/ingress/annotations/alias/main.go | 31 ++- .../ingress/annotations/alias/main_test.go | 23 +- internal/ingress/annotations/annotations.go | 14 +- .../ingress/annotations/annotations_test.go | 32 ++- internal/ingress/annotations/auth/main.go | 80 ++++++- .../ingress/annotations/auth/main_test.go | 85 ++++++- internal/ingress/annotations/authreq/main.go | 215 ++++++++++++++--- .../ingress/annotations/authreq/main_test.go | 6 +- .../ingress/annotations/authreqglobal/main.go | 30 ++- internal/ingress/annotations/authtls/main.go | 98 +++++++- .../ingress/annotations/authtls/main_test.go | 66 ++++-- .../annotations/backendprotocol/main.go | 54 +++-- .../annotations/backendprotocol/main_test.go | 4 +- internal/ingress/annotations/canary/main.go | 116 ++++++++- .../annotations/clientbodybuffersize/main.go | 33 ++- .../clientbodybuffersize/main_test.go | 3 + .../ingress/annotations/connection/main.go | 36 ++- .../annotations/connection/main_test.go | 20 +- internal/ingress/annotations/cors/main.go | 115 ++++++++- .../ingress/annotations/cors/main_test.go | 42 ++-- .../annotations/customhttperrors/main.go | 38 ++- .../annotations/defaultbackend/main.go | 31 ++- .../annotations/defaultbackend/main_test.go | 58 +++-- .../annotations/globalratelimit/main.go | 76 +++++- .../annotations/globalratelimit/main_test.go | 22 +- .../annotations/http2pushpreload/main.go | 30 ++- .../annotations/http2pushpreload/main_test.go | 20 +- .../{ipwhitelist => ipallowlist}/main.go | 59 ++++- .../{ipwhitelist => ipallowlist}/main_test.go | 83 +++++-- .../ingress/annotations/ipdenylist/main.go | 42 +++- .../annotations/ipdenylist/main_test.go | 30 +-- .../ingress/annotations/loadbalancing/main.go | 37 ++- .../annotations/loadbalancing/main_test.go | 3 +- internal/ingress/annotations/log/main.go | 39 ++- internal/ingress/annotations/log/main_test.go | 22 +- internal/ingress/annotations/mirror/main.go | 69 +++++- .../ingress/annotations/mirror/main_test.go | 11 +- .../ingress/annotations/modsecurity/main.go | 68 +++++- .../ingress/annotations/opentelemetry/main.go | 62 ++++- .../annotations/opentelemetry/main_test.go | 30 ++- .../ingress/annotations/opentracing/main.go | 40 +++- .../annotations/opentracing/main_test.go | 8 +- internal/ingress/annotations/parser/main.go | 88 +++++-- .../ingress/annotations/parser/main_test.go | 21 +- .../ingress/annotations/parser/validators.go | 224 ++++++++++++++++++ .../annotations/parser/validators_test.go | 223 +++++++++++++++++ .../annotations/portinredirect/main.go | 30 ++- .../annotations/portinredirect/main_test.go | 28 +-- internal/ingress/annotations/proxy/main.go | 183 ++++++++++++-- .../ingress/annotations/proxy/main_test.go | 68 ++++++ internal/ingress/annotations/proxyssl/main.go | 108 ++++++++- .../ingress/annotations/proxyssl/main_test.go | 2 +- .../ingress/annotations/ratelimit/main.go | 115 +++++++-- .../annotations/ratelimit/main_test.go | 93 ++++++-- .../ingress/annotations/redirect/redirect.go | 62 ++++- .../annotations/redirect/redirect_test.go | 10 +- internal/ingress/annotations/rewrite/main.go | 98 +++++++- .../ingress/annotations/rewrite/main_test.go | 24 ++ internal/ingress/annotations/satisfy/main.go | 32 ++- .../ingress/annotations/satisfy/main_test.go | 2 +- .../ingress/annotations/serversnippet/main.go | 30 ++- .../annotations/serversnippet/main_test.go | 2 +- .../annotations/serviceupstream/main.go | 30 ++- .../annotations/serviceupstream/main_test.go | 6 +- .../annotations/sessionaffinity/main.go | 120 ++++++++-- internal/ingress/annotations/snippet/main.go | 30 ++- .../ingress/annotations/snippet/main_test.go | 2 +- .../ingress/annotations/sslcipher/main.go | 51 +++- .../annotations/sslcipher/main_test.go | 27 ++- .../annotations/sslpassthrough/main.go | 29 ++- .../annotations/sslpassthrough/main_test.go | 2 +- .../ingress/annotations/streamsnippet/main.go | 30 ++- .../annotations/streamsnippet/main_test.go | 2 +- .../annotations/upstreamhashby/main.go | 66 +++++- .../annotations/upstreamhashby/main_test.go | 32 ++- .../ingress/annotations/upstreamvhost/main.go | 31 ++- .../annotations/upstreamvhost/main_test.go | 2 +- .../annotations/xforwardedprefix/main.go | 30 ++- .../annotations/xforwardedprefix/main_test.go | 2 +- internal/ingress/controller/controller.go | 15 +- .../ingress/controller/controller_test.go | 6 +- internal/ingress/controller/store/store.go | 16 +- .../ingress/controller/store/store_test.go | 17 +- internal/ingress/errors/errors.go | 22 ++ pkg/apis/ingress/types.go | 8 +- pkg/apis/ingress/types_equals.go | 2 +- rootfs/etc/nginx/template/nginx.tmpl | 10 +- test/e2e-image/e2e.sh | 2 +- .../{ipwhitelist.go => ipallowlist.go} | 10 +- 89 files changed, 3456 insertions(+), 568 deletions(-) rename internal/ingress/annotations/{ipwhitelist => ipallowlist}/main.go (56%) rename internal/ingress/annotations/{ipwhitelist => ipallowlist}/main_test.go (64%) create mode 100644 internal/ingress/annotations/parser/validators.go create mode 100644 internal/ingress/annotations/parser/validators_test.go rename test/e2e/annotations/{ipwhitelist.go => ipallowlist.go} (81%) diff --git a/internal/ingress/annotations/alias/main.go b/internal/ingress/annotations/alias/main.go index bd2067c9f6..902478af0d 100644 --- a/internal/ingress/annotations/alias/main.go +++ b/internal/ingress/annotations/alias/main.go @@ -27,19 +27,44 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + serverAliasAnnotation = "server-alias" +) + +var aliasAnnotation = parser.Annotation{ + Group: "alias", + Annotations: parser.AnnotationFields{ + serverAliasAnnotation: { + Validator: parser.ValidateArrayOfServerName, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set + Documentation: `this annotation can be used to define additional server + aliases for this Ingress`, + }, + }, +} + type alias struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new Alias annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return alias{r} + return alias{ + r: r, + annotationConfig: aliasAnnotation, + } +} + +func (a alias) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } // Parse parses the annotations contained in the ingress rule // used to add an alias to the provided hosts func (a alias) Parse(ing *networking.Ingress) (interface{}, error) { - val, err := parser.GetStringAnnotation("server-alias", ing) + val, err := parser.GetStringAnnotation(serverAliasAnnotation, ing, a.annotationConfig.Annotations) if err != nil { return []string{}, err } diff --git a/internal/ingress/annotations/alias/main_test.go b/internal/ingress/annotations/alias/main_test.go index 8e6fca4470..6860c6cb69 100644 --- a/internal/ingress/annotations/alias/main_test.go +++ b/internal/ingress/annotations/alias/main_test.go @@ -27,7 +27,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) -var annotation = parser.GetAnnotationWithPrefix("server-alias") +var annotation = parser.GetAnnotationWithPrefix(serverAliasAnnotation) func TestParse(t *testing.T) { ap := NewParser(&resolver.Mock{}) @@ -38,14 +38,16 @@ func TestParse(t *testing.T) { testCases := []struct { annotations map[string]string expected []string + wantErr bool }{ - {map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}}, - {map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}}, - {map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}}, - {map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}}, - {map[string]string{annotation: ""}, []string{}}, - {map[string]string{}, []string{}}, - {nil, []string{}}, + {map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}, false}, + {map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}, false}, + {map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}, false}, + {map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}, false}, + {map[string]string{annotation: `www.xpto;lala`}, []string{}, true}, + {map[string]string{annotation: ""}, []string{}, true}, + {map[string]string{}, []string{}, true}, + {nil, []string{}, true}, } ing := &networking.Ingress{ @@ -58,7 +60,10 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) - result, _ := ap.Parse(ing) + result, err := ap.Parse(ing) + if (err != nil) != testCase.wantErr { + t.Errorf("ParseAliasAnnotation() annotation: %s, error = %v, wantErr %v", testCase.annotations, err, testCase.wantErr) + } if !reflect.DeepEqual(result, testCase.expected) { t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) } diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go index 5bb2bf5e60..5a9cbd4f94 100644 --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -44,8 +44,8 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi" "k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit" "k8s.io/ingress-nginx/internal/ingress/annotations/http2pushpreload" + "k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist" "k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist" - "k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist" "k8s.io/ingress-nginx/internal/ingress/annotations/loadbalancing" "k8s.io/ingress-nginx/internal/ingress/annotations/log" "k8s.io/ingress-nginx/internal/ingress/annotations/mirror" @@ -109,7 +109,6 @@ type Ingress struct { UpstreamHashBy upstreamhashby.Config LoadBalancing string UpstreamVhost string - Whitelist ipwhitelist.SourceRange Denylist ipdenylist.SourceRange XForwardedPrefix string SSLCipher sslcipher.Config @@ -117,6 +116,7 @@ type Ingress struct { ModSecurity modsecurity.Config Mirror mirror.Config StreamSnippet string + Allowlist ipallowlist.SourceRange } // Extractor defines the annotation parsers to be used in the extraction of annotations @@ -159,7 +159,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor { "UpstreamHashBy": upstreamhashby.NewParser(cfg), "LoadBalancing": loadbalancing.NewParser(cfg), "UpstreamVhost": upstreamvhost.NewParser(cfg), - "Whitelist": ipwhitelist.NewParser(cfg), + "Allowlist": ipallowlist.NewParser(cfg), "Denylist": ipdenylist.NewParser(cfg), "XForwardedPrefix": xforwardedprefix.NewParser(cfg), "SSLCipher": sslcipher.NewParser(cfg), @@ -173,7 +173,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor { } // Extract extracts the annotations from an Ingress -func (e Extractor) Extract(ing *networking.Ingress) *Ingress { +func (e Extractor) Extract(ing *networking.Ingress) (*Ingress, error) { pia := &Ingress{ ObjectMeta: ing.ObjectMeta, } @@ -183,6 +183,10 @@ func (e Extractor) Extract(ing *networking.Ingress) *Ingress { val, err := annotationParser.Parse(ing) klog.V(5).InfoS("Parsing Ingress annotation", "name", name, "ingress", klog.KObj(ing), "value", val) if err != nil { + if errors.IsValidationError(err) { + klog.ErrorS(err, "ingress contains invalid annotation value") + return nil, err + } if errors.IsMissingAnnotations(err) { continue } @@ -220,5 +224,5 @@ func (e Extractor) Extract(ing *networking.Ingress) *Ingress { klog.ErrorS(err, "unexpected error merging extracted annotations") } - return pia + return pia, nil } diff --git a/internal/ingress/annotations/annotations_test.go b/internal/ingress/annotations/annotations_test.go index d792801bcb..2b2a64268e 100644 --- a/internal/ingress/annotations/annotations_test.go +++ b/internal/ingress/annotations/annotations_test.go @@ -134,8 +134,11 @@ func TestSSLPassthrough(t *testing.T) { for _, foo := range fooAnns { ing.SetAnnotations(foo.annotations) - r := ec.Extract(ing).SSLPassthrough - if r != foo.er { + r, err := ec.Extract(ing) + if err != nil { + t.Errorf("Errors should be null: %v", err) + } + if r.SSLPassthrough != foo.er { t.Errorf("Returned %v but expected %v", r, foo.er) } } @@ -158,8 +161,11 @@ func TestUpstreamHashBy(t *testing.T) { for _, foo := range fooAnns { ing.SetAnnotations(foo.annotations) - r := ec.Extract(ing).UpstreamHashBy.UpstreamHashBy - if r != foo.er { + r, err := ec.Extract(ing) + if err != nil { + t.Errorf("error should be null: %v", err) + } + if r.UpstreamHashBy.UpstreamHashBy != foo.er { t.Errorf("Returned %v but expected %v", r, foo.er) } } @@ -185,7 +191,11 @@ func TestAffinitySession(t *testing.T) { for _, foo := range fooAnns { ing.SetAnnotations(foo.annotations) - r := ec.Extract(ing).SessionAffinity + rann, err := ec.Extract(ing) + if err != nil { + t.Errorf("error should be null: %v", err) + } + r := rann.SessionAffinity t.Logf("Testing pass %v %v", foo.affinitytype, foo.cookiename) if r.Type != foo.affinitytype { @@ -228,7 +238,11 @@ func TestCors(t *testing.T) { for _, foo := range fooAnns { ing.SetAnnotations(foo.annotations) - r := ec.Extract(ing).CorsConfig + rann, err := ec.Extract(ing) + if err != nil { + t.Errorf("error should be null: %v", err) + } + r := rann.CorsConfig t.Logf("Testing pass %v %v %v %v %v", foo.corsenabled, foo.methods, foo.headers, foo.origin, foo.credentials) if r.CorsEnabled != foo.corsenabled { @@ -277,7 +291,11 @@ func TestCustomHTTPErrors(t *testing.T) { for _, foo := range fooAnns { ing.SetAnnotations(foo.annotations) - r := ec.Extract(ing).CustomHTTPErrors + rann, err := ec.Extract(ing) + if err != nil { + t.Errorf("error should be null: %v", err) + } + r := rann.CustomHTTPErrors // Check that expected codes were created for i := range foo.er { diff --git a/internal/ingress/annotations/auth/main.go b/internal/ingress/annotations/auth/main.go index 62d70873e9..ccdcfd7300 100644 --- a/internal/ingress/annotations/auth/main.go +++ b/internal/ingress/annotations/auth/main.go @@ -32,13 +32,56 @@ import ( "k8s.io/ingress-nginx/pkg/util/file" ) +const ( + authSecretTypeAnnotation = "auth-secret-type" //#nosec G101 + authRealmAnnotation = "auth-realm" + authTypeAnnotation = "auth-type" + // This should be exported as it is imported by other packages + AuthSecretAnnotation = "auth-secret" //#nosec G101 +) + var ( - authTypeRegex = regexp.MustCompile(`basic|digest`) + authTypeRegex = regexp.MustCompile(`basic|digest`) + authSecretTypeRegex = regexp.MustCompile(`auth-file|auth-map`) + // AuthDirectory default directory used to store files // to authenticate request AuthDirectory = "/etc/ingress-controller/auth" ) +var AuthSecretConfig = parser.AnnotationConfig{ + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars + Documentation: `This annotation defines the name of the Secret that contains the usernames and passwords which are granted access to the paths defined in the Ingress rules. `, +} + +var authSecretAnnotations = parser.Annotation{ + Group: "authentication", + Annotations: parser.AnnotationFields{ + AuthSecretAnnotation: AuthSecretConfig, + authSecretTypeAnnotation: { + Validator: parser.ValidateRegex(*authSecretTypeRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation what is the format of auth-secret value. Can be "auth-file" that defines the content of an htpasswd file, or "auth-map" where each key + is a user and each value is the password.`, + }, + authRealmAnnotation: { + Validator: parser.ValidateRegex(*parser.CharsWithSpace, false), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars + Documentation: `This annotation defines the realm (message) that should be shown to user when authentication is requested.`, + }, + authTypeAnnotation: { + Validator: parser.ValidateRegex(*authTypeRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines the basic authentication type. Should be "basic" or "digest"`, + }, + }, +} + const ( fileAuth = "auth-file" mapAuth = "auth-map" @@ -85,13 +128,18 @@ func (bd1 *Config) Equal(bd2 *Config) bool { } type auth struct { - r resolver.Resolver - authDirectory string + r resolver.Resolver + authDirectory string + annotationConfig parser.Annotation } // NewParser creates a new authentication annotation parser func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotation { - return auth{r, authDirectory} + return auth{ + r: r, + authDirectory: authDirectory, + annotationConfig: authSecretAnnotations, + } } // Parse parses the annotations contained in the ingress @@ -99,7 +147,7 @@ func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotati // and generated an htpasswd compatible file to be used as source // during the authentication process func (a auth) Parse(ing *networking.Ingress) (interface{}, error) { - at, err := parser.GetStringAnnotation("auth-type", ing) + at, err := parser.GetStringAnnotation(authTypeAnnotation, ing, a.annotationConfig.Annotations) if err != nil { return nil, err } @@ -109,12 +157,15 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) { } var secretType string - secretType, err = parser.GetStringAnnotation("auth-secret-type", ing) + secretType, err = parser.GetStringAnnotation(authSecretTypeAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if ing_errors.IsValidationError(err) { + return nil, err + } secretType = fileAuth } - s, err := parser.GetStringAnnotation("auth-secret", ing) + s, err := parser.GetStringAnnotation(AuthSecretAnnotation, ing, a.annotationConfig.Annotations) if err != nil { return nil, ing_errors.LocationDenied{ Reason: fmt.Errorf("error reading secret name from annotation: %w", err), @@ -131,6 +182,12 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) { if sns == "" { sns = ing.Namespace } + // We don't accept different namespaces for secrets. + if sns != ing.Namespace { + return nil, ing_errors.LocationDenied{ + Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"), + } + } name := fmt.Sprintf("%v/%v", sns, sname) secret, err := a.r.GetSecret(name) @@ -140,7 +197,10 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) { } } - realm, _ := parser.GetStringAnnotation("auth-realm", ing) + realm, err := parser.GetStringAnnotation(authRealmAnnotation, ing, a.annotationConfig.Annotations) + if ing_errors.IsValidationError(err) { + return nil, err + } passFilename := fmt.Sprintf("%v/%v-%v-%v.passwd", a.authDirectory, ing.GetNamespace(), ing.UID, secret.UID) @@ -210,3 +270,7 @@ func dumpSecretAuthMap(filename string, secret *api.Secret) error { return nil } + +func (a auth) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/auth/main_test.go b/internal/ingress/annotations/auth/main_test.go index d4ec53459f..412ddb7119 100644 --- a/internal/ingress/annotations/auth/main_test.go +++ b/internal/ingress/annotations/auth/main_test.go @@ -106,13 +106,72 @@ func TestIngressAuthBadAuthType(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("auth-type")] = "invalid" + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "invalid" ing.SetAnnotations(data) _, dir, _ := dummySecretContent(t) defer os.RemoveAll(dir) - expected := ing_errors.NewLocationDenied("invalid authentication type") + expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-type") + _, err := NewParser(dir, &mockSecret{}).Parse(ing) + if err.Error() != expected.Error() { + t.Errorf("expected '%v' but got '%v'", expected, err) + } +} + +func TestIngressInvalidRealm(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" + data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "something weird ; location trying to { break }" + data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret" + ing.SetAnnotations(data) + + _, dir, _ := dummySecretContent(t) + defer os.RemoveAll(dir) + + expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-realm") + _, err := NewParser(dir, &mockSecret{}).Parse(ing) + if err.Error() != expected.Error() { + t.Errorf("expected '%v' but got '%v'", expected, err) + } +} + +func TestIngressInvalidDifferentNamespace(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" + data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "otherns/demo-secret" + ing.SetAnnotations(data) + + _, dir, _ := dummySecretContent(t) + defer os.RemoveAll(dir) + + expected := ing_errors.LocationDenied{ + Reason: errors.New("cross namespace usage of secrets is not allowed"), + } + _, err := NewParser(dir, &mockSecret{}).Parse(ing) + if err.Error() != expected.Error() { + t.Errorf("expected '%v' but got '%v'", expected, err) + } +} + +func TestIngressInvalidSecretName(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" + data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret;xpto" + ing.SetAnnotations(data) + + _, dir, _ := dummySecretContent(t) + defer os.RemoveAll(dir) + + expected := ing_errors.LocationDenied{ + Reason: errors.New("error reading secret name from annotation: annotation nginx.ingress.kubernetes.io/auth-secret contains invalid value"), + } _, err := NewParser(dir, &mockSecret{}).Parse(ing) if err.Error() != expected.Error() { t.Errorf("expected '%v' but got '%v'", expected, err) @@ -123,7 +182,7 @@ func TestInvalidIngressAuthNoSecret(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("auth-type")] = "basic" + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" ing.SetAnnotations(data) _, dir, _ := dummySecretContent(t) @@ -142,9 +201,9 @@ func TestIngressAuth(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("auth-type")] = "basic" - data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret" - data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-" + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" + data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret" + data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-" ing.SetAnnotations(data) _, dir, _ := dummySecretContent(t) @@ -173,9 +232,9 @@ func TestIngressAuthWithoutSecret(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("auth-type")] = "basic" - data[parser.GetAnnotationWithPrefix("auth-secret")] = "invalid-secret" - data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-" + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" + data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "invalid-secret" + data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-" ing.SetAnnotations(data) _, dir, _ := dummySecretContent(t) @@ -191,10 +250,10 @@ func TestIngressAuthInvalidSecretKey(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("auth-type")] = "basic" - data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret" - data[parser.GetAnnotationWithPrefix("auth-secret-type")] = "invalid-type" - data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-" + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" + data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret" + data[parser.GetAnnotationWithPrefix(authSecretTypeAnnotation)] = "invalid-type" + data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-" ing.SetAnnotations(data) _, dir, _ := dummySecretContent(t) diff --git a/internal/ingress/annotations/authreq/main.go b/internal/ingress/annotations/authreq/main.go index 2ab389146f..c9e4a10086 100644 --- a/internal/ingress/annotations/authreq/main.go +++ b/internal/ingress/annotations/authreq/main.go @@ -24,13 +24,127 @@ import ( "k8s.io/klog/v2" networking "k8s.io/api/networking/v1" + "k8s.io/client-go/tools/cache" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" "k8s.io/ingress-nginx/pkg/util/sets" ) +const ( + authReqURLAnnotation = "auth-url" + authReqMethodAnnotation = "auth-method" + authReqSigninAnnotation = "auth-signin" + authReqSigninRedirParamAnnotation = "auth-signin-redirect-param" + authReqSnippetAnnotation = "auth-snippet" + authReqCacheKeyAnnotation = "auth-cache-key" + authReqKeepaliveAnnotation = "auth-keepalive" + authReqKeepaliveRequestsAnnotation = "auth-keepalive-requests" + authReqKeepaliveTimeout = "auth-keepalive-timeout" + authReqCacheDuration = "auth-cache-duration" + authReqResponseHeadersAnnotation = "auth-response-headers" + authReqProxySetHeadersAnnotation = "auth-proxy-set-headers" + authReqRequestRedirectAnnotation = "auth-request-redirect" + authReqAlwaysSetCookieAnnotation = "auth-always-set-cookie" + + // This should be exported as it is imported by other packages + AuthSecretAnnotation = "auth-secret" +) + +var authReqAnnotations = parser.Annotation{ + Group: "authentication", + Annotations: parser.AnnotationFields{ + authReqURLAnnotation: { + Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation allows to indicate the URL where the HTTP request should be sent`, + }, + authReqMethodAnnotation: { + Validator: parser.ValidateRegex(*methodsRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation allows to specify the HTTP method to use`, + }, + authReqSigninAnnotation: { + Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation allows to specify the location of the error page`, + }, + authReqSigninRedirParamAnnotation: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation allows to specify the URL parameter in the error page which should contain the original URL for a failed signin request`, + }, + authReqSnippetAnnotation: { + Validator: parser.ValidateNull, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskCritical, + Documentation: `This annotation allows to specify a custom snippet to use with external authentication`, + }, + authReqCacheKeyAnnotation: { + Validator: parser.ValidateRegex(*parser.NGINXVariable, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation enables caching for auth requests.`, + }, + authReqKeepaliveAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation specifies the maximum number of keepalive connections to auth-url. Only takes effect when no variables are used in the host part of the URL`, + }, + authReqKeepaliveRequestsAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines the maximum number of requests that can be served through one keepalive connection`, + }, + authReqKeepaliveTimeout: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation specifies a duration in seconds which an idle keepalive connection to an upstream server will stay open`, + }, + authReqCacheDuration: { + Validator: parser.ValidateRegex(*parser.ExtendedChars, false), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation allows to specify a caching time for auth responses based on their response codes, e.g. 200 202 30m`, + }, + authReqResponseHeadersAnnotation: { + Validator: parser.ValidateRegex(*parser.HeadersVariable, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation sets the headers to pass to backend once authentication request completes. They should be separated by comma.`, + }, + authReqProxySetHeadersAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation sets the name of a ConfigMap that specifies headers to pass to the authentication service. + Only ConfigMaps on the same namespace are allowed`, + }, + authReqRequestRedirectAnnotation: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation allows to specify the X-Auth-Request-Redirect header value`, + }, + authReqAlwaysSetCookieAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables setting a cookie returned by auth request. + By default, the cookie will be set only if an upstream reports with the code 200, 201, 204, 206, 301, 302, 303, 304, 307, or 308`, + }, + }, +} + // Config returns external authentication configuration for an Ingress rule type Config struct { URL string `json:"url"` @@ -121,7 +235,7 @@ func (e1 *Config) Equal(e2 *Config) bool { } var ( - methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"} + methodsRegex = regexp.MustCompile("(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)") headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`) statusCodeRegex = regexp.MustCompile(`^[\d]{3}$`) durationRegex = regexp.MustCompile(`^[\d]+(ms|s|m|h|d|w|M|y)$`) // see http://nginx.org/en/docs/syntax.html @@ -129,16 +243,7 @@ var ( // ValidMethod checks is the provided string a valid HTTP method func ValidMethod(method string) bool { - if len(method) == 0 { - return false - } - - for _, m := range methods { - if method == m { - return true - } - } - return false + return methodsRegex.MatchString(method) } // ValidHeader checks is the provided string satisfies the header's name regex @@ -173,19 +278,23 @@ func ValidCacheDuration(duration string) bool { } type authReq struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new authentication request annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return authReq{r} + return authReq{ + r: r, + annotationConfig: authReqAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress // rule used to use an Config URL as source for authentication func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { // Required Parameters - urlString, err := parser.GetStringAnnotation("auth-url", ing) + urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations) if err != nil { return nil, err } @@ -195,33 +304,44 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { return nil, ing_errors.LocationDenied{Reason: fmt.Errorf("could not parse auth-url annotation: %v", err)} } - authMethod, _ := parser.GetStringAnnotation("auth-method", ing) - if len(authMethod) != 0 && !ValidMethod(authMethod) { - return nil, ing_errors.NewLocationDenied("invalid HTTP method") + authMethod, err := parser.GetStringAnnotation(authReqMethodAnnotation, ing, a.annotationConfig.Annotations) + if err != nil { + if errors.IsValidationError(err) { + return nil, ing_errors.NewLocationDenied("invalid HTTP method") + } } // Optional Parameters - signIn, err := parser.GetStringAnnotation("auth-signin", ing) + signIn, err := parser.GetStringAnnotation(authReqSigninAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s value is invalid: %s", authReqSigninAnnotation, err) + } klog.V(3).InfoS("auth-signin annotation is undefined and will not be set") } - signInRedirectParam, err := parser.GetStringAnnotation("auth-signin-redirect-param", ing) + signInRedirectParam, err := parser.GetStringAnnotation(authReqSigninRedirParamAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s value is invalid: %s", authReqSigninRedirParamAnnotation, err) + } klog.V(3).Infof("auth-signin-redirect-param annotation is undefined and will not be set") } - authSnippet, err := parser.GetStringAnnotation("auth-snippet", ing) + authSnippet, err := parser.GetStringAnnotation(authReqSnippetAnnotation, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("auth-snippet annotation is undefined and will not be set") } - authCacheKey, err := parser.GetStringAnnotation("auth-cache-key", ing) + authCacheKey, err := parser.GetStringAnnotation(authReqCacheKeyAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s value is invalid: %s", authReqCacheKeyAnnotation, err) + } klog.V(3).InfoS("auth-cache-key annotation is undefined and will not be set") } - keepaliveConnections, err := parser.GetIntAnnotation("auth-keepalive", ing) + keepaliveConnections, err := parser.GetIntAnnotation(authReqKeepaliveAnnotation, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("auth-keepalive annotation is undefined and will be set to its default value") keepaliveConnections = defaultKeepaliveConnections @@ -238,9 +358,9 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { } } - keepaliveRequests, err := parser.GetIntAnnotation("auth-keepalive-requests", ing) + keepaliveRequests, err := parser.GetIntAnnotation(authReqKeepaliveRequestsAnnotation, ing, a.annotationConfig.Annotations) if err != nil { - klog.V(3).InfoS("auth-keepalive-requests annotation is undefined and will be set to its default value") + klog.V(3).InfoS("auth-keepalive-requests annotation is undefined or invalid and will be set to its default value") keepaliveRequests = defaultKeepaliveRequests } if keepaliveRequests <= 0 { @@ -248,7 +368,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { keepaliveConnections = 0 } - keepaliveTimeout, err := parser.GetIntAnnotation("auth-keepalive-timeout", ing) + keepaliveTimeout, err := parser.GetIntAnnotation(authReqKeepaliveTimeout, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("auth-keepalive-timeout annotation is undefined and will be set to its default value") keepaliveTimeout = defaultKeepaliveTimeout @@ -258,14 +378,20 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { keepaliveConnections = 0 } - durstr, _ := parser.GetStringAnnotation("auth-cache-duration", ing) + durstr, err := parser.GetStringAnnotation(authReqCacheDuration, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return nil, fmt.Errorf("%s contains invalid value", authReqCacheDuration) + } authCacheDuration, err := ParseStringToCacheDurations(durstr) if err != nil { return nil, err } responseHeaders := []string{} - hstr, _ := parser.GetStringAnnotation("auth-response-headers", ing) + hstr, err := parser.GetStringAnnotation(authReqResponseHeadersAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return nil, ing_errors.NewLocationDenied("validation error") + } if len(hstr) != 0 { harr := strings.Split(hstr, ",") for _, header := range harr { @@ -279,9 +405,26 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { } } - proxySetHeaderMap, err := parser.GetStringAnnotation("auth-proxy-set-headers", ing) + proxySetHeaderMap, err := parser.GetStringAnnotation(authReqProxySetHeadersAnnotation, ing, a.annotationConfig.Annotations) + if err != nil { + klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set", "err", err) + } + + cns, _, err := cache.SplitMetaNamespaceKey(proxySetHeaderMap) if err != nil { - klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set") + return nil, ing_errors.LocationDenied{ + Reason: fmt.Errorf("error reading configmap name %s from annotation: %w", proxySetHeaderMap, err), + } + } + + if cns == "" { + cns = ing.Namespace + } + // We don't accept different namespaces for secrets. + if cns != ing.Namespace { + return nil, ing_errors.LocationDenied{ + Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"), + } } var proxySetHeaders map[string]string @@ -301,9 +444,15 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { proxySetHeaders = proxySetHeadersMapContents.Data } - requestRedirect, _ := parser.GetStringAnnotation("auth-request-redirect", ing) + requestRedirect, err := parser.GetStringAnnotation(authReqRequestRedirectAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return nil, fmt.Errorf("%s is invalid: %w", authReqRequestRedirectAnnotation, err) + } - alwaysSetCookie, _ := parser.GetBoolAnnotation("auth-always-set-cookie", ing) + alwaysSetCookie, err := parser.GetBoolAnnotation(authReqAlwaysSetCookieAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return nil, fmt.Errorf("%s is invalid: %w", authReqAlwaysSetCookieAnnotation, err) + } return &Config{ URL: urlString, @@ -348,3 +497,7 @@ func ParseStringToCacheDurations(input string) ([]string, error) { } return authCacheDuration, nil } + +func (a authReq) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/authreq/main_test.go b/internal/ingress/annotations/authreq/main_test.go index e1325235b5..833bebe788 100644 --- a/internal/ingress/annotations/authreq/main_test.go +++ b/internal/ingress/annotations/authreq/main_test.go @@ -192,11 +192,13 @@ func TestHeaderAnnotations(t *testing.T) { i, err := NewParser(&resolver.Mock{}).Parse(ing) if test.expErr { if err == nil { - t.Error("expected error but retuned nil") + t.Errorf("%v expected error but retuned nil", test.title) } continue } - + if err != nil { + t.Errorf("no error was expected but %v happened in %s", err, test.title) + } u, ok := i.(*Config) if !ok { t.Errorf("%v: expected an External type", test.title) diff --git a/internal/ingress/annotations/authreqglobal/main.go b/internal/ingress/annotations/authreqglobal/main.go index 78dd7d6a57..429588e5a5 100644 --- a/internal/ingress/annotations/authreqglobal/main.go +++ b/internal/ingress/annotations/authreqglobal/main.go @@ -23,23 +23,47 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + enableGlobalAuthAnnotation = "enable-global-auth" +) + +var globalAuthAnnotations = parser.Annotation{ + Group: "authentication", + Annotations: parser.AnnotationFields{ + enableGlobalAuthAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `Defines if the gloabl external authentication should be enabled.`, + }, + }, +} + type authReqGlobal struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new authentication request annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return authReqGlobal{r} + return authReqGlobal{ + r: r, + annotationConfig: globalAuthAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress // rule used to enable or disable global external authentication func (a authReqGlobal) Parse(ing *networking.Ingress) (interface{}, error) { - enableGlobalAuth, err := parser.GetBoolAnnotation("enable-global-auth", ing) + enableGlobalAuth, err := parser.GetBoolAnnotation(enableGlobalAuthAnnotation, ing, a.annotationConfig.Annotations) if err != nil { enableGlobalAuth = true } return enableGlobalAuth, nil } + +func (a authReqGlobal) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/authtls/main.go b/internal/ingress/annotations/authtls/main.go index 2efd6d176f..25c292e29c 100644 --- a/internal/ingress/annotations/authtls/main.go +++ b/internal/ingress/annotations/authtls/main.go @@ -32,13 +32,64 @@ import ( const ( defaultAuthTLSDepth = 1 defaultAuthVerifyClient = "on" + + annotationAuthTLSSecret = "auth-tls-secret" //#nosec G101 + annotationAuthTLSVerifyClient = "auth-tls-verify-client" + annotationAuthTLSVerifyDepth = "auth-tls-verify-depth" + annotationAuthTLSErrorPage = "auth-tls-error-page" + annotationAuthTLSPassCertToUpstream = "auth-tls-pass-certificate-to-upstream" //#nosec G101 + annotationAuthTLSMatchCN = "auth-tls-match-cn" ) var ( + regexChars = regexp.QuoteMeta(`()|=`) authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`) - commonNameRegex = regexp.MustCompile(`CN=`) + commonNameRegex = regexp.MustCompile(`^CN=[/\-.\_\~a-zA-Z0-9` + regexChars + `]*$`) + redirectRegex = regexp.MustCompile(`^((https?://)?[A-Za-z0-9\-\.]*(:[0-9]+)?/[A-Za-z0-9\-\.]*)?$`) ) +var authTLSAnnotations = parser.Annotation{ + Group: "authentication", + Annotations: parser.AnnotationFields{ + annotationAuthTLSSecret: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars + Documentation: `This annotation defines the secret that contains the certificate chain of allowed certs`, + }, + annotationAuthTLSVerifyClient: { + Validator: parser.ValidateRegex(*authVerifyClientRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars + Documentation: `This annotation enables verification of client certificates. Can be "on", "off", "optional" or "optional_no_ca"`, + }, + annotationAuthTLSVerifyDepth: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines validation depth between the provided client certificate and the Certification Authority chain.`, + }, + annotationAuthTLSErrorPage: { + Validator: parser.ValidateRegex(*redirectRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation defines the URL/Page that user should be redirected in case of a Certificate Authentication Error`, + }, + annotationAuthTLSPassCertToUpstream: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines if the received certificates should be passed or not to the upstream server in the header "ssl-client-cert"`, + }, + annotationAuthTLSMatchCN: { + Validator: parser.ValidateRegex(*commonNameRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation adds a sanity check for the CN of the client certificate that is sent over using a string / regex starting with "CN="`, + }, + }, +} + // Config contains the AuthSSLCert used for mutual authentication // and the configured ValidationDepth type Config struct { @@ -80,11 +131,15 @@ func (assl1 *Config) Equal(assl2 *Config) bool { // NewParser creates a new TLS authentication annotation parser func NewParser(resolver resolver.Resolver) parser.IngressAnnotation { - return authTLS{resolver} + return authTLS{ + r: resolver, + annotationConfig: authTLSAnnotations, + } } type authTLS struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Parse parses the annotations contained in the ingress @@ -93,15 +148,21 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) { var err error config := &Config{} - tlsauthsecret, err := parser.GetStringAnnotation("auth-tls-secret", ing) + tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing, a.annotationConfig.Annotations) if err != nil { return &Config{}, err } - _, _, err = k8s.ParseNameNS(tlsauthsecret) + ns, _, err := k8s.ParseNameNS(tlsauthsecret) if err != nil { return &Config{}, ing_errors.NewLocationDenied(err.Error()) } + if ns == "" { + ns = ing.Namespace + } + if ns != ing.Namespace { + return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported") + } authCert, err := a.r.GetAuthCertificate(tlsauthsecret) if err != nil { @@ -110,30 +171,45 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) { } config.AuthSSLCert = *authCert - config.VerifyClient, err = parser.GetStringAnnotation("auth-tls-verify-client", ing) + config.VerifyClient, err = parser.GetStringAnnotation(annotationAuthTLSVerifyClient, ing, a.annotationConfig.Annotations) + // We can set a default value here in case of validation error if err != nil || !authVerifyClientRegex.MatchString(config.VerifyClient) { config.VerifyClient = defaultAuthVerifyClient } - config.ValidationDepth, err = parser.GetIntAnnotation("auth-tls-verify-depth", ing) + config.ValidationDepth, err = parser.GetIntAnnotation(annotationAuthTLSVerifyDepth, ing, a.annotationConfig.Annotations) + // We can set a default value here in case of validation error if err != nil || config.ValidationDepth == 0 { config.ValidationDepth = defaultAuthTLSDepth } - config.ErrorPage, err = parser.GetStringAnnotation("auth-tls-error-page", ing) + config.ErrorPage, err = parser.GetStringAnnotation(annotationAuthTLSErrorPage, ing, a.annotationConfig.Annotations) if err != nil { + if ing_errors.IsValidationError(err) { + return &Config{}, err + } config.ErrorPage = "" } - config.PassCertToUpstream, err = parser.GetBoolAnnotation("auth-tls-pass-certificate-to-upstream", ing) + config.PassCertToUpstream, err = parser.GetBoolAnnotation(annotationAuthTLSPassCertToUpstream, ing, a.annotationConfig.Annotations) if err != nil { + if ing_errors.IsValidationError(err) { + return &Config{}, err + } config.PassCertToUpstream = false } - config.MatchCN, err = parser.GetStringAnnotation("auth-tls-match-cn", ing) - if err != nil || !commonNameRegex.MatchString(config.MatchCN) { + config.MatchCN, err = parser.GetStringAnnotation(annotationAuthTLSMatchCN, ing, a.annotationConfig.Annotations) + if err != nil { + if ing_errors.IsValidationError(err) { + return &Config{}, err + } config.MatchCN = "" } return config, nil } + +func (a authTLS) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/authtls/main_test.go b/internal/ingress/annotations/authtls/main_test.go index 569f3865bc..a1f3f0f92c 100644 --- a/internal/ingress/annotations/authtls/main_test.go +++ b/internal/ingress/annotations/authtls/main_test.go @@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret" ing.SetAnnotations(data) @@ -132,11 +132,11 @@ func TestAnnotations(t *testing.T) { t.Errorf("expected empty string, but got %v", u.MatchCN) } - data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off" - data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "2" - data[parser.GetAnnotationWithPrefix("auth-tls-error-page")] = "ok.com/error" - data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "true" - data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "CN=hello-app" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "off" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "2" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSErrorPage)] = "ok.com/error" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "true" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "CN=(hello-app|ok|goodbye)" ing.SetAnnotations(data) @@ -165,8 +165,8 @@ func TestAnnotations(t *testing.T) { if u.PassCertToUpstream != true { t.Errorf("expected %v but got %v", true, u.PassCertToUpstream) } - if u.MatchCN != "CN=hello-app" { - t.Errorf("expected %v but got %v", "CN=hello-app", u.MatchCN) + if u.MatchCN != "CN=(hello-app|ok|goodbye)" { + t.Errorf("expected %v but got %v", "CN=(hello-app|ok|goodbye)", u.MatchCN) } } @@ -182,15 +182,24 @@ func TestInvalidAnnotations(t *testing.T) { } // Invalid NameSpace - data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "demo-secret" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "demo-secret" ing.SetAnnotations(data) _, err = NewParser(fakeSecret).Parse(ing) if err == nil { t.Errorf("Expected error with ingress but got nil") } + // Invalid Cross NameSpace + data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "nondefault/demo-secret" + ing.SetAnnotations(data) + _, err = NewParser(fakeSecret).Parse(ing) + expErr := errors.NewLocationDenied("cross namespace secrets are not supported") + if err.Error() != expErr.Error() { + t.Errorf("received error is different from cross namespace error: %s Expected %s", err, expErr) + } + // Invalid Auth Certificate - data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/invalid-demo-secret" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/invalid-demo-secret" ing.SetAnnotations(data) _, err = NewParser(fakeSecret).Parse(ing) if err == nil { @@ -198,11 +207,38 @@ func TestInvalidAnnotations(t *testing.T) { } // Invalid optional Annotations - data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret" - data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "w00t" - data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "abcd" - data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "nahh" - data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "" + data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret" + + data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "w00t" + ing.SetAnnotations(data) + _, err = NewParser(fakeSecret).Parse(ing) + if err != nil { + t.Errorf("Error should be nil and verify client should be defaulted") + } + + data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "abcd" + ing.SetAnnotations(data) + _, err = NewParser(fakeSecret).Parse(ing) + if err != nil { + t.Errorf("Error should be nil and verify depth should be defaulted") + } + + data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "nahh" + ing.SetAnnotations(data) + _, err = NewParser(fakeSecret).Parse(ing) + if err == nil { + t.Errorf("Expected error with ingress but got nil") + } + delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)) + + data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "" + ing.SetAnnotations(data) + _, err = NewParser(fakeSecret).Parse(ing) + if err == nil { + t.Errorf("Expected error with ingress CN but got nil") + } + delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)) + ing.SetAnnotations(data) i, err := NewParser(fakeSecret).Parse(ing) diff --git a/internal/ingress/annotations/backendprotocol/main.go b/internal/ingress/annotations/backendprotocol/main.go index c749072e37..a8f7d144aa 100644 --- a/internal/ingress/annotations/backendprotocol/main.go +++ b/internal/ingress/annotations/backendprotocol/main.go @@ -17,48 +17,66 @@ limitations under the License. package backendprotocol import ( - "regexp" - "strings" - networking "k8s.io/api/networking/v1" "k8s.io/klog/v2" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) -// HTTP protocol -const HTTP = "HTTP" - var ( - validProtocols = regexp.MustCompile(`^(AUTO_HTTP|HTTP|HTTPS|GRPC|GRPCS|FCGI)$`) + validProtocols = []string{"auto_http", "http", "https", "ajp", "grpc", "grpcs", "fcgi"} +) + +const ( + http = "HTTP" + backendProtocolAnnotation = "backend-protocol" ) +var backendProtocolConfig = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + backendProtocolAnnotation: { + Validator: parser.ValidateOptions(validProtocols, false, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `this annotation can be used to define which protocol should + be used to communicate with backends`, + }, + }, +} + type backendProtocol struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new backend protocol annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return backendProtocol{r} + return backendProtocol{ + r: r, + annotationConfig: backendProtocolConfig, + } +} + +func (a backendProtocol) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } // ParseAnnotations parses the annotations contained in the ingress // rule used to indicate the backend protocol. func (a backendProtocol) Parse(ing *networking.Ingress) (interface{}, error) { if ing.GetAnnotations() == nil { - return HTTP, nil + return http, nil } - proto, err := parser.GetStringAnnotation("backend-protocol", ing) + proto, err := parser.GetStringAnnotation(backendProtocolAnnotation, ing, a.annotationConfig.Annotations) if err != nil { - return HTTP, nil - } - - proto = strings.TrimSpace(strings.ToUpper(proto)) - if !validProtocols.MatchString(proto) { - klog.Warningf("Protocol %v is not a valid value for the backend-protocol annotation. Using HTTP as protocol", proto) - return HTTP, nil + if errors.IsValidationError(err) { + klog.Warningf("validation error %s. Using HTTP as protocol", err) + } + return http, nil } return proto, nil diff --git a/internal/ingress/annotations/backendprotocol/main_test.go b/internal/ingress/annotations/backendprotocol/main_test.go index e8c018998c..490be447b2 100644 --- a/internal/ingress/annotations/backendprotocol/main_test.go +++ b/internal/ingress/annotations/backendprotocol/main_test.go @@ -77,7 +77,7 @@ func TestParseInvalidAnnotations(t *testing.T) { } // Test invalid annotation set - data[parser.GetAnnotationWithPrefix("backend-protocol")] = "INVALID" + data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = "INVALID" ing.SetAnnotations(data) i, err = NewParser(&resolver.Mock{}).Parse(ing) @@ -97,7 +97,7 @@ func TestParseAnnotations(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("backend-protocol")] = "HTTPS" + data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = " HTTPS " ing.SetAnnotations(data) i, err := NewParser(&resolver.Mock{}).Parse(ing) diff --git a/internal/ingress/annotations/canary/main.go b/internal/ingress/annotations/canary/main.go index d9e53b3b81..e999018033 100644 --- a/internal/ingress/annotations/canary/main.go +++ b/internal/ingress/annotations/canary/main.go @@ -18,14 +18,82 @@ package canary import ( networking "k8s.io/api/networking/v1" + "k8s.io/klog/v2" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + canaryAnnotation = "canary" + canaryWeightAnnotation = "canary-weight" + canaryWeightTotalAnnotation = "canary-weight-total" + canaryByHeaderAnnotation = "canary-by-header" + canaryByHeaderValueAnnotation = "canary-by-header-value" + canaryByHeaderPatternAnnotation = "canary-by-header-pattern" + canaryByCookieAnnotation = "canary-by-cookie" +) + +var CanaryAnnotations = parser.Annotation{ + Group: "canary", + Annotations: parser.AnnotationFields{ + canaryAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables the Ingress spec to act as an alternative service for requests to route to depending on the rules applied`, + }, + canaryWeightAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines the integer based (0 - ) percent of random requests that should be routed to the service specified in the canary Ingress`, + }, + canaryWeightTotalAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation The total weight of traffic. If unspecified, it defaults to 100`, + }, + canaryByHeaderAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines the header that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress. + When the request header is set to 'always', it will be routed to the canary. When the header is set to 'never', it will never be routed to the canary. + For any other value, the header will be ignored and the request compared against the other canary rules by precedence`, + }, + canaryByHeaderValueAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines the header value to match for notifying the Ingress to route the request to the service specified in the Canary Ingress. + When the request header is set to this value, it will be routed to the canary. For any other header value, the header will be ignored and the request compared against the other canary rules by precedence. + This annotation has to be used together with 'canary-by-header'. The annotation is an extension of the 'canary-by-header' to allow customizing the header value instead of using hardcoded values. + It doesn't have any effect if the 'canary-by-header' annotation is not defined`, + }, + canaryByHeaderPatternAnnotation: { + Validator: parser.ValidateRegex(*parser.IsValidRegex, false), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation works the same way as canary-by-header-value except it does PCRE Regex matching. + Note that when 'canary-by-header-value' is set this annotation will be ignored. + When the given Regex causes error during request processing, the request will be considered as not matching.`, + }, + canaryByCookieAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines the cookie that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress. + When the cookie is set to 'always', it will be routed to the canary. When the cookie is set to 'never', it will never be routed to the canary`, + }, + }, +} + type canary struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config returns the configuration rules for setting up the Canary @@ -41,7 +109,10 @@ type Config struct { // NewParser parses the ingress for canary related annotations func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return canary{r} + return canary{ + r: r, + annotationConfig: CanaryAnnotations, + } } // Parse parses the annotations contained in the ingress @@ -50,45 +121,70 @@ func (c canary) Parse(ing *networking.Ingress) (interface{}, error) { config := &Config{} var err error - config.Enabled, err = parser.GetBoolAnnotation("canary", ing) + config.Enabled, err = parser.GetBoolAnnotation(canaryAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s is invalid, defaulting to 'false'", canaryAnnotation) + } config.Enabled = false } - config.Weight, err = parser.GetIntAnnotation("canary-weight", ing) + config.Weight, err = parser.GetIntAnnotation(canaryWeightAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s is invalid, defaulting to '0'", canaryWeightAnnotation) + } config.Weight = 0 } - config.WeightTotal, err = parser.GetIntAnnotation("canary-weight-total", ing) + config.WeightTotal, err = parser.GetIntAnnotation(canaryWeightTotalAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s is invalid, defaulting to '100'", canaryWeightTotalAnnotation) + } config.WeightTotal = 100 } - config.Header, err = parser.GetStringAnnotation("canary-by-header", ing) + config.Header, err = parser.GetStringAnnotation(canaryByHeaderAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderAnnotation) + } config.Header = "" } - config.HeaderValue, err = parser.GetStringAnnotation("canary-by-header-value", ing) + config.HeaderValue, err = parser.GetStringAnnotation(canaryByHeaderValueAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderValueAnnotation) + } config.HeaderValue = "" } - config.HeaderPattern, err = parser.GetStringAnnotation("canary-by-header-pattern", ing) + config.HeaderPattern, err = parser.GetStringAnnotation(canaryByHeaderPatternAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderPatternAnnotation) + } config.HeaderPattern = "" } - config.Cookie, err = parser.GetStringAnnotation("canary-by-cookie", ing) + config.Cookie, err = parser.GetStringAnnotation(canaryByCookieAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%s is invalid, defaulting to ''", canaryByCookieAnnotation) + } config.Cookie = "" } if !config.Enabled && (config.Weight > 0 || len(config.Header) > 0 || len(config.HeaderValue) > 0 || len(config.Cookie) > 0 || len(config.HeaderPattern) > 0) { - return nil, errors.NewInvalidAnnotationConfiguration("canary", "configured but not enabled") + return nil, errors.NewInvalidAnnotationConfiguration(canaryAnnotation, "configured but not enabled") } return config, nil } + +func (c canary) GetDocumentation() parser.AnnotationFields { + return c.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/clientbodybuffersize/main.go b/internal/ingress/annotations/clientbodybuffersize/main.go index 9020ee594f..fd82e85b05 100644 --- a/internal/ingress/annotations/clientbodybuffersize/main.go +++ b/internal/ingress/annotations/clientbodybuffersize/main.go @@ -23,17 +23,44 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + clientBodyBufferSizeAnnotation = "client-body-buffer-size" +) + +var clientBodyBufferSizeConfig = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + clientBodyBufferSizeAnnotation: { + Validator: parser.ValidateRegex(*parser.SizeRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Sets buffer size for reading client request body per location. + In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file. + By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64. + It is usually 16K on other 64-bit platforms. This annotation is applied to each location provided in the ingress rule.`, + }, + }, +} + type clientBodyBufferSize struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new clientBodyBufferSize annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return clientBodyBufferSize{r} + return clientBodyBufferSize{ + r: r, + annotationConfig: clientBodyBufferSizeConfig, + } +} + +func (cbbs clientBodyBufferSize) GetDocumentation() parser.AnnotationFields { + return cbbs.annotationConfig.Annotations } // Parse parses the annotations contained in the ingress rule // used to add an client-body-buffer-size to the provided locations func (cbbs clientBodyBufferSize) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetStringAnnotation("client-body-buffer-size", ing) + return parser.GetStringAnnotation(clientBodyBufferSizeAnnotation, ing, cbbs.annotationConfig.Annotations) } diff --git a/internal/ingress/annotations/clientbodybuffersize/main_test.go b/internal/ingress/annotations/clientbodybuffersize/main_test.go index 9932f83143..0f2c8474ae 100644 --- a/internal/ingress/annotations/clientbodybuffersize/main_test.go +++ b/internal/ingress/annotations/clientbodybuffersize/main_test.go @@ -39,6 +39,9 @@ func TestParse(t *testing.T) { }{ {map[string]string{annotation: "8k"}, "8k"}, {map[string]string{annotation: "16k"}, "16k"}, + {map[string]string{annotation: "10000"}, "10000"}, + {map[string]string{annotation: "16R"}, ""}, + {map[string]string{annotation: "16kkk"}, ""}, {map[string]string{annotation: ""}, ""}, {map[string]string{}, ""}, {nil, ""}, diff --git a/internal/ingress/annotations/connection/main.go b/internal/ingress/annotations/connection/main.go index e9b0c1865d..8b10145a6a 100644 --- a/internal/ingress/annotations/connection/main.go +++ b/internal/ingress/annotations/connection/main.go @@ -17,12 +17,34 @@ limitations under the License. package connection import ( + "regexp" + networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + connectionProxyHeaderAnnotation = "connection-proxy-header" +) + +var ( + validConnectionHeaderValue = regexp.MustCompile(`^(close|keep-alive)$`) +) + +var connectionHeadersAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + connectionProxyHeaderAnnotation: { + Validator: parser.ValidateRegex(*validConnectionHeaderValue, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation allows setting a specific value for "proxy_set_header Connection" directive. Right now it is restricted to "close" or "keep-alive"`, + }, + }, +} + // Config returns the connection header configuration for an Ingress rule type Config struct { Header string `json:"header"` @@ -30,18 +52,22 @@ type Config struct { } type connection struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new port in redirect annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return connection{r} + return connection{ + r: r, + annotationConfig: connectionHeadersAnnotations, + } } // Parse parses the annotations contained in the ingress // rule used to indicate if the connection header should be overridden. func (a connection) Parse(ing *networking.Ingress) (interface{}, error) { - cp, err := parser.GetStringAnnotation("connection-proxy-header", ing) + cp, err := parser.GetStringAnnotation(connectionProxyHeaderAnnotation, ing, a.annotationConfig.Annotations) if err != nil { return &Config{ Enabled: false, @@ -70,3 +96,7 @@ func (r1 *Config) Equal(r2 *Config) bool { return true } + +func (a connection) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/connection/main_test.go b/internal/ingress/annotations/connection/main_test.go index 011a2948cf..a952883855 100644 --- a/internal/ingress/annotations/connection/main_test.go +++ b/internal/ingress/annotations/connection/main_test.go @@ -37,10 +37,12 @@ func TestParse(t *testing.T) { testCases := []struct { annotations map[string]string expected *Config + expectErr bool }{ - {map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}}, - {map[string]string{}, &Config{Enabled: false}}, - {nil, &Config{Enabled: false}}, + {map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}, false}, + {map[string]string{annotation: "not-allowed-value"}, &Config{Enabled: false}, true}, + {map[string]string{}, &Config{Enabled: false}, true}, + {nil, &Config{Enabled: false}, true}, } ing := &networking.Ingress{ @@ -53,11 +55,17 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) - i, _ := ap.Parse(ing) - p, _ := i.(*Config) - + i, err := ap.Parse(ing) + if (err != nil) != testCase.expectErr { + t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations) + } + p, ok := i.(*Config) + if !ok { + t.Fatalf("expected a Config type") + } if !p.Equal(testCase.expected) { t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, p, testCase.annotations) } + } } diff --git a/internal/ingress/annotations/cors/main.go b/internal/ingress/annotations/cors/main.go index 3888f2909e..8ee0b4f38c 100644 --- a/internal/ingress/annotations/cors/main.go +++ b/internal/ingress/annotations/cors/main.go @@ -24,6 +24,7 @@ import ( "k8s.io/klog/v2" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) @@ -38,20 +39,87 @@ var ( // Regex are defined here to prevent information leak, if user tries to set anything not valid // that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc) // Origin must contain a http/s Origin (including or not the port) or the value '*' + // This Regex is composed of the following: + // * Sets a group that can be (https?://)?*?.something.com:port? + // * Allows this to be repeated as much as possible, and separated by comma + // Otherwise it should be '*' + corsOriginRegexValidator = regexp.MustCompile(`^((((https?://)?(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?,?)+)|\*)?$`) + // corsOriginRegex defines the regex for validation inside Parse corsOriginRegex = regexp.MustCompile(`^(https?://(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`) // Method must contain valid methods list (PUT, GET, POST, BLA) // May contain or not spaces between each verb corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`) - // Headers must contain valid values only (X-HEADER12, X-ABC) - // May contain or not spaces between each Header - corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`) // Expose Headers must contain valid values only (*, X-HEADER12, X-ABC) // May contain or not spaces between each Header corsExposeHeadersRegex = regexp.MustCompile(`^(([A-Za-z0-9\-\_]+|\*),?\s?)+$`) ) +const ( + corsEnableAnnotation = "enable-cors" + corsAllowOriginAnnotation = "cors-allow-origin" + corsAllowHeadersAnnotation = "cors-allow-headers" + corsAllowMethodsAnnotation = "cors-allow-methods" + corsAllowCredentialsAnnotation = "cors-allow-credentials" //#nosec G101 + corsExposeHeadersAnnotation = "cors-expose-headers" + corsMaxAgeAnnotation = "cors-max-age" +) + +var corsAnnotation = parser.Annotation{ + Group: "cors", + Annotations: parser.AnnotationFields{ + corsEnableAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables Cross-Origin Resource Sharing (CORS) in an Ingress rule`, + }, + corsAllowOriginAnnotation: { + Validator: parser.ValidateRegex(*corsOriginRegexValidator, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation controls what's the accepted Origin for CORS. + This is a multi-valued field, separated by ','. It must follow this format: http(s)://origin-site.com or http(s)://origin-site.com:port + It also supports single level wildcard subdomains and follows this format: http(s)://*.foo.bar, http(s)://*.bar.foo:8080 or http(s)://*.abc.bar.foo:9000`, + }, + corsAllowHeadersAnnotation: { + Validator: parser.ValidateRegex(*parser.HeadersVariable, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation controls which headers are accepted. + This is a multi-valued field, separated by ',' and accepts letters, numbers, _ and -`, + }, + corsAllowMethodsAnnotation: { + Validator: parser.ValidateRegex(*corsMethodsRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation controls which methods are accepted. + This is a multi-valued field, separated by ',' and accepts only letters (upper and lower case)`, + }, + corsAllowCredentialsAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation controls if credentials can be passed during CORS operations.`, + }, + corsExposeHeadersAnnotation: { + Validator: parser.ValidateRegex(*corsExposeHeadersRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation controls which headers are exposed to response. + This is a multi-valued field, separated by ',' and accepts letters, numbers, _, - and *.`, + }, + corsMaxAgeAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation controls how long, in seconds, preflight requests can be cached.`, + }, + }, +} + type cors struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config contains the Cors configuration to be used in the Ingress @@ -67,7 +135,10 @@ type Config struct { // NewParser creates a new CORS annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return cors{r} + return cors{ + r: r, + annotationConfig: corsAnnotation, + } } // Equal tests for equality between two External types @@ -116,13 +187,16 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) { var err error config := &Config{} - config.CorsEnabled, err = parser.GetBoolAnnotation("enable-cors", ing) + config.CorsEnabled, err = parser.GetBoolAnnotation(corsEnableAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("enable-cors is invalid, defaulting to 'false'") + } config.CorsEnabled = false } config.CorsAllowOrigin = []string{} - unparsedOrigins, err := parser.GetStringAnnotation("cors-allow-origin", ing) + unparsedOrigins, err := parser.GetStringAnnotation(corsAllowOriginAnnotation, ing, c.annotationConfig.Annotations) if err == nil { origins := strings.Split(unparsedOrigins, ",") for _, origin := range origins { @@ -140,33 +214,48 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) { klog.Infof("Current config.corsAllowOrigin %v", config.CorsAllowOrigin) } } else { + if errors.IsValidationError(err) { + klog.Warningf("cors-allow-origin is invalid, defaulting to '*'") + } config.CorsAllowOrigin = []string{"*"} } - config.CorsAllowHeaders, err = parser.GetStringAnnotation("cors-allow-headers", ing) - if err != nil || !corsHeadersRegex.MatchString(config.CorsAllowHeaders) { + config.CorsAllowHeaders, err = parser.GetStringAnnotation(corsAllowHeadersAnnotation, ing, c.annotationConfig.Annotations) + if err != nil || !parser.HeadersVariable.MatchString(config.CorsAllowHeaders) { config.CorsAllowHeaders = defaultCorsHeaders } - config.CorsAllowMethods, err = parser.GetStringAnnotation("cors-allow-methods", ing) + config.CorsAllowMethods, err = parser.GetStringAnnotation(corsAllowMethodsAnnotation, ing, c.annotationConfig.Annotations) if err != nil || !corsMethodsRegex.MatchString(config.CorsAllowMethods) { config.CorsAllowMethods = defaultCorsMethods } - config.CorsAllowCredentials, err = parser.GetBoolAnnotation("cors-allow-credentials", ing) + config.CorsAllowCredentials, err = parser.GetBoolAnnotation(corsAllowCredentialsAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + if errors.IsValidationError(err) { + klog.Warningf("cors-allow-credentials is invalid, defaulting to 'true'") + } + } config.CorsAllowCredentials = true } - config.CorsExposeHeaders, err = parser.GetStringAnnotation("cors-expose-headers", ing) + config.CorsExposeHeaders, err = parser.GetStringAnnotation(corsExposeHeadersAnnotation, ing, c.annotationConfig.Annotations) if err != nil || !corsExposeHeadersRegex.MatchString(config.CorsExposeHeaders) { config.CorsExposeHeaders = "" } - config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing) + config.CorsMaxAge, err = parser.GetIntAnnotation(corsMaxAgeAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("cors-max-age is invalid, defaulting to %d", defaultCorsMaxAge) + } config.CorsMaxAge = defaultCorsMaxAge } return config, nil } + +func (c cors) GetDocumentation() parser.AnnotationFields { + return c.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/cors/main_test.go b/internal/ingress/annotations/cors/main_test.go index 086a59d891..d371d183b4 100644 --- a/internal/ingress/annotations/cors/main_test.go +++ b/internal/ingress/annotations/cors/main_test.go @@ -75,13 +75,13 @@ func TestIngressCorsConfigValid(t *testing.T) { data := map[string]string{} // Valid - data[parser.GetAnnotationWithPrefix("enable-cors")] = "true" - data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "DNT,X-CustomHeader, Keep-Alive,User-Agent" - data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false" - data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH" - data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "https://origin123.test.com:4443" - data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "*, X-CustomResponseHeader" - data[parser.GetAnnotationWithPrefix("cors-max-age")] = "600" + data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "true" + data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "DNT,X-CustomHeader, Keep-Alive,User-Agent" + data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "false" + data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH" + data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "https://origin123.test.com:4443" + data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "*, X-CustomResponseHeader" + data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "600" ing.SetAnnotations(data) corst, err := NewParser(&resolver.Mock{}).Parse(ing) @@ -95,31 +95,31 @@ func TestIngressCorsConfigValid(t *testing.T) { } if !nginxCors.CorsEnabled { - t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("enable-cors")], nginxCors.CorsEnabled) + t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)], nginxCors.CorsEnabled) } if nginxCors.CorsAllowCredentials { - t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-credentials")], nginxCors.CorsAllowCredentials) + t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)], nginxCors.CorsAllowCredentials) } if nginxCors.CorsAllowHeaders != "DNT,X-CustomHeader, Keep-Alive,User-Agent" { - t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-headers")], nginxCors.CorsAllowHeaders) + t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)], nginxCors.CorsAllowHeaders) } if nginxCors.CorsAllowMethods != "GET, PATCH" { - t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-methods")], nginxCors.CorsAllowMethods) + t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)], nginxCors.CorsAllowMethods) } if nginxCors.CorsAllowOrigin[0] != "https://origin123.test.com:4443" { - t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin) + t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)], nginxCors.CorsAllowOrigin) } if nginxCors.CorsExposeHeaders != "*, X-CustomResponseHeader" { - t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-expose-headers")], nginxCors.CorsExposeHeaders) + t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)], nginxCors.CorsExposeHeaders) } if nginxCors.CorsMaxAge != 600 { - t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-max-age")], nginxCors.CorsMaxAge) + t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)], nginxCors.CorsMaxAge) } } @@ -129,13 +129,13 @@ func TestIngressCorsConfigInvalid(t *testing.T) { data := map[string]string{} // Valid - data[parser.GetAnnotationWithPrefix("enable-cors")] = "yes" - data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "@alright, #ingress" - data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "no" - data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH, $nginx" - data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "origin123.test.com:4443" - data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "@alright, #ingress" - data[parser.GetAnnotationWithPrefix("cors-max-age")] = "abcd" + data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "yes" + data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "@alright, #ingress" + data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "no" + data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH, $nginx" + data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "origin123.test.com:4443" + data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "@alright, #ingress" + data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "abcd" ing.SetAnnotations(data) corst, err := NewParser(&resolver.Mock{}).Parse(ing) diff --git a/internal/ingress/annotations/customhttperrors/main.go b/internal/ingress/annotations/customhttperrors/main.go index a05fb16c86..2eb19ed8bb 100644 --- a/internal/ingress/annotations/customhttperrors/main.go +++ b/internal/ingress/annotations/customhttperrors/main.go @@ -17,6 +17,7 @@ limitations under the License. package customhttperrors import ( + "regexp" "strconv" "strings" @@ -26,19 +27,46 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + customHTTPErrorsAnnotation = "custom-http-errors" +) + +var ( + // We accept anything between 400 and 599, on a comma separated. + arrayOfHTTPErrors = regexp.MustCompile(`^(?:[4,5][0-9][0-9],?)*$`) +) + +var customHTTPErrorsAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + customHTTPErrorsAnnotation: { + Validator: parser.ValidateRegex(*arrayOfHTTPErrors, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `If a default backend annotation is specified on the ingress, the errors code specified on this annotation + will be routed to that annotation's default backend service. Otherwise they will be routed to the global default backend. + A comma-separated list of error codes is accepted (anything between 400 and 599, like 403, 503)`, + }, + }, +} + type customhttperrors struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new custom http errors annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return customhttperrors{r} + return customhttperrors{ + r: r, + annotationConfig: customHTTPErrorsAnnotations, + } } // Parse parses the annotations contained in the ingress to use // custom http errors func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) { - c, err := parser.GetStringAnnotation("custom-http-errors", ing) + c, err := parser.GetStringAnnotation("custom-http-errors", ing, e.annotationConfig.Annotations) if err != nil { return nil, err } @@ -55,3 +83,7 @@ func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) { return codes, nil } + +func (e customhttperrors) GetDocumentation() parser.AnnotationFields { + return e.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/defaultbackend/main.go b/internal/ingress/annotations/defaultbackend/main.go index b1685015e0..d73b7fa942 100644 --- a/internal/ingress/annotations/defaultbackend/main.go +++ b/internal/ingress/annotations/defaultbackend/main.go @@ -25,19 +25,40 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + defaultBackendAnnotation = "default-backend" +) + +var defaultBackendAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + defaultBackendAnnotation: { + Validator: parser.ValidateServiceName, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This service will be used to handle the response when the configured service in the Ingress rule does not have any active endpoints. + It will also be used to handle the error responses if both this annotation and the custom-http-errors annotation are set.`, + }, + }, +} + type backend struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new default backend annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return backend{r} + return backend{ + r: r, + annotationConfig: defaultBackendAnnotations, + } } // Parse parses the annotations contained in the ingress to use // a custom default backend func (db backend) Parse(ing *networking.Ingress) (interface{}, error) { - s, err := parser.GetStringAnnotation("default-backend", ing) + s, err := parser.GetStringAnnotation(defaultBackendAnnotation, ing, db.annotationConfig.Annotations) if err != nil { return nil, err } @@ -50,3 +71,7 @@ func (db backend) Parse(ing *networking.Ingress) (interface{}, error) { return svc, nil } + +func (db backend) GetDocumentation() parser.AnnotationFields { + return db.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/defaultbackend/main_test.go b/internal/ingress/annotations/defaultbackend/main_test.go index ec23d32c2a..214d07803b 100644 --- a/internal/ingress/annotations/defaultbackend/main_test.go +++ b/internal/ingress/annotations/defaultbackend/main_test.go @@ -91,21 +91,51 @@ func (m mockService) GetService(name string) (*api.Service, error) { func TestAnnotations(t *testing.T) { ing := buildIngress() - data := map[string]string{} - data[parser.GetAnnotationWithPrefix("default-backend")] = "demo-service" - ing.SetAnnotations(data) - - fakeService := &mockService{} - i, err := NewParser(fakeService).Parse(ing) - if err != nil { - t.Errorf("unexpected error %v", err) + tests := map[string]struct { + expectErr bool + serviceName string + }{ + "valid name": { + serviceName: "demo-service", + expectErr: false, + }, + "not in backend": { + serviceName: "demo1-service", + expectErr: true, + }, + "invalid dns name": { + serviceName: "demo-service.something.tld", + expectErr: true, + }, + "invalid name": { + serviceName: "something/xpto", + expectErr: true, + }, + "invalid characters": { + serviceName: "something;xpto", + expectErr: true, + }, } - svc, ok := i.(*api.Service) - if !ok { - t.Errorf("expected *api.Service but got %v", svc) - } - if svc.Name != "demo-service" { - t.Errorf("expected %v but got %v", "demo-service", svc.Name) + for _, test := range tests { + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(defaultBackendAnnotation)] = test.serviceName + ing.SetAnnotations(data) + + fakeService := &mockService{} + i, err := NewParser(fakeService).Parse(ing) + if (err != nil) != test.expectErr { + t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i) + } + + if !test.expectErr { + svc, ok := i.(*api.Service) + if !ok { + t.Errorf("expected *api.Service but got %v", svc) + } + if svc.Name != test.serviceName { + t.Errorf("expected %v but got %v", test.serviceName, svc.Name) + } + } } } diff --git a/internal/ingress/annotations/globalratelimit/main.go b/internal/ingress/annotations/globalratelimit/main.go index ea9fc46786..97e820edf6 100644 --- a/internal/ingress/annotations/globalratelimit/main.go +++ b/internal/ingress/annotations/globalratelimit/main.go @@ -22,8 +22,10 @@ import ( "time" networking "k8s.io/api/networking/v1" + "k8s.io/klog/v2" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" "k8s.io/ingress-nginx/internal/net" @@ -32,6 +34,46 @@ import ( const defaultKey = "$remote_addr" +const ( + globalRateLimitAnnotation = "global-rate-limit" + globalRateLimitWindowAnnotation = "global-rate-limit-window" + globalRateLimitKeyAnnotation = "global-rate-limit-key" + globalRateLimitIgnoredCidrsAnnotation = "global-rate-limit-ignored-cidrs" +) + +var globalRateLimitAnnotationConfig = parser.Annotation{ + Group: "ratelimit", + Annotations: parser.AnnotationFields{ + globalRateLimitAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation configures maximum allowed number of requests per window`, + }, + globalRateLimitWindowAnnotation: { + Validator: parser.ValidateDuration, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `Configures a time window (i.e 1m) that the limit is applied`, + }, + globalRateLimitKeyAnnotation: { + Validator: parser.ValidateRegex(*parser.NGINXVariable, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation Configures a key for counting the samples. Defaults to $remote_addr. + You can also combine multiple NGINX variables here, like ${remote_addr}-${http_x_api_client} which would mean the limit will be applied to + requests coming from the same API client (indicated by X-API-Client HTTP request header) with the same source IP address`, + }, + globalRateLimitIgnoredCidrsAnnotation: { + Validator: parser.ValidateCIDRs, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines a comma separated list of IPs and CIDRs to match client IP against. + When there's a match request is not considered for rate limiting.`, + }, + }, +} + // Config encapsulates all global rate limit attributes type Config struct { Namespace string `json:"namespace"` @@ -63,12 +105,16 @@ func (l *Config) Equal(r *Config) bool { } type globalratelimit struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new globalratelimit annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return globalratelimit{r} + return globalratelimit{ + r: r, + annotationConfig: globalRateLimitAnnotationConfig, + } } // Parse extracts globalratelimit annotations from the given ingress @@ -76,8 +122,16 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation { func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) { config := &Config{} - limit, _ := parser.GetIntAnnotation("global-rate-limit", ing) - rawWindowSize, _ := parser.GetStringAnnotation("global-rate-limit-window", ing) + limit, err := parser.GetIntAnnotation(globalRateLimitAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsInvalidContent(err) { + return nil, err + } + rawWindowSize, err := parser.GetStringAnnotation(globalRateLimitWindowAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return config, ing_errors.LocationDenied{ + Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: %w", err), + } + } if limit == 0 || len(rawWindowSize) == 0 { return config, nil @@ -90,12 +144,18 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) { } } - key, _ := parser.GetStringAnnotation("global-rate-limit-key", ing) + key, err := parser.GetStringAnnotation(globalRateLimitKeyAnnotation, ing, a.annotationConfig.Annotations) + if err != nil { + klog.Warningf("invalid %s, defaulting to %s", globalRateLimitKeyAnnotation, defaultKey) + } if len(key) == 0 { key = defaultKey } - rawIgnoredCIDRs, _ := parser.GetStringAnnotation("global-rate-limit-ignored-cidrs", ing) + rawIgnoredCIDRs, err := parser.GetStringAnnotation(globalRateLimitIgnoredCidrsAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsInvalidContent(err) { + return nil, err + } ignoredCIDRs, err := net.ParseCIDRs(rawIgnoredCIDRs) if err != nil { return nil, err @@ -109,3 +169,7 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) { return config, nil } + +func (a globalratelimit) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/globalratelimit/main_test.go b/internal/ingress/annotations/globalratelimit/main_test.go index 815d6cfff1..5d79226666 100644 --- a/internal/ingress/annotations/globalratelimit/main_test.go +++ b/internal/ingress/annotations/globalratelimit/main_test.go @@ -149,6 +149,22 @@ func TestGlobalRateLimiting(t *testing.T) { }, nil, }, + { + "global-rate-limit-complex-key", + map[string]string{ + annRateLimit: "100", + annRateLimitWindow: "2m", + annRateLimitKey: "${http_x_api_user}${otherinfo}", + }, + &Config{ + Namespace: expectedUID, + Limit: 100, + WindowSize: 120, + Key: "${http_x_api_user}${otherinfo}", + IgnoredCIDRs: make([]string, 0), + }, + nil, + }, { "incorrect duration for window", map[string]string{ @@ -157,8 +173,8 @@ func TestGlobalRateLimiting(t *testing.T) { annRateLimitKey: "$http_x_api_user", }, &Config{}, - ing_errors.LocationDenied{ - Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: time: unknown unit \"mb\" in duration \"2mb\""), + ing_errors.ValidationError{ + Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: annotation nginx.ingress.kubernetes.io/global-rate-limit-window contains invalid value"), }, }, } @@ -168,7 +184,7 @@ func TestGlobalRateLimiting(t *testing.T) { i, actualErr := NewParser(mockBackend{}).Parse(ing) if (testCase.expectedErr == nil || actualErr == nil) && testCase.expectedErr != actualErr { - t.Errorf("expected error 'nil' but got '%v'", actualErr) + t.Errorf("%s expected error '%v' but got '%v'", testCase.title, testCase.expectedErr, actualErr) } else if testCase.expectedErr != nil && actualErr != nil && testCase.expectedErr.Error() != actualErr.Error() { t.Errorf("expected error '%v' but got '%v'", testCase.expectedErr, actualErr) diff --git a/internal/ingress/annotations/http2pushpreload/main.go b/internal/ingress/annotations/http2pushpreload/main.go index 27d3368f43..263d7a7b2f 100644 --- a/internal/ingress/annotations/http2pushpreload/main.go +++ b/internal/ingress/annotations/http2pushpreload/main.go @@ -23,17 +23,41 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + http2PushPreloadAnnotation = "http2-push-preload" +) + +var http2PushPreloadAnnotations = parser.Annotation{ + Group: "", // TODO: TBD + Annotations: parser.AnnotationFields{ + http2PushPreloadAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `Enables automatic conversion of preload links specified in the “Link” response header fields into push requests`, + }, + }, +} + type http2PushPreload struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new http2PushPreload annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return http2PushPreload{r} + return http2PushPreload{ + r: r, + annotationConfig: http2PushPreloadAnnotations, + } } // Parse parses the annotations contained in the ingress rule // used to add http2 push preload to the server func (h2pp http2PushPreload) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetBoolAnnotation("http2-push-preload", ing) + return parser.GetBoolAnnotation(http2PushPreloadAnnotation, ing, h2pp.annotationConfig.Annotations) +} + +func (h2pp http2PushPreload) GetDocumentation() parser.AnnotationFields { + return h2pp.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/http2pushpreload/main_test.go b/internal/ingress/annotations/http2pushpreload/main_test.go index bb98af93f3..eb6e9111d5 100644 --- a/internal/ingress/annotations/http2pushpreload/main_test.go +++ b/internal/ingress/annotations/http2pushpreload/main_test.go @@ -23,11 +23,12 @@ import ( networking "k8s.io/api/networking/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) func TestParse(t *testing.T) { - annotation := parser.GetAnnotationWithPrefix("http2-push-preload") + annotation := parser.GetAnnotationWithPrefix(http2PushPreloadAnnotation) ap := NewParser(&resolver.Mock{}) if ap == nil { t.Fatalf("expected a parser.IngressAnnotation but returned nil") @@ -36,12 +37,14 @@ func TestParse(t *testing.T) { testCases := []struct { annotations map[string]string expected bool + expectErr bool }{ - {map[string]string{annotation: "true"}, true}, - {map[string]string{annotation: "1"}, true}, - {map[string]string{annotation: ""}, false}, - {map[string]string{}, false}, - {nil, false}, + {map[string]string{annotation: "true"}, true, false}, + {map[string]string{annotation: "1"}, true, false}, + {map[string]string{annotation: "xpto"}, false, true}, + {map[string]string{annotation: ""}, false, false}, + {map[string]string{}, false, false}, + {nil, false, false}, } ing := &networking.Ingress{ @@ -54,7 +57,10 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) - result, _ := ap.Parse(ing) + result, err := ap.Parse(ing) + if ((err != nil) != testCase.expectErr) && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) { + t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations) + } if result != testCase.expected { t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) } diff --git a/internal/ingress/annotations/ipwhitelist/main.go b/internal/ingress/annotations/ipallowlist/main.go similarity index 56% rename from internal/ingress/annotations/ipwhitelist/main.go rename to internal/ingress/annotations/ipallowlist/main.go index 63c049fef3..94ea0a7f1d 100644 --- a/internal/ingress/annotations/ipwhitelist/main.go +++ b/internal/ingress/annotations/ipallowlist/main.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package ipwhitelist +package ipallowlist import ( "fmt" @@ -30,6 +30,24 @@ import ( "k8s.io/ingress-nginx/pkg/util/sets" ) +const ( + ipWhitelistAnnotation = "whitelist-source-range" + ipAllowlistAnnotation = "allowlist-source-range" +) + +var allowlistAnnotations = parser.Annotation{ + Group: "acl", + Annotations: parser.AnnotationFields{ + ipAllowlistAnnotation: { + Validator: parser.ValidateCIDRs, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access + Documentation: `This annotation allows setting a list of IPs and networks allowed to access this Location`, + AnnotationAliases: []string{ipWhitelistAnnotation}, + }, + }, +} + // SourceRange returns the CIDR type SourceRange struct { CIDR []string `json:"cidr,omitempty"` @@ -47,36 +65,47 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool { return sets.StringElementsMatch(sr1.CIDR, sr2.CIDR) } -type ipwhitelist struct { - r resolver.Resolver +type ipallowlist struct { + r resolver.Resolver + annotationConfig parser.Annotation } -// NewParser creates a new whitelist annotation parser +// NewParser creates a new ipallowlist annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return ipwhitelist{r} + return ipallowlist{ + r: r, + annotationConfig: allowlistAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress // rule used to limit access to certain client addresses or networks. // Multiple ranges can specified using commas as separator // e.g. `18.0.0.0/8,56.0.0.0/8` -func (a ipwhitelist) Parse(ing *networking.Ingress) (interface{}, error) { +func (a ipallowlist) Parse(ing *networking.Ingress) (interface{}, error) { defBackend := a.r.GetDefaultBackend() - defaultWhitelistSourceRange := make([]string, len(defBackend.WhitelistSourceRange)) - copy(defaultWhitelistSourceRange, defBackend.WhitelistSourceRange) - sort.Strings(defaultWhitelistSourceRange) + defaultAllowlistSourceRange := make([]string, len(defBackend.WhitelistSourceRange)) + copy(defaultAllowlistSourceRange, defBackend.WhitelistSourceRange) + sort.Strings(defaultAllowlistSourceRange) - val, err := parser.GetStringAnnotation("whitelist-source-range", ing) + val, err := parser.GetStringAnnotation(ipAllowlistAnnotation, ing, a.annotationConfig.Annotations) // A missing annotation is not a problem, just use the default - if err == ing_errors.ErrMissingAnnotations { - return &SourceRange{CIDR: defaultWhitelistSourceRange}, nil + if err != nil { + if err == ing_errors.ErrMissingAnnotations { + return &SourceRange{CIDR: defaultAllowlistSourceRange}, nil + } + + return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{ + Reason: err, + } + } values := strings.Split(val, ",") ipnets, ips, err := net.ParseIPNets(values...) if err != nil && len(ips) == 0 { - return &SourceRange{CIDR: defaultWhitelistSourceRange}, ing_errors.LocationDenied{ + return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{ Reason: fmt.Errorf("the annotation does not contain a valid IP address or network: %w", err), } } @@ -93,3 +122,7 @@ func (a ipwhitelist) Parse(ing *networking.Ingress) (interface{}, error) { return &SourceRange{cidrs}, nil } + +func (a ipallowlist) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/ipwhitelist/main_test.go b/internal/ingress/annotations/ipallowlist/main_test.go similarity index 64% rename from internal/ingress/annotations/ipwhitelist/main_test.go rename to internal/ingress/annotations/ipallowlist/main_test.go index 5042bb2003..b16b25a5b5 100644 --- a/internal/ingress/annotations/ipwhitelist/main_test.go +++ b/internal/ingress/annotations/ipallowlist/main_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package ipwhitelist +package ipallowlist import ( "testing" @@ -86,12 +86,12 @@ func TestParseAnnotations(t *testing.T) { "test parse a invalid net": { net: "ww", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww", + errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value", }, "test parse a empty net": { net: "", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ", + errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()", }, "test parse multiple valid cidr": { net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24", @@ -102,16 +102,16 @@ func TestParseAnnotations(t *testing.T) { for testName, test := range tests { data := map[string]string{} - data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net + data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = test.net ing.SetAnnotations(data) p := NewParser(&resolver.Mock{}) i, err := p.Parse(ing) - if err != nil && !test.expectErr { - t.Errorf("%v:unexpected error: %v", testName, err) + if (err != nil) != test.expectErr { + t.Errorf("%s expected error: %t got error: %t err value: %s. %+v", testName, test.expectErr, err != nil, err, i) } - if test.expectErr { + if test.expectErr && err != nil { if err.Error() != test.errOut { - t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error()) + t.Errorf("expected error %s but got %s", test.errOut, err) } } if !test.expectErr { @@ -137,7 +137,7 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend { } } -// Test that when we have a whitelist set on the Backend that is used when we +// Test that when we have a allowlist set on the Backend that is used when we // don't have the annotation func TestParseAnnotationsWithDefaultConfig(t *testing.T) { ing := buildIngress() @@ -158,12 +158,63 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) { "test parse a invalid net": { net: "ww", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww", + errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value", }, "test parse a empty net": { net: "", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ", + errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()", + }, + "test parse multiple valid cidr": { + net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24", + expectCidr: []string{"1.1.1.1/32", "2.2.2.2/32", "3.3.3.0/24"}, + expectErr: false, + }, + } + + for testName, test := range tests { + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = test.net + ing.SetAnnotations(data) + p := NewParser(mockBackend) + i, err := p.Parse(ing) + if (err != nil) != test.expectErr { + t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i) + } + if test.expectErr && err != nil { + if err.Error() != test.errOut { + t.Errorf("expected error %s but got %s", test.errOut, err) + } + } + if !test.expectErr { + sr, ok := i.(*SourceRange) + if !ok { + t.Errorf("%v:expected a SourceRange type", testName) + } + if !strsEquals(sr.CIDR, test.expectCidr) { + t.Errorf("%v:expected %v CIDR but %v returned", testName, test.expectCidr, sr.CIDR) + } + } + } +} + +// Test that when we have a whitelist set on the Backend that is used when we +// don't have the annotation +func TestLegacyAnnotation(t *testing.T) { + ing := buildIngress() + + mockBackend := mockBackend{} + + tests := map[string]struct { + net string + expectCidr []string + expectErr bool + errOut string + }{ + "test parse a valid net": { + net: "10.0.0.0/24", + expectCidr: []string{"10.0.0.0/24"}, + expectErr: false, }, "test parse multiple valid cidr": { net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24", @@ -174,16 +225,16 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) { for testName, test := range tests { data := map[string]string{} - data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net + data[parser.GetAnnotationWithPrefix(ipWhitelistAnnotation)] = test.net ing.SetAnnotations(data) p := NewParser(mockBackend) i, err := p.Parse(ing) - if err != nil && !test.expectErr { - t.Errorf("%v:unexpected error: %v", testName, err) + if (err != nil) != test.expectErr { + t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i) } - if test.expectErr { + if test.expectErr && err != nil { if err.Error() != test.errOut { - t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error()) + t.Errorf("expected error %s but got %s", test.errOut, err) } } if !test.expectErr { diff --git a/internal/ingress/annotations/ipdenylist/main.go b/internal/ingress/annotations/ipdenylist/main.go index f6a0e10f19..6d09578be4 100644 --- a/internal/ingress/annotations/ipdenylist/main.go +++ b/internal/ingress/annotations/ipdenylist/main.go @@ -30,6 +30,22 @@ import ( "k8s.io/ingress-nginx/pkg/util/sets" ) +const ( + ipDenylistAnnotation = "denylist-source-range" +) + +var denylistAnnotations = parser.Annotation{ + Group: "acl", + Annotations: parser.AnnotationFields{ + ipDenylistAnnotation: { + Validator: parser.ValidateCIDRs, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access + Documentation: `This annotation allows setting a list of IPs and networks that should be blocked to access this Location`, + }, + }, +} + // SourceRange returns the CIDR type SourceRange struct { CIDR []string `json:"cidr,omitempty"` @@ -48,12 +64,16 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool { } type ipdenylist struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new denylist annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return ipdenylist{r} + return ipdenylist{ + r: r, + annotationConfig: denylistAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress @@ -67,10 +87,16 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) { copy(defaultDenylistSourceRange, defBackend.DenylistSourceRange) sort.Strings(defaultDenylistSourceRange) - val, err := parser.GetStringAnnotation("denylist-source-range", ing) - // A missing annotation is not a problem, just use the default - if err == ing_errors.ErrMissingAnnotations { - return &SourceRange{CIDR: defaultDenylistSourceRange}, nil + val, err := parser.GetStringAnnotation(ipDenylistAnnotation, ing, a.annotationConfig.Annotations) + if err != nil { + if err == ing_errors.ErrMissingAnnotations { + return &SourceRange{CIDR: defaultDenylistSourceRange}, nil + } + + return &SourceRange{CIDR: defaultDenylistSourceRange}, ing_errors.LocationDenied{ + Reason: err, + } + } values := strings.Split(val, ",") @@ -93,3 +119,7 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) { return &SourceRange{cidrs}, nil } + +func (a ipdenylist) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/ipdenylist/main_test.go b/internal/ingress/annotations/ipdenylist/main_test.go index eb69aa5202..ebd81179a5 100644 --- a/internal/ingress/annotations/ipdenylist/main_test.go +++ b/internal/ingress/annotations/ipdenylist/main_test.go @@ -86,17 +86,17 @@ func TestParseAnnotations(t *testing.T) { "test parse a invalid net": { net: "ww", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww", + errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value", }, "test parse a empty net": { net: "", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ", + errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()", }, "test parse a malicious escaped string": { net: `10.0.0.0/8"rm /tmp",11.0.0.0/8`, expectErr: true, - errOut: `the annotation does not contain a valid IP address or network: invalid CIDR address: 10.0.0.0/8"rm /tmp"`, + errOut: `annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value`, }, "test parse multiple valid cidr": { net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24", @@ -107,16 +107,16 @@ func TestParseAnnotations(t *testing.T) { for testName, test := range tests { data := map[string]string{} - data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net + data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = test.net ing.SetAnnotations(data) p := NewParser(&resolver.Mock{}) i, err := p.Parse(ing) - if err != nil && !test.expectErr { - t.Errorf("%v:unexpected error: %v", testName, err) + if (err != nil) != test.expectErr { + t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i) } - if test.expectErr { + if test.expectErr && err != nil { if err.Error() != test.errOut { - t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error()) + t.Errorf("expected error %s but got %s", test.errOut, err) } } if !test.expectErr { @@ -163,12 +163,12 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) { "test parse a invalid net": { net: "ww", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww", + errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value", }, "test parse a empty net": { net: "", expectErr: true, - errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ", + errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()", }, "test parse multiple valid cidr": { net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24", @@ -179,16 +179,16 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) { for testName, test := range tests { data := map[string]string{} - data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net + data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = test.net ing.SetAnnotations(data) p := NewParser(mockBackend) i, err := p.Parse(ing) - if err != nil && !test.expectErr { - t.Errorf("%v:unexpected error: %v", testName, err) + if (err != nil) != test.expectErr { + t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i) } - if test.expectErr { + if test.expectErr && err != nil { if err.Error() != test.errOut { - t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error()) + t.Errorf("expected error %s but got %s", test.errOut, err) } } if !test.expectErr { diff --git a/internal/ingress/annotations/loadbalancing/main.go b/internal/ingress/annotations/loadbalancing/main.go index a8b4335e6f..4555def4d8 100644 --- a/internal/ingress/annotations/loadbalancing/main.go +++ b/internal/ingress/annotations/loadbalancing/main.go @@ -23,18 +23,47 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +// LB Alghorithms are defined in https://github.com/kubernetes/ingress-nginx/blob/d3e75b056f77be54e01bdb18675f1bb46caece31/rootfs/etc/nginx/lua/balancer.lua#L28 + +const ( + loadBalanceAlghoritmAnnotation = "load-balance" +) + +var loadBalanceAlghoritms = []string{"round_robin", "chash", "chashsubset", "sticky_balanced", "sticky_persistent", "ewma"} + +var loadBalanceAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + loadBalanceAlghoritmAnnotation: { + Validator: parser.ValidateOptions(loadBalanceAlghoritms, true, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation allows setting the load balancing alghorithm that should be used. If none is specified, defaults to + the default configured by Ingress admin, otherwise to round_robin`, + }, + }, +} + type loadbalancing struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } -// NewParser creates a new CORS annotation parser +// NewParser creates a new Load Balancer annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return loadbalancing{r} + return loadbalancing{ + r: r, + annotationConfig: loadBalanceAnnotations, + } } // Parse parses the annotations contained in the ingress rule // used to indicate if the location/s contains a fragment of // configuration to be included inside the paths of the rules func (a loadbalancing) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetStringAnnotation("load-balance", ing) + return parser.GetStringAnnotation(loadBalanceAlghoritmAnnotation, ing, a.annotationConfig.Annotations) +} + +func (a loadbalancing) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/loadbalancing/main_test.go b/internal/ingress/annotations/loadbalancing/main_test.go index e2be5c0ae4..b0442c37fc 100644 --- a/internal/ingress/annotations/loadbalancing/main_test.go +++ b/internal/ingress/annotations/loadbalancing/main_test.go @@ -38,7 +38,8 @@ func TestParse(t *testing.T) { annotations map[string]string expected string }{ - {map[string]string{annotation: "ip_hash"}, "ip_hash"}, + {map[string]string{annotation: "ewma"}, "ewma"}, + {map[string]string{annotation: "ip_hash"}, ""}, // This is invalid and should not return anything {map[string]string{}, ""}, {nil, ""}, } diff --git a/internal/ingress/annotations/log/main.go b/internal/ingress/annotations/log/main.go index 4bc76dcf7a..e42de2bd18 100644 --- a/internal/ingress/annotations/log/main.go +++ b/internal/ingress/annotations/log/main.go @@ -23,8 +23,32 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + enableAccessLogAnnotation = "enable-access-log" + enableRewriteLogAnnotation = "enable-rewrite-log" +) + +var logAnnotations = parser.Annotation{ + Group: "log", + Annotations: parser.AnnotationFields{ + enableAccessLogAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This configuration setting allows you to control if this location should generate an access_log`, + }, + enableRewriteLogAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This configuration setting allows you to control if this location should generate logs from the rewrite feature usage`, + }, + }, +} + type log struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config contains the configuration to be used in the Ingress @@ -48,7 +72,10 @@ func (bd1 *Config) Equal(bd2 *Config) bool { // NewParser creates a new log annotations parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return log{r} + return log{ + r: r, + annotationConfig: logAnnotations, + } } // Parse parses the annotations contained in the ingress @@ -57,15 +84,19 @@ func (l log) Parse(ing *networking.Ingress) (interface{}, error) { var err error config := &Config{} - config.Access, err = parser.GetBoolAnnotation("enable-access-log", ing) + config.Access, err = parser.GetBoolAnnotation(enableAccessLogAnnotation, ing, l.annotationConfig.Annotations) if err != nil { config.Access = true } - config.Rewrite, err = parser.GetBoolAnnotation("enable-rewrite-log", ing) + config.Rewrite, err = parser.GetBoolAnnotation(enableRewriteLogAnnotation, ing, l.annotationConfig.Annotations) if err != nil { config.Rewrite = false } return config, nil } + +func (l log) GetDocumentation() parser.AnnotationFields { + return l.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/log/main_test.go b/internal/ingress/annotations/log/main_test.go index c4632b0104..df97c2e5b5 100644 --- a/internal/ingress/annotations/log/main_test.go +++ b/internal/ingress/annotations/log/main_test.go @@ -73,7 +73,7 @@ func TestIngressAccessLogConfig(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("enable-access-log")] = "false" + data[parser.GetAnnotationWithPrefix(enableAccessLogAnnotation)] = "false" ing.SetAnnotations(data) log, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -91,7 +91,7 @@ func TestIngressRewriteLogConfig(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("enable-rewrite-log")] = "true" + data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "true" ing.SetAnnotations(data) log, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -104,3 +104,21 @@ func TestIngressRewriteLogConfig(t *testing.T) { t.Errorf("expected rewrite log to be enabled but it is disabled") } } + +func TestInvalidBoolConfig(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "blo" + ing.SetAnnotations(data) + + log, _ := NewParser(&resolver.Mock{}).Parse(ing) + nginxLogs, ok := log.(*Config) + if !ok { + t.Errorf("expected a Config type") + } + + if !nginxLogs.Access { + t.Errorf("expected access log to be enabled due to invalid config, but it is disabled") + } +} diff --git a/internal/ingress/annotations/mirror/main.go b/internal/ingress/annotations/mirror/main.go index 9cb1b0ede1..33133bedca 100644 --- a/internal/ingress/annotations/mirror/main.go +++ b/internal/ingress/annotations/mirror/main.go @@ -18,13 +18,50 @@ package mirror import ( "fmt" + "regexp" "strings" networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" + "k8s.io/klog/v2" ) +const ( + mirrorRequestBodyAnnotation = "mirror-request-body" + mirrorTargetAnnotation = "mirror-target" + mirrorHostAnnotation = "mirror-host" +) + +var ( + OnOffRegex = regexp.MustCompile(`^(on|off)$`) +) + +var mirrorAnnotation = parser.Annotation{ + Group: "mirror", + Annotations: parser.AnnotationFields{ + mirrorRequestBodyAnnotation: { + Validator: parser.ValidateRegex(*OnOffRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines if the request-body should be sent to the mirror backend. Can be 'on' or 'off'`, + }, + mirrorTargetAnnotation: { + Validator: parser.ValidateServerName, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation enables a request to be mirrored to a mirror backend.`, + }, + mirrorHostAnnotation: { + Validator: parser.ValidateServerName, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation defines if a specific Host header should be set for mirrored request.`, + }, + }, +} + // Config returns the mirror to use in a given location type Config struct { Source string `json:"source"` @@ -63,12 +100,16 @@ func (m1 *Config) Equal(m2 *Config) bool { } type mirror struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new mirror configuration annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return mirror{r} + return mirror{ + r: r, + annotationConfig: mirrorAnnotation, + } } // ParseAnnotations parses the annotations contained in the ingress @@ -79,19 +120,29 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) { } var err error - config.RequestBody, err = parser.GetStringAnnotation("mirror-request-body", ing) + config.RequestBody, err = parser.GetStringAnnotation(mirrorRequestBodyAnnotation, ing, a.annotationConfig.Annotations) if err != nil || config.RequestBody != "off" { + if errors.IsValidationError(err) { + klog.Warningf("annotation %s contains invalid value", mirrorRequestBodyAnnotation) + } config.RequestBody = "on" } - config.Target, err = parser.GetStringAnnotation("mirror-target", ing) + config.Target, err = parser.GetStringAnnotation(mirrorTargetAnnotation, ing, a.annotationConfig.Annotations) if err != nil { - config.Target = "" - config.Source = "" + if errors.IsValidationError(err) { + klog.Warningf("annotation %s contains invalid value, defaulting", mirrorTargetAnnotation) + } else { + config.Target = "" + config.Source = "" + } } - config.Host, err = parser.GetStringAnnotation("mirror-host", ing) + config.Host, err = parser.GetStringAnnotation(mirrorHostAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("annotation %s contains invalid value, defaulting", mirrorHostAnnotation) + } if config.Target != "" { target := strings.Split(config.Target, "$") @@ -106,3 +157,7 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) { return config, nil } + +func (a mirror) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/mirror/main_test.go b/internal/ingress/annotations/mirror/main_test.go index add90d7689..1f6b44d61d 100644 --- a/internal/ingress/annotations/mirror/main_test.go +++ b/internal/ingress/annotations/mirror/main_test.go @@ -94,13 +94,13 @@ func TestParse(t *testing.T) { Source: ngxURI, RequestBody: "on", Target: "http://some.test.env.com", - Host: "someInvalidParm.%^&*()_=!@#'\"", + Host: "some.test.env.com", }}, {map[string]string{backendURL: "http://some.test.env.com", host: "_sbrubles-i\"@xpto:12345"}, &Config{ Source: ngxURI, RequestBody: "on", Target: "http://some.test.env.com", - Host: "_sbrubles-i\"@xpto:12345", + Host: "some.test.env.com", }}, } @@ -115,9 +115,12 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) - result, _ := ap.Parse(ing) + result, err := ap.Parse(ing) + if err != nil { + t.Errorf(err.Error()) + } if !reflect.DeepEqual(result, testCase.expected) { - t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) + t.Errorf("expected %+v but returned %+v, annotations: %s", testCase.expected, result, testCase.annotations) } } } diff --git a/internal/ingress/annotations/modsecurity/main.go b/internal/ingress/annotations/modsecurity/main.go index c537394413..607d98d466 100644 --- a/internal/ingress/annotations/modsecurity/main.go +++ b/internal/ingress/annotations/modsecurity/main.go @@ -19,9 +19,48 @@ package modsecurity import ( networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" + "k8s.io/klog/v2" ) +const ( + modsecEnableAnnotation = "enable-modsecurity" + modsecEnableOwaspCoreAnnotation = "enable-owasp-core-rules" + modesecTransactionIdAnnotation = "modsecurity-transaction-id" + modsecSnippetAnnotation = "modsecurity-snippet" +) + +var modsecurityAnnotation = parser.Annotation{ + Group: "modsecurity", + Annotations: parser.AnnotationFields{ + modsecEnableAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables ModSecurity`, + }, + modsecEnableOwaspCoreAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables the OWASP Core Rule Set`, + }, + modesecTransactionIdAnnotation: { + Validator: parser.ValidateRegex(*parser.NGINXVariable, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation enables passing an NGINX variable to ModSecurity.`, + }, + modsecSnippetAnnotation: { + Validator: parser.ValidateNull, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskCritical, + Documentation: `This annotation enables adding a specific snippet configuration for ModSecurity`, + }, + }, +} + // Config contains ModSecurity Configuration items type Config struct { Enable bool `json:"enable-modsecurity"` @@ -60,11 +99,15 @@ func (modsec1 *Config) Equal(modsec2 *Config) bool { // NewParser creates a new ModSecurity annotation parser func NewParser(resolver resolver.Resolver) parser.IngressAnnotation { - return modSecurity{resolver} + return modSecurity{ + r: resolver, + annotationConfig: modsecurityAnnotation, + } } type modSecurity struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Parse parses the annotations contained in the ingress @@ -74,26 +117,39 @@ func (a modSecurity) Parse(ing *networking.Ingress) (interface{}, error) { config := &Config{} config.EnableSet = true - config.Enable, err = parser.GetBoolAnnotation("enable-modsecurity", ing) + config.Enable, err = parser.GetBoolAnnotation(modsecEnableAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsInvalidContent(err) { + klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableAnnotation) + } config.Enable = false config.EnableSet = false } - config.OWASPRules, err = parser.GetBoolAnnotation("enable-owasp-core-rules", ing) + config.OWASPRules, err = parser.GetBoolAnnotation(modsecEnableOwaspCoreAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsInvalidContent(err) { + klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableOwaspCoreAnnotation) + } config.OWASPRules = false } - config.TransactionID, err = parser.GetStringAnnotation("modsecurity-transaction-id", ing) + config.TransactionID, err = parser.GetStringAnnotation(modesecTransactionIdAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsInvalidContent(err) { + klog.Warningf("annotation %s contains invalid directive, defaulting", modesecTransactionIdAnnotation) + } config.TransactionID = "" } - config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing) + config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing, a.annotationConfig.Annotations) if err != nil { config.Snippet = "" } return config, nil } + +func (a modSecurity) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/opentelemetry/main.go b/internal/ingress/annotations/opentelemetry/main.go index 7dd2923228..89ab4b3c8d 100644 --- a/internal/ingress/annotations/opentelemetry/main.go +++ b/internal/ingress/annotations/opentelemetry/main.go @@ -17,14 +17,51 @@ limitations under the License. package opentelemetry import ( + "regexp" + networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + enableOpenTelemetryAnnotation = "enable-opentelemetry" + otelTrustSpanAnnotation = "opentelemetry-trust-incoming-span" + otelOperationNameAnnotation = "opentelemetry-operation-name" +) + +var regexOperationName = regexp.MustCompile(`^[A-Za-z0-9_\-]*$`) + +var otelAnnotations = parser.Annotation{ + Group: "opentelemetry", + Annotations: parser.AnnotationFields{ + enableOpenTelemetryAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines if Open Telemetry collector should be enable for this location. OpenTelemetry should + already be configured by Ingress administrator`, + }, + otelTrustSpanAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`, + }, + otelOperationNameAnnotation: { + Validator: parser.ValidateRegex(*regexOperationName, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines what operation name should be added to the span`, + }, + }, +} + type opentelemetry struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config contains the configuration to be used in the Ingress @@ -64,13 +101,16 @@ func (bd1 *Config) Equal(bd2 *Config) bool { // NewParser creates a new serviceUpstream annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return opentelemetry{r} + return opentelemetry{ + r: r, + annotationConfig: otelAnnotations, + } } // Parse parses the annotations to look for opentelemetry configurations func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) { cfg := Config{} - enabled, err := parser.GetBoolAnnotation("enable-opentelemetry", ing) + enabled, err := parser.GetBoolAnnotation(enableOpenTelemetryAnnotation, ing, c.annotationConfig.Annotations) if err != nil { return &cfg, nil } @@ -80,10 +120,13 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) { return &cfg, nil } - trustEnabled, err := parser.GetBoolAnnotation("opentelemetry-trust-incoming-span", ing) + trustEnabled, err := parser.GetBoolAnnotation(otelTrustSpanAnnotation, ing, c.annotationConfig.Annotations) if err != nil { - operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing) + operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + return nil, err + } return &cfg, nil } cfg.OperationName = operationName @@ -92,10 +135,17 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) { cfg.TrustSet = true cfg.TrustEnabled = trustEnabled - operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing) + operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + return nil, err + } return &cfg, nil } cfg.OperationName = operationName return &cfg, nil } + +func (c opentelemetry) GetDocumentation() parser.AnnotationFields { + return c.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/opentelemetry/main_test.go b/internal/ingress/annotations/opentelemetry/main_test.go index 619108fa6c..c78ebc8b32 100644 --- a/internal/ingress/annotations/opentelemetry/main_test.go +++ b/internal/ingress/annotations/opentelemetry/main_test.go @@ -73,7 +73,7 @@ func TestIngressAnnotationOpentelemetrySetTrue(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true" + data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true" ing.SetAnnotations(data) val, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -100,7 +100,7 @@ func TestIngressAnnotationOpentelemetrySetFalse(t *testing.T) { // Test with explicitly set to false data := map[string]string{} - data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "false" + data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "false" ing.SetAnnotations(data) val, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -123,12 +123,15 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) { data := map[string]string{} opName := "foo-op" - data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true" - data[parser.GetAnnotationWithPrefix("opentelemetry-trust-incoming-span")] = "true" - data[parser.GetAnnotationWithPrefix("opentelemetry-operation-name")] = opName + data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true" + data[parser.GetAnnotationWithPrefix(otelTrustSpanAnnotation)] = "true" + data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName ing.SetAnnotations(data) - val, _ := NewParser(&resolver.Mock{}).Parse(ing) + val, err := NewParser(&resolver.Mock{}).Parse(ing) + if err != nil { + t.Fatal(err) + } openTelemetry, ok := val.(*Config) if !ok { t.Errorf("expected a Config type") @@ -155,6 +158,21 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) { } } +func TestIngressAnnotationOpentelemetryWithBadOpName(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + opName := "fooxpto_123$la;" + data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true" + data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName + ing.SetAnnotations(data) + + _, err := NewParser(&resolver.Mock{}).Parse(ing) + if err == nil { + t.Fatalf("This operation should return an error but no error was returned") + } +} + func TestIngressAnnotationOpentelemetryUnset(t *testing.T) { ing := buildIngress() diff --git a/internal/ingress/annotations/opentracing/main.go b/internal/ingress/annotations/opentracing/main.go index 17ba7eb9f2..4723a0e352 100644 --- a/internal/ingress/annotations/opentracing/main.go +++ b/internal/ingress/annotations/opentracing/main.go @@ -23,8 +23,33 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + enableOpentracingAnnotation = "enable-opentracing" + opentracingTrustSpanAnnotation = "opentracing-trust-incoming-span" +) + +var opentracingAnnotations = parser.Annotation{ + Group: "opentracing", + Annotations: parser.AnnotationFields{ + enableOpentracingAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines if Opentracing collector should be enable for this location. Opentracing should + already be configured by Ingress administrator`, + }, + opentracingTrustSpanAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`, + }, + }, +} + type opentracing struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config contains the configuration to be used in the Ingress @@ -58,19 +83,26 @@ func (bd1 *Config) Equal(bd2 *Config) bool { // NewParser creates a new serviceUpstream annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return opentracing{r} + return opentracing{ + r: r, + annotationConfig: opentracingAnnotations, + } } func (s opentracing) Parse(ing *networking.Ingress) (interface{}, error) { - enabled, err := parser.GetBoolAnnotation("enable-opentracing", ing) + enabled, err := parser.GetBoolAnnotation(enableOpentracingAnnotation, ing, s.annotationConfig.Annotations) if err != nil { return &Config{}, nil } - trustSpan, err := parser.GetBoolAnnotation("opentracing-trust-incoming-span", ing) + trustSpan, err := parser.GetBoolAnnotation(opentracingTrustSpanAnnotation, ing, s.annotationConfig.Annotations) if err != nil { return &Config{Set: true, Enabled: enabled}, nil } return &Config{Set: true, Enabled: enabled, TrustSet: true, TrustEnabled: trustSpan}, nil } + +func (s opentracing) GetDocumentation() parser.AnnotationFields { + return s.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/opentracing/main_test.go b/internal/ingress/annotations/opentracing/main_test.go index 7bd9d31ff1..b7b62ac9d9 100644 --- a/internal/ingress/annotations/opentracing/main_test.go +++ b/internal/ingress/annotations/opentracing/main_test.go @@ -73,7 +73,7 @@ func TestIngressAnnotationOpentracingSetTrue(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true" + data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true" ing.SetAnnotations(data) val, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -92,7 +92,7 @@ func TestIngressAnnotationOpentracingSetFalse(t *testing.T) { // Test with explicitly set to false data := map[string]string{} - data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "false" + data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "false" ing.SetAnnotations(data) val, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -110,8 +110,8 @@ func TestIngressAnnotationOpentracingTrustSetTrue(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true" - data[parser.GetAnnotationWithPrefix("opentracing-trust-incoming-span")] = "true" + data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true" + data[parser.GetAnnotationWithPrefix(opentracingTrustSpanAnnotation)] = "true" ing.SetAnnotations(data) val, _ := NewParser(&resolver.Mock{}).Parse(ing) diff --git a/internal/ingress/annotations/parser/main.go b/internal/ingress/annotations/parser/main.go index 107a278b0b..9fcc77f0b3 100644 --- a/internal/ingress/annotations/parser/main.go +++ b/internal/ingress/annotations/parser/main.go @@ -29,20 +29,75 @@ import ( ) // DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller -const DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io" +const ( + DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io" +) var ( // AnnotationsPrefix is the mutable attribute that the controller explicitly refers to AnnotationsPrefix = DefaultAnnotationsPrefix ) +// AnnotationGroup defines the group that this annotation may belong +// eg.: Security, Snippets, Rewrite, etc +type AnnotationGroup string + +// AnnotationScope defines which scope this annotation applies. May be to the whole +// ingress, per location, etc +type AnnotationScope string + +var ( + AnnotationScopeLocation AnnotationScope = "location" + AnnotationScopeIngress AnnotationScope = "ingress" +) + +// AnnotationRisk is a subset of risk that an annotation may represent. +// Based on the Risk, the admin will be able to allow or disallow users to set it +// on their ingress objects +type AnnotationRisk string + +type AnnotationFields map[string]AnnotationConfig + +// AnnotationConfig defines the configuration that a single annotation field +// has, with the Validator and the documentation of this field. +type AnnotationConfig struct { + // Validator defines a function to validate the annotation value + Validator AnnotationValidator + // Documentation defines a user facing documentation for this annotation. This + // field will be used to auto generate documentations + Documentation string + // Risk defines a risk of this annotation being exposed to the user. Annotations + // with bool fields, or to set timeout are usually low risk. Annotations that allows + // string input without a limited set of options may represent a high risk + Risk AnnotationRisk + + // Scope defines which scope this annotation applies, may be to location, to an Ingress object, etc + Scope AnnotationScope + + // AnnotationAliases defines other names this annotation may have. + AnnotationAliases []string +} + +// Annotation defines an annotation feature an Ingress may have. +// It should contain the internal resolver, and all the annotations +// with configs and Validators that should be used for each Annotation +type Annotation struct { + // Annotations contains all the annotations that belong to this feature + Annotations AnnotationFields + // Group defines which annotation group this feature belongs to + Group AnnotationGroup +} + // IngressAnnotation has a method to parse annotations located in Ingress type IngressAnnotation interface { Parse(ing *networking.Ingress) (interface{}, error) + GetDocumentation() AnnotationFields } type ingAnnotations map[string]string +// TODO: We already parse all of this on checkAnnotation and can just do a parse over the +// value func (a ingAnnotations) parseBool(name string) (bool, error) { val, ok := a[name] if ok { @@ -92,21 +147,9 @@ func (a ingAnnotations) parseFloat32(name string) (float32, error) { return 0, errors.ErrMissingAnnotations } -func checkAnnotation(name string, ing *networking.Ingress) error { - if ing == nil || len(ing.GetAnnotations()) == 0 { - return errors.ErrMissingAnnotations - } - if name == "" { - return errors.ErrInvalidAnnotationName - } - - return nil -} - // GetBoolAnnotation extracts a boolean from an Ingress annotation -func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) { - v := GetAnnotationWithPrefix(name) - err := checkAnnotation(v, ing) +func GetBoolAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (bool, error) { + v, err := checkAnnotation(name, ing, fields) if err != nil { return false, err } @@ -114,9 +157,8 @@ func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) { } // GetStringAnnotation extracts a string from an Ingress annotation -func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) { - v := GetAnnotationWithPrefix(name) - err := checkAnnotation(v, ing) +func GetStringAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) { + v, err := checkAnnotation(name, ing, fields) if err != nil { return "", err } @@ -125,9 +167,8 @@ func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) { } // GetIntAnnotation extracts an int from an Ingress annotation -func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) { - v := GetAnnotationWithPrefix(name) - err := checkAnnotation(v, ing) +func GetIntAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (int, error) { + v, err := checkAnnotation(name, ing, fields) if err != nil { return 0, err } @@ -135,9 +176,8 @@ func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) { } // GetFloatAnnotation extracts a float32 from an Ingress annotation -func GetFloatAnnotation(name string, ing *networking.Ingress) (float32, error) { - v := GetAnnotationWithPrefix(name) - err := checkAnnotation(v, ing) +func GetFloatAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (float32, error) { + v, err := checkAnnotation(name, ing, fields) if err != nil { return 0, err } diff --git a/internal/ingress/annotations/parser/main_test.go b/internal/ingress/annotations/parser/main_test.go index 318e024d32..beca49370c 100644 --- a/internal/ingress/annotations/parser/main_test.go +++ b/internal/ingress/annotations/parser/main_test.go @@ -38,7 +38,7 @@ func buildIngress() *networking.Ingress { func TestGetBoolAnnotation(t *testing.T) { ing := buildIngress() - _, err := GetBoolAnnotation("", nil) + _, err := GetBoolAnnotation("", nil, nil) if err == nil { t.Errorf("expected error but retuned nil") } @@ -59,8 +59,8 @@ func TestGetBoolAnnotation(t *testing.T) { for _, test := range tests { data[GetAnnotationWithPrefix(test.field)] = test.value - - u, err := GetBoolAnnotation(test.field, ing) + ing.SetAnnotations(data) + u, err := GetBoolAnnotation(test.field, ing, nil) if test.expErr { if err == nil { t.Errorf("%v: expected error but retuned nil", test.name) @@ -68,7 +68,7 @@ func TestGetBoolAnnotation(t *testing.T) { continue } if u != test.exp { - t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, u) + t.Errorf("%v: expected \"%v\" but \"%v\" was returned, %+v", test.name, test.exp, u, ing) } delete(data, test.field) @@ -78,7 +78,7 @@ func TestGetBoolAnnotation(t *testing.T) { func TestGetStringAnnotation(t *testing.T) { ing := buildIngress() - _, err := GetStringAnnotation("", nil) + _, err := GetStringAnnotation("", nil, nil) if err == nil { t.Errorf("expected error but none returned") } @@ -109,7 +109,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv for _, test := range tests { data[GetAnnotationWithPrefix(test.field)] = test.value - s, err := GetStringAnnotation(test.field, ing) + s, err := GetStringAnnotation(test.field, ing, nil) if test.expErr { if err == nil { t.Errorf("%v: expected error but none returned", test.name) @@ -133,7 +133,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv func TestGetFloatAnnotation(t *testing.T) { ing := buildIngress() - _, err := GetFloatAnnotation("", nil) + _, err := GetFloatAnnotation("", nil, nil) if err == nil { t.Errorf("expected error but retuned nil") } @@ -156,7 +156,7 @@ func TestGetFloatAnnotation(t *testing.T) { for _, test := range tests { data[GetAnnotationWithPrefix(test.field)] = test.value - s, err := GetFloatAnnotation(test.field, ing) + s, err := GetFloatAnnotation(test.field, ing, nil) if test.expErr { if err == nil { t.Errorf("%v: expected error but retuned nil", test.name) @@ -174,7 +174,7 @@ func TestGetFloatAnnotation(t *testing.T) { func TestGetIntAnnotation(t *testing.T) { ing := buildIngress() - _, err := GetIntAnnotation("", nil) + _, err := GetIntAnnotation("", nil, nil) if err == nil { t.Errorf("expected error but retuned nil") } @@ -196,7 +196,7 @@ func TestGetIntAnnotation(t *testing.T) { for _, test := range tests { data[GetAnnotationWithPrefix(test.field)] = test.value - s, err := GetIntAnnotation(test.field, ing) + s, err := GetIntAnnotation(test.field, ing, nil) if test.expErr { if err == nil { t.Errorf("%v: expected error but retuned nil", test.name) @@ -224,6 +224,7 @@ func TestStringToURL(t *testing.T) { }{ {"empty", "", "url scheme is empty", nil, true}, {"no scheme", "bar", "url scheme is empty", nil, true}, + {"invalid parse", "://lala.com", "://lala.com is not a valid URL: parse \"://lala.com\": missing protocol scheme", nil, true}, {"invalid host", "http://", "url host is empty", nil, true}, {"invalid host (multiple dots)", "http://foo..bar.com", "invalid url host", nil, true}, {"valid URL", validURL, "", validParsedURL, false}, diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go new file mode 100644 index 0000000000..e0d4951cbf --- /dev/null +++ b/internal/ingress/annotations/parser/validators.go @@ -0,0 +1,224 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parser + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" + + networking "k8s.io/api/networking/v1" + machineryvalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/ingress-nginx/internal/ingress/errors" + "k8s.io/ingress-nginx/internal/net" + "k8s.io/klog/v2" +) + +type AnnotationValidator func(string) error + +const ( + AnnotationRiskLow AnnotationRisk = "Low" + AnnotationRiskMedium AnnotationRisk = "Medium" + AnnotationRiskHigh AnnotationRisk = "High" + AnnotationRiskCritical AnnotationRisk = "Critical" +) + +var ( + alphaNumericChars = `\-\.\_\~a-zA-Z0-9\/:` + extendedAlphaNumeric = alphaNumericChars + ", " + regexEnabledChars = regexp.QuoteMeta(`^$[](){}*+?|&=\`) + urlEnabledChars = regexp.QuoteMeta(`:?&=`) +) + +// IsValidRegex checks if the tested string can be used as a regex, but without any weird character. +// It includes regex characters for paths that may contain regexes +var IsValidRegex = regexp.MustCompile("^[/" + alphaNumericChars + regexEnabledChars + "]*$") + +// SizeRegex validates sizes understood by NGINX, like 1000, 100k, 1000M +var SizeRegex = regexp.MustCompile("^(?i)[0-9]+[bkmg]?$") + +// URLRegex is used to validate a URL but with only a specific set of characters: +// It is alphanumericChar + ":", "?", "&" +// A valid URL would be proto://something.com:port/something?arg=param +var ( + // URLIsValidRegex is used on full URLs, containing query strings (:, ? and &) + URLIsValidRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "]*$") + // BasicChars is alphanumeric and ".", "-", "_", "~" and ":", usually used on simple host:port/path composition + BasicCharsRegex = regexp.MustCompile("^[/" + alphaNumericChars + "]*$") + // ExtendedChars is alphanumeric and ".", "-", "_", "~" and ":" plus "," and spaces, usually used on simple host:port/path composition + ExtendedChars = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$") + // CharsWithSpace is like basic chars, but includes the space character + CharsWithSpace = regexp.MustCompile("^[/" + alphaNumericChars + " ]*$") + // NGINXVariable allows entries with alphanumeric characters, -, _ and the special "$" + NGINXVariable = regexp.MustCompile(`^[A-Za-z0-9\-\_\$\{\}]*$`) + // RegexPathWithCapture allows entries that SHOULD start with "/" and may contain alphanumeric + capture + // character for regex based paths, like /something/$1/anything/$2 + RegexPathWithCapture = regexp.MustCompile(`^/[` + alphaNumericChars + `\/\$]*$`) + // HeadersVariable defines a regex that allows headers separated by comma + HeadersVariable = regexp.MustCompile(`^[A-Za-z0-9-_, ]*$`) + // URLWithNginxVariableRegex defines a url that can contain nginx variables. + // It is a risky operation + URLWithNginxVariableRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "$]*$") +) + +// ValidateArrayOfServerName validates if all fields on a Server name annotation are +// regexes. They can be *.something*, ~^www\d+\.example\.com$ but not fancy character +func ValidateArrayOfServerName(value string) error { + for _, fqdn := range strings.Split(value, ",") { + if err := ValidateServerName(fqdn); err != nil { + return err + } + } + return nil +} + +func ValidateServerName(value string) error { + value = strings.TrimSpace(value) + if !IsValidRegex.MatchString(value) { + return fmt.Errorf("value %s is invalid server name", value) + } + return nil +} + +// ValidateRegex receives a regex as an argument and uses it to validate +// the value of the field. +// Annotation can define if the spaces should be trimmed before validating the value +func ValidateRegex(regex regexp.Regexp, removeSpace bool) AnnotationValidator { + return func(s string) error { + if removeSpace { + s = strings.ReplaceAll(s, " ", "") + } + if !regex.MatchString(s) { + return fmt.Errorf("value %s is invalid", s) + } + return nil + } +} + +// ValidateOptions receives an array of valid options that can be the value of annotation. +// If no valid option is found, it will return an error +func ValidateOptions(options []string, caseSensitive bool, trimSpace bool) AnnotationValidator { + return func(s string) error { + if trimSpace { + s = strings.TrimSpace(s) + } + if !caseSensitive { + s = strings.ToLower(s) + } + for _, option := range options { + if s == option { + return nil + } + } + return fmt.Errorf("value does not match any valid option") + } +} + +// ValidateBool validates if the specified value is a bool +func ValidateBool(value string) error { + _, err := strconv.ParseBool(value) + return err +} + +// ValidateInt validates if the specified value is an integer +func ValidateInt(value string) error { + _, err := strconv.Atoi(value) + return err +} + +// ValidateInt validates if the specified value is an array of IPs and CIDRs +func ValidateCIDRs(value string) error { + _, err := net.ParseCIDRs(value) + return err +} + +// ValidateDuration validates if the specified value is a valid time +func ValidateDuration(value string) error { + _, err := time.ParseDuration(value) + return err +} + +// ValidateNull always return null values and should not be widely used. +// It is used on the "snippet" annotations, as it is up to the admin to allow its +// usage, knowing it can be critical! +func ValidateNull(value string) error { + return nil +} + +// ValidateServiceName validates if a provided service name is a valid string +func ValidateServiceName(value string) error { + errs := machineryvalidation.NameIsDNS1035Label(value, false) + if len(errs) != 0 { + return fmt.Errorf("annotation does not contain a valid service name: %+v", errs) + } + return nil +} + +// checkAnnotations will check each annotation for: +// 1 - Does it contain the internal validation and docs config? +// 2 - Does the ingress contains annotations? (validate null pointers) +// 3 - Does it contains a validator? Should it contain a validator (not containing is a bug!) +// 4 - Does the annotation contain aliases? So we should use if the alias is defined an the annotation not. +// 4 - Runs the validator on the value +// It will return the full annotation name if all is fine +func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) { + var validateFunc AnnotationValidator + if fields != nil { + config, ok := fields[name] + if !ok { + return "", fmt.Errorf("annotation does not contain a valid internal configuration, this is an Ingress Controller issue! Please raise an issue on github.com/kubernetes/ingress-nginx") + } + validateFunc = config.Validator + } + + if ing == nil || len(ing.GetAnnotations()) == 0 { + return "", errors.ErrMissingAnnotations + } + + annotationFullName := GetAnnotationWithPrefix(name) + if annotationFullName == "" { + return "", errors.ErrInvalidAnnotationName + } + + annotationValue := ing.GetAnnotations()[annotationFullName] + if fields != nil { + if validateFunc == nil { + return "", fmt.Errorf("annotation does not contain a validator. This is an ingress-controller bug. Please open an issue") + } + if annotationValue == "" { + for _, annotationAlias := range fields[name].AnnotationAliases { + tempAnnotationFullName := GetAnnotationWithPrefix(annotationAlias) + if aliasVal := ing.GetAnnotations()[tempAnnotationFullName]; aliasVal != "" { + annotationValue = aliasVal + annotationFullName = tempAnnotationFullName + break + } + } + } + // We don't run validation against empty values + if annotationValue != "" { + if err := validateFunc(annotationValue); err != nil { + klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue) + return "", errors.NewValidationError(annotationFullName) + } + } + } + + return annotationFullName, nil +} diff --git a/internal/ingress/annotations/parser/validators_test.go b/internal/ingress/annotations/parser/validators_test.go new file mode 100644 index 0000000000..d37f83b1ec --- /dev/null +++ b/internal/ingress/annotations/parser/validators_test.go @@ -0,0 +1,223 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parser + +import ( + "fmt" + "testing" + + networking "k8s.io/api/networking/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestValidateArrayOfServerName(t *testing.T) { + tests := []struct { + name string + value string + wantErr bool + }{ + { + name: "should accept common name", + value: "something.com,anything.com", + wantErr: false, + }, + { + name: "should accept wildcard name", + value: "*.something.com,otherthing.com", + wantErr: false, + }, + { + name: "should allow names with spaces between array and some regexes", + value: `~^www\d+\.example\.com$,something.com`, + wantErr: false, + }, + { + name: "should allow names with regexes", + value: `http://some.test.env.com:2121/$someparam=1&$someotherparam=2`, + wantErr: false, + }, + { + name: "should allow names with wildcard in middle common name", + value: "*.so*mething.com,bla.com", + wantErr: false, + }, + { + name: "should deny names with weird characters", + value: "something.com,lolo;xpto.com,nothing.com", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateArrayOfServerName(tt.value); (err != nil) != tt.wantErr { + t.Errorf("ValidateArrayOfServerName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_checkAnnotation(t *testing.T) { + type args struct { + name string + ing *networking.Ingress + fields AnnotationFields + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "null ingress should error", + want: "", + args: args{ + name: "some-random-annotation", + }, + wantErr: true, + }, + { + name: "not having a validator for a specific annotation is a bug", + want: "", + args: args{ + name: "some-new-invalid-annotation", + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto", + }, + }, + }, + fields: AnnotationFields{ + "otherannotation": AnnotationConfig{ + Validator: func(value string) error { return nil }, + }, + }, + }, + wantErr: true, + }, + { + name: "annotationconfig found and no validation func defined on annotation is a bug", + want: "", + args: args{ + name: "some-new-invalid-annotation", + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto", + }, + }, + }, + fields: AnnotationFields{ + "some-new-invalid-annotation": AnnotationConfig{}, + }, + }, + wantErr: true, + }, + { + name: "no annotation can turn into a null pointer and should fail", + want: "", + args: args{ + name: "some-new-invalid-annotation", + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{}, + }, + fields: AnnotationFields{ + "some-new-invalid-annotation": AnnotationConfig{}, + }, + }, + wantErr: true, + }, + { + name: "no AnnotationField config should bypass validations", + want: GetAnnotationWithPrefix("some-valid-annotation"), + args: args{ + name: "some-valid-annotation", + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + GetAnnotationWithPrefix("some-valid-annotation"): "xpto", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "annotation with invalid value should fail", + want: "", + args: args{ + name: "some-new-annotation", + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + GetAnnotationWithPrefix("some-new-annotation"): "xpto1", + }, + }, + }, + fields: AnnotationFields{ + "some-new-annotation": AnnotationConfig{ + Validator: func(value string) error { + if value != "xpto" { + return fmt.Errorf("this is an error") + } + return nil + }, + }, + }, + }, + wantErr: true, + }, + { + name: "annotation with valid value should pass", + want: GetAnnotationWithPrefix("some-other-annotation"), + args: args{ + name: "some-other-annotation", + ing: &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + GetAnnotationWithPrefix("some-other-annotation"): "xpto", + }, + }, + }, + fields: AnnotationFields{ + "some-other-annotation": AnnotationConfig{ + Validator: func(value string) error { + if value != "xpto" { + return fmt.Errorf("this is an error") + } + return nil + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := checkAnnotation(tt.args.name, tt.args.ing, tt.args.fields) + if (err != nil) != tt.wantErr { + t.Errorf("checkAnnotation() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("checkAnnotation() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/ingress/annotations/portinredirect/main.go b/internal/ingress/annotations/portinredirect/main.go index 25d6655587..a8f8dc2bf0 100644 --- a/internal/ingress/annotations/portinredirect/main.go +++ b/internal/ingress/annotations/portinredirect/main.go @@ -23,22 +23,46 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + portsInRedirectAnnotation = "use-port-in-redirects" +) + +var portsInRedirectAnnotations = parser.Annotation{ + Group: "redirect", + Annotations: parser.AnnotationFields{ + portsInRedirectAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Enables or disables specifying the port in absolute redirects issued by nginx.`, + }, + }, +} + type portInRedirect struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new port in redirect annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return portInRedirect{r} + return portInRedirect{ + r: r, + annotationConfig: portsInRedirectAnnotations, + } } // Parse parses the annotations contained in the ingress // rule used to indicate if the redirects must func (a portInRedirect) Parse(ing *networking.Ingress) (interface{}, error) { - up, err := parser.GetBoolAnnotation("use-port-in-redirects", ing) + up, err := parser.GetBoolAnnotation(portsInRedirectAnnotation, ing, a.annotationConfig.Annotations) if err != nil { return a.r.GetDefaultBackend().UsePortInRedirects, nil } return up, nil } + +func (a portInRedirect) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/portinredirect/main_test.go b/internal/ingress/annotations/portinredirect/main_test.go index 71afd4cdf0..f5806e01a9 100644 --- a/internal/ingress/annotations/portinredirect/main_test.go +++ b/internal/ingress/annotations/portinredirect/main_test.go @@ -17,7 +17,6 @@ limitations under the License. package portinredirect import ( - "fmt" "testing" api "k8s.io/api/core/v1" @@ -84,23 +83,24 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend { func TestPortInRedirect(t *testing.T) { tests := []struct { title string - usePort *bool + usePort string def bool exp bool }{ - {"false - default false", newFalse(), false, false}, - {"false - default true", newFalse(), true, false}, - {"no annotation - default false", nil, false, false}, - {"no annotation - default true", nil, true, true}, - {"true - default true", newTrue(), true, true}, + {"false - default false", "false", false, false}, + {"false - default true", "false", true, false}, + {"no annotation - default false", "", false, false}, + {"no annotation - default false", "not-a-bool", false, false}, + {"no annotation - default true", "", true, true}, + {"true - default true", "true", true, true}, } for _, test := range tests { ing := buildIngress() data := map[string]string{} - if test.usePort != nil { - data[parser.GetAnnotationWithPrefix("use-port-in-redirects")] = fmt.Sprintf("%v", *test.usePort) + if test.usePort != "" { + data[parser.GetAnnotationWithPrefix(portsInRedirectAnnotation)] = test.usePort } ing.SetAnnotations(data) @@ -118,13 +118,3 @@ func TestPortInRedirect(t *testing.T) { } } } - -func newTrue() *bool { - b := true - return &b -} - -func newFalse() *bool { - b := false - return &b -} diff --git a/internal/ingress/annotations/proxy/main.go b/internal/ingress/annotations/proxy/main.go index 3a89b88557..f24f352c7c 100644 --- a/internal/ingress/annotations/proxy/main.go +++ b/internal/ingress/annotations/proxy/main.go @@ -17,12 +17,150 @@ limitations under the License. package proxy import ( + "regexp" + networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + proxyConnectTimeoutAnnotation = "proxy-connect-timeout" + proxySendTimeoutAnnotation = "proxy-send-timeout" + proxyReadTimeoutAnnotation = "proxy-read-timeout" + proxyBuffersNumberAnnotation = "proxy-buffers-number" + proxyBufferSizeAnnotation = "proxy-buffer-size" + proxyCookiePathAnnotation = "proxy-cookie-path" + proxyCookieDomainAnnotation = "proxy-cookie-domain" + proxyBodySizeAnnotation = "proxy-body-size" + proxyNextUpstreamAnnotation = "proxy-next-upstream" + proxyNextUpstreamTimeoutAnnotation = "proxy-next-upstream-timeout" + proxyNextUpstreamTriesAnnotation = "proxy-next-upstream-tries" + proxyRequestBufferingAnnotation = "proxy-request-buffering" + proxyRedirectFromAnnotation = "proxy-redirect-from" + proxyRedirectToAnnotation = "proxy-redirect-to" + proxyBufferingAnnotation = "proxy-buffering" + proxyHTTPVersionAnnotation = "proxy-http-version" + proxyMaxTempFileSizeAnnotation = "proxy-max-temp-file-size" +) + +var ( + validUpstreamAnnotation = regexp.MustCompile(`^((error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_403|http_404|http_429|non_idempotent|off)\s?)+$`) +) + +var proxyAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + proxyConnectTimeoutAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation allows setting the timeout in seconds of the connect operation to the backend.`, + }, + proxySendTimeoutAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation allows setting the timeout in seconds of the send operation to the backend.`, + }, + proxyReadTimeoutAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation allows setting the timeout in seconds of the read operation to the backend.`, + }, + proxyBuffersNumberAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation sets the number of the buffers in proxy_buffers used for reading the first part of the response received from the proxied server. + By default proxy buffers number is set as 4`, + }, + proxyBufferSizeAnnotation: { + Validator: parser.ValidateRegex(*parser.SizeRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation sets the size of the buffer proxy_buffer_size used for reading the first part of the response received from the proxied server. + By default proxy buffer size is set as "4k".`, + }, + proxyCookiePathAnnotation: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation sets a text that should be changed in the path attribute of the "Set-Cookie" header fields of a proxied server response.`, + }, + proxyCookieDomainAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation ets a text that should be changed in the domain attribute of the "Set-Cookie" header fields of a proxied server response.`, + }, + proxyBodySizeAnnotation: { + Validator: parser.ValidateRegex(*parser.SizeRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation allows setting the maximum allowed size of a client request body.`, + }, + proxyNextUpstreamAnnotation: { + Validator: parser.ValidateRegex(*validUpstreamAnnotation, false), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines when the next upstream should be used. + This annotation reflect the directive https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream + and only the allowed values on upstream are allowed here.`, + }, + proxyNextUpstreamTimeoutAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation limits the time during which a request can be passed to the next server`, + }, + proxyNextUpstreamTriesAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation limits the number of possible tries for passing a request to the next server`, + }, + proxyRequestBufferingAnnotation: { + Validator: parser.ValidateOptions([]string{"on", "off"}, true, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables or disables buffering of a client request body.`, + }, + proxyRedirectFromAnnotation: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`, + }, + proxyRedirectToAnnotation: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`, + }, + proxyBufferingAnnotation: { + Validator: parser.ValidateOptions([]string{"on", "off"}, true, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables or disables buffering of responses from the proxied server. It can be "on" or "off"`, + }, + proxyHTTPVersionAnnotation: { + Validator: parser.ValidateOptions([]string{"1.0", "1.1"}, true, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotations sets the HTTP protocol version for proxying. Can be "1.0" or "1.1".`, + }, + proxyMaxTempFileSizeAnnotation: { + Validator: parser.ValidateRegex(*parser.SizeRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines the maximum size of a temporary file when buffering responses.`, + }, + }, +} + // Config returns the proxy timeout to use in the upstream server/s type Config struct { BodySize string `json:"bodySize"` @@ -109,12 +247,15 @@ func (l1 *Config) Equal(l2 *Config) bool { } type proxy struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new reverse proxy configuration annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return proxy{r} + return proxy{r: r, + annotationConfig: proxyAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress @@ -125,90 +266,94 @@ func (a proxy) Parse(ing *networking.Ingress) (interface{}, error) { var err error - config.ConnectTimeout, err = parser.GetIntAnnotation("proxy-connect-timeout", ing) + config.ConnectTimeout, err = parser.GetIntAnnotation(proxyConnectTimeoutAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.ConnectTimeout = defBackend.ProxyConnectTimeout } - config.SendTimeout, err = parser.GetIntAnnotation("proxy-send-timeout", ing) + config.SendTimeout, err = parser.GetIntAnnotation(proxySendTimeoutAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.SendTimeout = defBackend.ProxySendTimeout } - config.ReadTimeout, err = parser.GetIntAnnotation("proxy-read-timeout", ing) + config.ReadTimeout, err = parser.GetIntAnnotation(proxyReadTimeoutAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.ReadTimeout = defBackend.ProxyReadTimeout } - config.BuffersNumber, err = parser.GetIntAnnotation("proxy-buffers-number", ing) + config.BuffersNumber, err = parser.GetIntAnnotation(proxyBuffersNumberAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.BuffersNumber = defBackend.ProxyBuffersNumber } - config.BufferSize, err = parser.GetStringAnnotation("proxy-buffer-size", ing) + config.BufferSize, err = parser.GetStringAnnotation(proxyBufferSizeAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.BufferSize = defBackend.ProxyBufferSize } - config.CookiePath, err = parser.GetStringAnnotation("proxy-cookie-path", ing) + config.CookiePath, err = parser.GetStringAnnotation(proxyCookiePathAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.CookiePath = defBackend.ProxyCookiePath } - config.CookieDomain, err = parser.GetStringAnnotation("proxy-cookie-domain", ing) + config.CookieDomain, err = parser.GetStringAnnotation(proxyCookieDomainAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.CookieDomain = defBackend.ProxyCookieDomain } - config.BodySize, err = parser.GetStringAnnotation("proxy-body-size", ing) + config.BodySize, err = parser.GetStringAnnotation(proxyBodySizeAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.BodySize = defBackend.ProxyBodySize } - config.NextUpstream, err = parser.GetStringAnnotation("proxy-next-upstream", ing) + config.NextUpstream, err = parser.GetStringAnnotation(proxyNextUpstreamAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.NextUpstream = defBackend.ProxyNextUpstream } - config.NextUpstreamTimeout, err = parser.GetIntAnnotation("proxy-next-upstream-timeout", ing) + config.NextUpstreamTimeout, err = parser.GetIntAnnotation(proxyNextUpstreamTimeoutAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.NextUpstreamTimeout = defBackend.ProxyNextUpstreamTimeout } - config.NextUpstreamTries, err = parser.GetIntAnnotation("proxy-next-upstream-tries", ing) + config.NextUpstreamTries, err = parser.GetIntAnnotation(proxyNextUpstreamTriesAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.NextUpstreamTries = defBackend.ProxyNextUpstreamTries } - config.RequestBuffering, err = parser.GetStringAnnotation("proxy-request-buffering", ing) + config.RequestBuffering, err = parser.GetStringAnnotation(proxyRequestBufferingAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.RequestBuffering = defBackend.ProxyRequestBuffering } - config.ProxyRedirectFrom, err = parser.GetStringAnnotation("proxy-redirect-from", ing) + config.ProxyRedirectFrom, err = parser.GetStringAnnotation(proxyRedirectFromAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.ProxyRedirectFrom = defBackend.ProxyRedirectFrom } - config.ProxyRedirectTo, err = parser.GetStringAnnotation("proxy-redirect-to", ing) + config.ProxyRedirectTo, err = parser.GetStringAnnotation(proxyRedirectToAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.ProxyRedirectTo = defBackend.ProxyRedirectTo } - config.ProxyBuffering, err = parser.GetStringAnnotation("proxy-buffering", ing) + config.ProxyBuffering, err = parser.GetStringAnnotation(proxyBufferingAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.ProxyBuffering = defBackend.ProxyBuffering } - config.ProxyHTTPVersion, err = parser.GetStringAnnotation("proxy-http-version", ing) + config.ProxyHTTPVersion, err = parser.GetStringAnnotation(proxyHTTPVersionAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.ProxyHTTPVersion = defBackend.ProxyHTTPVersion } - config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation("proxy-max-temp-file-size", ing) + config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation(proxyMaxTempFileSizeAnnotation, ing, a.annotationConfig.Annotations) if err != nil { config.ProxyMaxTempFileSize = defBackend.ProxyMaxTempFileSize } return config, nil } + +func (a proxy) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/proxy/main_test.go b/internal/ingress/annotations/proxy/main_test.go index e377ccb195..fa185551b8 100644 --- a/internal/ingress/annotations/proxy/main_test.go +++ b/internal/ingress/annotations/proxy/main_test.go @@ -161,6 +161,74 @@ func TestProxy(t *testing.T) { } } +func TestProxyComplex(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("proxy-connect-timeout")] = "1" + data[parser.GetAnnotationWithPrefix("proxy-send-timeout")] = "2" + data[parser.GetAnnotationWithPrefix("proxy-read-timeout")] = "3" + data[parser.GetAnnotationWithPrefix("proxy-buffers-number")] = "8" + data[parser.GetAnnotationWithPrefix("proxy-buffer-size")] = "1k" + data[parser.GetAnnotationWithPrefix("proxy-body-size")] = "2k" + data[parser.GetAnnotationWithPrefix("proxy-next-upstream")] = "error http_502" + data[parser.GetAnnotationWithPrefix("proxy-next-upstream-timeout")] = "5" + data[parser.GetAnnotationWithPrefix("proxy-next-upstream-tries")] = "3" + data[parser.GetAnnotationWithPrefix("proxy-request-buffering")] = "off" + data[parser.GetAnnotationWithPrefix("proxy-buffering")] = "on" + data[parser.GetAnnotationWithPrefix("proxy-http-version")] = "1.0" + data[parser.GetAnnotationWithPrefix("proxy-max-temp-file-size")] = "128k" + ing.SetAnnotations(data) + + i, err := NewParser(mockBackend{}).Parse(ing) + if err != nil { + t.Fatalf("unexpected error parsing a valid") + } + p, ok := i.(*Config) + if !ok { + t.Fatalf("expected a Config type") + } + if p.ConnectTimeout != 1 { + t.Errorf("expected 1 as connect-timeout but returned %v", p.ConnectTimeout) + } + if p.SendTimeout != 2 { + t.Errorf("expected 2 as send-timeout but returned %v", p.SendTimeout) + } + if p.ReadTimeout != 3 { + t.Errorf("expected 3 as read-timeout but returned %v", p.ReadTimeout) + } + if p.BuffersNumber != 8 { + t.Errorf("expected 8 as proxy-buffers-number but returned %v", p.BuffersNumber) + } + if p.BufferSize != "1k" { + t.Errorf("expected 1k as buffer-size but returned %v", p.BufferSize) + } + if p.BodySize != "2k" { + t.Errorf("expected 2k as body-size but returned %v", p.BodySize) + } + if p.NextUpstream != "error http_502" { + t.Errorf("expected off as next-upstream but returned %v", p.NextUpstream) + } + if p.NextUpstreamTimeout != 5 { + t.Errorf("expected 5 as next-upstream-timeout but returned %v", p.NextUpstreamTimeout) + } + if p.NextUpstreamTries != 3 { + t.Errorf("expected 3 as next-upstream-tries but returned %v", p.NextUpstreamTries) + } + if p.RequestBuffering != "off" { + t.Errorf("expected off as request-buffering but returned %v", p.RequestBuffering) + } + if p.ProxyBuffering != "on" { + t.Errorf("expected on as proxy-buffering but returned %v", p.ProxyBuffering) + } + if p.ProxyHTTPVersion != "1.0" { + t.Errorf("expected 1.0 as proxy-http-version but returned %v", p.ProxyHTTPVersion) + } + if p.ProxyMaxTempFileSize != "128k" { + t.Errorf("expected 128k as proxy-max-temp-file-size but returned %v", p.ProxyMaxTempFileSize) + } +} + func TestProxyWithNoAnnotation(t *testing.T) { ing := buildIngress() diff --git a/internal/ingress/annotations/proxyssl/main.go b/internal/ingress/annotations/proxyssl/main.go index 22f49b3ebe..5b9a91bf3f 100644 --- a/internal/ingress/annotations/proxyssl/main.go +++ b/internal/ingress/annotations/proxyssl/main.go @@ -24,9 +24,11 @@ import ( networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" "k8s.io/ingress-nginx/internal/k8s" + "k8s.io/klog/v2" ) const ( @@ -39,9 +41,73 @@ const ( var ( proxySSLOnOffRegex = regexp.MustCompile(`^(on|off)$`) - proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3)$`) + proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3| )*$`) + proxySSLCiphersRegex = regexp.MustCompile(`^[A-Za-z0-9\+\:\_\-\!]*$`) ) +const ( + proxySSLSecretAnnotation = "proxy-ssl-secret" + proxySSLCiphersAnnotation = "proxy-ssl-ciphers" + proxySSLProtocolsAnnotation = "proxy-ssl-protocols" + proxySSLNameAnnotation = "proxy-ssl-name" + proxySSLVerifyAnnotation = "proxy-ssl-verify" + proxySSLVerifyDepthAnnotation = "proxy-ssl-verify-depth" + proxySSLServerNameAnnotation = "proxy-ssl-server-name" +) + +var proxySSLAnnotation = parser.Annotation{ + Group: "proxy", + Annotations: parser.AnnotationFields{ + proxySSLSecretAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation specifies a Secret with the certificate tls.crt, key tls.key in PEM format used for authentication to a proxied HTTPS server. + It should also contain trusted CA certificates ca.crt in PEM format used to verify the certificate of the proxied HTTPS server. + This annotation expects the Secret name in the form "namespace/secretName" + Just secrets on the same namespace of the ingress can be used.`, + }, + proxySSLCiphersAnnotation: { + Validator: parser.ValidateRegex(*proxySSLCiphersRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation Specifies the enabled ciphers for requests to a proxied HTTPS server. + The ciphers are specified in the format understood by the OpenSSL library.`, + }, + proxySSLProtocolsAnnotation: { + Validator: parser.ValidateRegex(*proxySSLProtocolRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables the specified protocols for requests to a proxied HTTPS server.`, + }, + proxySSLNameAnnotation: { + Validator: parser.ValidateServerName, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskHigh, + Documentation: `This annotation allows to set proxy_ssl_name. This allows overriding the server name used to verify the certificate of the proxied HTTPS server. + This value is also passed through SNI when a connection is established to the proxied HTTPS server.`, + }, + proxySSLVerifyAnnotation: { + Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables or disables verification of the proxied HTTPS server certificate. (default: off)`, + }, + proxySSLVerifyDepthAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation Sets the verification depth in the proxied HTTPS server certificates chain. (default: 1).`, + }, + proxySSLServerNameAnnotation: { + Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables passing of the server name through TLS Server Name Indication extension (SNI, RFC 6066) when establishing a connection with the proxied HTTPS server.`, + }, + }, +} + // Config contains the AuthSSLCert used for mutual authentication // and the configured VerifyDepth type Config struct { @@ -85,11 +151,14 @@ func (pssl1 *Config) Equal(pssl2 *Config) bool { // NewParser creates a new TLS authentication annotation parser func NewParser(resolver resolver.Resolver) parser.IngressAnnotation { - return proxySSL{resolver} + return proxySSL{ + r: resolver, + annotationConfig: proxySSLAnnotation} } type proxySSL struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } func sortProtocols(protocols string) string { @@ -120,16 +189,20 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) { var err error config := &Config{} - proxysslsecret, err := parser.GetStringAnnotation("proxy-ssl-secret", ing) + proxysslsecret, err := parser.GetStringAnnotation(proxySSLSecretAnnotation, ing, p.annotationConfig.Annotations) if err != nil { return &Config{}, err } - _, _, err = k8s.ParseNameNS(proxysslsecret) + ns, _, err := k8s.ParseNameNS(proxysslsecret) if err != nil { return &Config{}, ing_errors.NewLocationDenied(err.Error()) } + if ns != ing.Namespace { + return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported") + } + proxyCert, err := p.r.GetAuthCertificate(proxysslsecret) if err != nil { e := fmt.Errorf("error obtaining certificate: %w", err) @@ -137,37 +210,50 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) { } config.AuthSSLCert = *proxyCert - config.Ciphers, err = parser.GetStringAnnotation("proxy-ssl-ciphers", ing) + config.Ciphers, err = parser.GetStringAnnotation(proxySSLCiphersAnnotation, ing, p.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("invalid value passed to proxy-ssl-ciphers, defaulting to %s", defaultProxySSLCiphers) + } config.Ciphers = defaultProxySSLCiphers } - config.Protocols, err = parser.GetStringAnnotation("proxy-ssl-protocols", ing) + config.Protocols, err = parser.GetStringAnnotation(proxySSLProtocolsAnnotation, ing, p.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("invalid value passed to proxy-ssl-protocolos, defaulting to %s", defaultProxySSLProtocols) + } config.Protocols = defaultProxySSLProtocols } else { config.Protocols = sortProtocols(config.Protocols) } - config.ProxySSLName, err = parser.GetStringAnnotation("proxy-ssl-name", ing) + config.ProxySSLName, err = parser.GetStringAnnotation(proxySSLNameAnnotation, ing, p.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("invalid value passed to proxy-ssl-name, defaulting to empty") + } config.ProxySSLName = "" } - config.Verify, err = parser.GetStringAnnotation("proxy-ssl-verify", ing) + config.Verify, err = parser.GetStringAnnotation(proxySSLVerifyAnnotation, ing, p.annotationConfig.Annotations) if err != nil || !proxySSLOnOffRegex.MatchString(config.Verify) { config.Verify = defaultProxySSLVerify } - config.VerifyDepth, err = parser.GetIntAnnotation("proxy-ssl-verify-depth", ing) + config.VerifyDepth, err = parser.GetIntAnnotation(proxySSLVerifyDepthAnnotation, ing, p.annotationConfig.Annotations) if err != nil || config.VerifyDepth == 0 { config.VerifyDepth = defaultProxySSLVerifyDepth } - config.ProxySSLServerName, err = parser.GetStringAnnotation("proxy-ssl-server-name", ing) + config.ProxySSLServerName, err = parser.GetStringAnnotation(proxySSLServerNameAnnotation, ing, p.annotationConfig.Annotations) if err != nil || !proxySSLOnOffRegex.MatchString(config.ProxySSLServerName) { config.ProxySSLServerName = defaultProxySSLServerName } return config, nil } + +func (p proxySSL) GetDocumentation() parser.AnnotationFields { + return p.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/proxyssl/main_test.go b/internal/ingress/annotations/proxyssl/main_test.go index 29949796c8..edd65343eb 100644 --- a/internal/ingress/annotations/proxyssl/main_test.go +++ b/internal/ingress/annotations/proxyssl/main_test.go @@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("proxy-ssl-secret")] = "default/demo-secret" + data[parser.GetAnnotationWithPrefix(proxySSLSecretAnnotation)] = "default/demo-secret" data[parser.GetAnnotationWithPrefix("proxy-ssl-ciphers")] = "HIGH:-SHA" data[parser.GetAnnotationWithPrefix("proxy-ssl-name")] = "$host" data[parser.GetAnnotationWithPrefix("proxy-ssl-protocols")] = "TLSv1.3 SSLv2 TLSv1 TLSv1.2" diff --git a/internal/ingress/annotations/ratelimit/main.go b/internal/ingress/annotations/ratelimit/main.go index 84a5f10f0d..6283434078 100644 --- a/internal/ingress/annotations/ratelimit/main.go +++ b/internal/ingress/annotations/ratelimit/main.go @@ -24,6 +24,7 @@ import ( networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" "k8s.io/ingress-nginx/internal/net" "k8s.io/ingress-nginx/pkg/util/sets" @@ -58,7 +59,7 @@ type Config struct { ID string `json:"id"` - Whitelist []string `json:"whitelist"` + Allowlist []string `json:"allowlist"` } // Equal tests for equality between two RateLimit types @@ -90,11 +91,11 @@ func (rt1 *Config) Equal(rt2 *Config) bool { if rt1.Name != rt2.Name { return false } - if len(rt1.Whitelist) != len(rt2.Whitelist) { + if len(rt1.Allowlist) != len(rt2.Allowlist) { return false } - return sets.StringElementsMatch(rt1.Whitelist, rt2.Whitelist) + return sets.StringElementsMatch(rt1.Allowlist, rt2.Allowlist) } // Zone returns information about the NGINX rate limit (limit_req_zone) @@ -131,43 +132,121 @@ func (z1 *Zone) Equal(z2 *Zone) bool { return true } +const ( + limitRateAnnotation = "limit-rate" + limitRateAfterAnnotation = "limit-rate-after" + limitRateRPMAnnotation = "limit-rpm" + limitRateRPSAnnotation = "limit-rps" + limitRateConnectionsAnnotation = "limit-connections" + limitRateBurstMultiplierAnnotation = "limit-burst-multiplier" + limitWhitelistAnnotation = "limit-whitelist" // This annotation is an alias for limit-allowlist + limitAllowlistAnnotation = "limit-allowlist" +) + +var rateLimitAnnotations = parser.Annotation{ + Group: "rate-limit", + Annotations: parser.AnnotationFields{ + limitRateAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Limits the rate of response transmission to a client. The rate is specified in bytes per second. + The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit. + References: https://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate`, + }, + limitRateAfterAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Sets the initial amount after which the further transmission of a response to a client will be rate limited.`, + }, + limitRateRPMAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Requests per minute that will be allowed.`, + }, + limitRateRPSAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Requests per second that will be allowed.`, + }, + limitRateConnectionsAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Number of connections that will be allowed`, + }, + limitRateBurstMultiplierAnnotation: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `Burst multiplier for a limit-rate enabled location.`, + }, + limitAllowlistAnnotation: { + Validator: parser.ValidateCIDRs, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `List of CIDR/IP addresses that will not be rate-limited.`, + AnnotationAliases: []string{limitWhitelistAnnotation}, + }, + }, +} + type ratelimit struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new ratelimit annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return ratelimit{r} + return ratelimit{ + r: r, + annotationConfig: rateLimitAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress // rule used to rewrite the defined paths func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) { defBackend := a.r.GetDefaultBackend() - lr, err := parser.GetIntAnnotation("limit-rate", ing) + lr, err := parser.GetIntAnnotation(limitRateAnnotation, ing, a.annotationConfig.Annotations) if err != nil { lr = defBackend.LimitRate } - lra, err := parser.GetIntAnnotation("limit-rate-after", ing) + lra, err := parser.GetIntAnnotation(limitRateAfterAnnotation, ing, a.annotationConfig.Annotations) if err != nil { lra = defBackend.LimitRateAfter } - rpm, _ := parser.GetIntAnnotation("limit-rpm", ing) - rps, _ := parser.GetIntAnnotation("limit-rps", ing) - conn, _ := parser.GetIntAnnotation("limit-connections", ing) - burstMultiplier, err := parser.GetIntAnnotation("limit-burst-multiplier", ing) + rpm, err := parser.GetIntAnnotation(limitRateRPMAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return nil, err + } + rps, err := parser.GetIntAnnotation(limitRateRPSAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return nil, err + } + conn, err := parser.GetIntAnnotation(limitRateConnectionsAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { + return nil, err + } + burstMultiplier, err := parser.GetIntAnnotation(limitRateBurstMultiplierAnnotation, ing, a.annotationConfig.Annotations) if err != nil { burstMultiplier = defBurst } - val, _ := parser.GetStringAnnotation("limit-whitelist", ing) - - cidrs, err := net.ParseCIDRs(val) - if err != nil { + val, err := parser.GetStringAnnotation(limitAllowlistAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && errors.IsValidationError(err) { return nil, err } + cidrs, errCidr := net.ParseCIDRs(val) + if errCidr != nil { + return nil, errCidr + } + if rpm == 0 && rps == 0 && conn == 0 { return &Config{ Connections: Zone{}, @@ -203,7 +282,7 @@ func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) { LimitRateAfter: lra, Name: zoneName, ID: encode(zoneName), - Whitelist: cidrs, + Allowlist: cidrs, }, nil } @@ -211,3 +290,7 @@ func encode(s string) string { str := base64.URLEncoding.EncodeToString([]byte(s)) return strings.Replace(str, "=", "", -1) } + +func (a ratelimit) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/ratelimit/main_test.go b/internal/ingress/annotations/ratelimit/main_test.go index 9f101cc3b9..d3a2cc0e94 100644 --- a/internal/ingress/annotations/ratelimit/main_test.go +++ b/internal/ingress/annotations/ratelimit/main_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/parser" "k8s.io/ingress-nginx/internal/ingress/defaults" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) @@ -85,8 +86,8 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend { func TestWithoutAnnotations(t *testing.T) { ing := buildIngress() _, err := NewParser(mockBackend{}).Parse(ing) - if err != nil { - t.Error("unexpected error with ingress without annotations") + if err != nil && !errors.IsMissingAnnotations(err) { + t.Errorf("unexpected error with ingress without annotations: %s", err) } } @@ -94,22 +95,22 @@ func TestRateLimiting(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("limit-connections")] = "0" - data[parser.GetAnnotationWithPrefix("limit-rps")] = "0" - data[parser.GetAnnotationWithPrefix("limit-rpm")] = "0" + data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "0" + data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "0" + data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "0" ing.SetAnnotations(data) _, err := NewParser(mockBackend{}).Parse(ing) if err != nil { - t.Errorf("unexpected error with invalid limits (0)") + t.Errorf("unexpected error with invalid limits (0): %s", err) } data = map[string]string{} - data[parser.GetAnnotationWithPrefix("limit-connections")] = "5" - data[parser.GetAnnotationWithPrefix("limit-rps")] = "100" - data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10" - data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100" - data[parser.GetAnnotationWithPrefix("limit-rate")] = "10" + data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5" + data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100" + data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10" + data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100" + data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10" ing.SetAnnotations(data) @@ -147,12 +148,12 @@ func TestRateLimiting(t *testing.T) { } data = map[string]string{} - data[parser.GetAnnotationWithPrefix("limit-connections")] = "5" - data[parser.GetAnnotationWithPrefix("limit-rps")] = "100" - data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10" - data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100" - data[parser.GetAnnotationWithPrefix("limit-rate")] = "10" - data[parser.GetAnnotationWithPrefix("limit-burst-multiplier")] = "3" + data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5" + data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100" + data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10" + data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100" + data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10" + data[parser.GetAnnotationWithPrefix(limitRateBurstMultiplierAnnotation)] = "3" ing.SetAnnotations(data) @@ -189,3 +190,61 @@ func TestRateLimiting(t *testing.T) { t.Errorf("expected 10 in limit by limitrate but %v was returned", rateLimit.LimitRate) } } + +func TestAnnotationCIDR(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5" + data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.5, 192.168.50.32/24" + ing.SetAnnotations(data) + + i, err := NewParser(mockBackend{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + rateLimit, ok := i.(*Config) + if !ok { + t.Errorf("expected a RateLimit type") + } + if len(rateLimit.Allowlist) != 2 { + t.Errorf("expected 2 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist)) + } + + data = map[string]string{} + data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5" + data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1" + ing.SetAnnotations(data) + + i, err = NewParser(mockBackend{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + rateLimit, ok = i.(*Config) + if !ok { + t.Errorf("expected a RateLimit type") + } + if len(rateLimit.Allowlist) != 3 { + t.Errorf("expected 3 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist)) + } + + // Parent annotation surpasses any alias + data = map[string]string{} + data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5" + data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1" + data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.9" + ing.SetAnnotations(data) + + i, err = NewParser(mockBackend{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + rateLimit, ok = i.(*Config) + if !ok { + t.Errorf("expected a RateLimit type") + } + if len(rateLimit.Allowlist) != 1 { + t.Errorf("expected 1 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist)) + } +} diff --git a/internal/ingress/annotations/redirect/redirect.go b/internal/ingress/annotations/redirect/redirect.go index 11b08a4a29..a7081da2f7 100644 --- a/internal/ingress/annotations/redirect/redirect.go +++ b/internal/ingress/annotations/redirect/redirect.go @@ -37,13 +37,56 @@ type Config struct { FromToWWW bool `json:"fromToWWW"` } +const ( + fromToWWWRedirAnnotation = "from-to-www-redirect" + temporalRedirectAnnotation = "temporal-redirect" + permanentRedirectAnnotation = "permanent-redirect" + permanentRedirectAnnotationCode = "permanent-redirect-code" +) + +var redirectAnnotations = parser.Annotation{ + Group: "redirect", + Annotations: parser.AnnotationFields{ + fromToWWWRedirAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `In some scenarios is required to redirect from www.domain.com to domain.com or vice versa. To enable this feature use this annotation.`, + }, + temporalRedirectAnnotation: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated + Documentation: `This annotation allows you to return a temporal redirect (Return Code 302) instead of sending data to the upstream. + For example setting this annotation to https://www.google.com would redirect everything to Google with a Return Code of 302 (Moved Temporarily).`, + }, + permanentRedirectAnnotation: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated + Documentation: `This annotation allows to return a permanent redirect (Return Code 301) instead of sending data to the upstream. + For example setting this annotation https://www.google.com would redirect everything to Google with a code 301`, + }, + permanentRedirectAnnotationCode: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options + Documentation: `This annotation allows you to modify the status code used for permanent redirects.`, + }, + }, +} + type redirect struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new redirect annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return redirect{r} + return redirect{ + r: r, + annotationConfig: redirectAnnotations, + } } // Parse parses the annotations contained in the ingress @@ -51,9 +94,12 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation { // If the Ingress contains both annotations the execution order is // temporal and then permanent func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) { - r3w, _ := parser.GetBoolAnnotation("from-to-www-redirect", ing) + r3w, err := parser.GetBoolAnnotation(fromToWWWRedirAnnotation, ing, r.annotationConfig.Annotations) + if err != nil && !errors.IsMissingAnnotations(err) { + return nil, err + } - tr, err := parser.GetStringAnnotation("temporal-redirect", ing) + tr, err := parser.GetStringAnnotation(temporalRedirectAnnotation, ing, r.annotationConfig.Annotations) if err != nil && !errors.IsMissingAnnotations(err) { return nil, err } @@ -70,12 +116,12 @@ func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) { }, nil } - pr, err := parser.GetStringAnnotation("permanent-redirect", ing) + pr, err := parser.GetStringAnnotation(permanentRedirectAnnotation, ing, r.annotationConfig.Annotations) if err != nil && !errors.IsMissingAnnotations(err) { return nil, err } - prc, err := parser.GetIntAnnotation("permanent-redirect-code", ing) + prc, err := parser.GetIntAnnotation(permanentRedirectAnnotationCode, ing, r.annotationConfig.Annotations) if err != nil && !errors.IsMissingAnnotations(err) { return nil, err } @@ -127,3 +173,7 @@ func isValidURL(s string) error { return nil } + +func (a redirect) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/redirect/redirect_test.go b/internal/ingress/annotations/redirect/redirect_test.go index b5a87a5d38..5a61f364db 100644 --- a/internal/ingress/annotations/redirect/redirect_test.go +++ b/internal/ingress/annotations/redirect/redirect_test.go @@ -43,7 +43,7 @@ func TestPermanentRedirectWithDefaultCode(t *testing.T) { ing := new(networking.Ingress) data := make(map[string]string, 1) - data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL + data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL ing.SetAnnotations(data) i, err := rp.Parse(ing) @@ -81,8 +81,8 @@ func TestPermanentRedirectWithCustomCode(t *testing.T) { ing := new(networking.Ingress) data := make(map[string]string, 2) - data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL - data[parser.GetAnnotationWithPrefix("permanent-redirect-code")] = strconv.Itoa(tc.input) + data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL + data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotationCode)] = strconv.Itoa(tc.input) ing.SetAnnotations(data) i, err := rp.Parse(ing) @@ -112,8 +112,8 @@ func TestTemporalRedirect(t *testing.T) { ing := new(networking.Ingress) data := make(map[string]string, 1) - data[parser.GetAnnotationWithPrefix("from-to-www-redirect")] = "true" - data[parser.GetAnnotationWithPrefix("temporal-redirect")] = defRedirectURL + data[parser.GetAnnotationWithPrefix(fromToWWWRedirAnnotation)] = "true" + data[parser.GetAnnotationWithPrefix(temporalRedirectAnnotation)] = defRedirectURL ing.SetAnnotations(data) i, err := rp.Parse(ing) diff --git a/internal/ingress/annotations/rewrite/main.go b/internal/ingress/annotations/rewrite/main.go index f92d508dc3..b430a22183 100644 --- a/internal/ingress/annotations/rewrite/main.go +++ b/internal/ingress/annotations/rewrite/main.go @@ -27,6 +27,59 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + rewriteTargetAnnotation = "rewrite-target" + sslRedirectAnnotation = "ssl-redirect" + preserveTrailingSlashAnnotation = "preserve-trailing-slash" + forceSSLRedirectAnnotation = "force-ssl-redirect" + useRegexAnnotation = "use-regex" + appRootAnnotation = "app-root" +) + +var rewriteAnnotations = parser.Annotation{ + Group: "rewrite", + Annotations: parser.AnnotationFields{ + rewriteTargetAnnotation: { + Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation allows to specify the target URI where the traffic must be redirected. It can contain regular characters and captured + groups specified as '$1', '$2', etc.`, + }, + sslRedirectAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines if the location section is only accessible via SSL`, + }, + preserveTrailingSlashAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines if the trailing slash should be preserved in the URI with 'ssl-redirect'`, + }, + forceSSLRedirectAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation forces the redirection to HTTPS even if the Ingress is not TLS Enabled`, + }, + useRegexAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines if the paths defined on an Ingress use regular expressions. To use regex on path + the pathType should also be defined as 'ImplementationSpecific'.`, + }, + appRootAnnotation: { + Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines the Application Root that the Controller must redirect if it's in / context`, + }, + }, +} + // Config describes the per location redirect config type Config struct { // Target URI where the traffic must be redirected @@ -71,12 +124,16 @@ func (r1 *Config) Equal(r2 *Config) bool { } type rewrite struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new rewrite annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return rewrite{r} + return rewrite{ + r: r, + annotationConfig: rewriteAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress @@ -85,24 +142,45 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) { var err error config := &Config{} - config.Target, _ = parser.GetStringAnnotation("rewrite-target", ing) - config.SSLRedirect, err = parser.GetBoolAnnotation("ssl-redirect", ing) + config.Target, err = parser.GetStringAnnotation(rewriteTargetAnnotation, ing, a.annotationConfig.Annotations) + if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%sis invalid, defaulting to empty", rewriteTargetAnnotation) + } + config.Target = "" + } + config.SSLRedirect, err = parser.GetBoolAnnotation(sslRedirectAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%sis invalid, defaulting to '%s'", sslRedirectAnnotation, a.r.GetDefaultBackend().SSLRedirect) + } config.SSLRedirect = a.r.GetDefaultBackend().SSLRedirect } - config.PreserveTrailingSlash, err = parser.GetBoolAnnotation("preserve-trailing-slash", ing) + config.PreserveTrailingSlash, err = parser.GetBoolAnnotation(preserveTrailingSlashAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%sis invalid, defaulting to '%s'", preserveTrailingSlashAnnotation, a.r.GetDefaultBackend().PreserveTrailingSlash) + } config.PreserveTrailingSlash = a.r.GetDefaultBackend().PreserveTrailingSlash } - config.ForceSSLRedirect, err = parser.GetBoolAnnotation("force-ssl-redirect", ing) + config.ForceSSLRedirect, err = parser.GetBoolAnnotation(forceSSLRedirectAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%sis invalid, defaulting to '%s'", forceSSLRedirectAnnotation, a.r.GetDefaultBackend().ForceSSLRedirect) + } config.ForceSSLRedirect = a.r.GetDefaultBackend().ForceSSLRedirect } - config.UseRegex, _ = parser.GetBoolAnnotation("use-regex", ing) + config.UseRegex, err = parser.GetBoolAnnotation(useRegexAnnotation, ing, a.annotationConfig.Annotations) + if err != nil { + if errors.IsValidationError(err) { + klog.Warningf("%sis invalid, defaulting to 'false'", useRegexAnnotation) + } + config.UseRegex = false + } - config.AppRoot, err = parser.GetStringAnnotation("app-root", ing) + config.AppRoot, err = parser.GetStringAnnotation(appRootAnnotation, ing, a.annotationConfig.Annotations) if err != nil { if !errors.IsMissingAnnotations(err) && !errors.IsInvalidContent(err) { klog.Warningf("Annotation app-root contains an invalid value: %v", err) @@ -126,3 +204,7 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) { return config, nil } + +func (a rewrite) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/rewrite/main_test.go b/internal/ingress/annotations/rewrite/main_test.go index c2cb42c78f..6b97d2e014 100644 --- a/internal/ingress/annotations/rewrite/main_test.go +++ b/internal/ingress/annotations/rewrite/main_test.go @@ -129,6 +129,30 @@ func TestSSLRedirect(t *testing.T) { t.Errorf("Expected true but returned false") } + data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/$1/abc/$2" + ing.SetAnnotations(data) + + i, _ = NewParser(mockBackend{redirect: true}).Parse(ing) + redirect, ok = i.(*Config) + if !ok { + t.Errorf("expected a Redirect type") + } + if redirect.Target != "/xpto/$1/abc/$2" { + t.Errorf("Expected /xpto/$1/abc/$2 but returned %s", redirect.Target) + } + + data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/xas{445}" + ing.SetAnnotations(data) + + i, _ = NewParser(mockBackend{redirect: true}).Parse(ing) + redirect, ok = i.(*Config) + if !ok { + t.Errorf("expected a Redirect type") + } + if redirect.Target != "" { + t.Errorf("Expected empty rewrite target but returned %s", redirect.Target) + } + data[parser.GetAnnotationWithPrefix("ssl-redirect")] = "false" ing.SetAnnotations(data) diff --git a/internal/ingress/annotations/satisfy/main.go b/internal/ingress/annotations/satisfy/main.go index 0d4fd4ff6b..e9eefa7b08 100644 --- a/internal/ingress/annotations/satisfy/main.go +++ b/internal/ingress/annotations/satisfy/main.go @@ -23,18 +23,40 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + satisfyAnnotation = "satisfy" +) + +var satisfyAnnotations = parser.Annotation{ + Group: "authentication", + Annotations: parser.AnnotationFields{ + satisfyAnnotation: { + Validator: parser.ValidateOptions([]string{"any", "all"}, true, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `By default, a request would need to satisfy all authentication requirements in order to be allowed. + By using this annotation, requests that satisfy either any or all authentication requirements are allowed, based on the configuration value. + Valid options are "all" and "any"`, + }, + }, +} + type satisfy struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new SATISFY annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return satisfy{r} + return satisfy{ + r: r, + annotationConfig: satisfyAnnotations, + } } // Parse parses annotation contained in the ingress func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) { - satisfy, err := parser.GetStringAnnotation("satisfy", ing) + satisfy, err := parser.GetStringAnnotation(satisfyAnnotation, ing, s.annotationConfig.Annotations) if err != nil || (satisfy != "any" && satisfy != "all") { satisfy = "" @@ -42,3 +64,7 @@ func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) { return satisfy, nil } + +func (s satisfy) GetDocumentation() parser.AnnotationFields { + return s.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/satisfy/main_test.go b/internal/ingress/annotations/satisfy/main_test.go index b45205d9f0..c8d5782f92 100644 --- a/internal/ingress/annotations/satisfy/main_test.go +++ b/internal/ingress/annotations/satisfy/main_test.go @@ -83,7 +83,7 @@ func TestSatisfyParser(t *testing.T) { annotations := map[string]string{} for input, expected := range data { - annotations[parser.GetAnnotationWithPrefix("satisfy")] = input + annotations[parser.GetAnnotationWithPrefix(satisfyAnnotation)] = input ing.SetAnnotations(annotations) satisfyt, err := NewParser(&resolver.Mock{}).Parse(ing) diff --git a/internal/ingress/annotations/serversnippet/main.go b/internal/ingress/annotations/serversnippet/main.go index 70f0af8e5d..e44675f35a 100644 --- a/internal/ingress/annotations/serversnippet/main.go +++ b/internal/ingress/annotations/serversnippet/main.go @@ -23,18 +23,42 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + serverSnippetAnnotation = "server-snippet" +) + +var serverSnippetAnnotations = parser.Annotation{ + Group: "snippets", + Annotations: parser.AnnotationFields{ + serverSnippetAnnotation: { + Validator: parser.ValidateNull, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations + Documentation: `This annotation allows setting a custom NGINX configuration on a server block. This annotation does not contain any validation and it's usage is not recommended!`, + }, + }, +} + type serverSnippet struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new server snippet annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return serverSnippet{r} + return serverSnippet{ + r: r, + annotationConfig: serverSnippetAnnotations, + } } // Parse parses the annotations contained in the ingress rule // used to indicate if the location/s contains a fragment of // configuration to be included inside the paths of the rules func (a serverSnippet) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetStringAnnotation("server-snippet", ing) + return parser.GetStringAnnotation(serverSnippetAnnotation, ing, a.annotationConfig.Annotations) +} + +func (a serverSnippet) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/serversnippet/main_test.go b/internal/ingress/annotations/serversnippet/main_test.go index c9e0979ad2..601e11a42a 100644 --- a/internal/ingress/annotations/serversnippet/main_test.go +++ b/internal/ingress/annotations/serversnippet/main_test.go @@ -27,7 +27,7 @@ import ( ) func TestParse(t *testing.T) { - annotation := parser.GetAnnotationWithPrefix("server-snippet") + annotation := parser.GetAnnotationWithPrefix(serverSnippetAnnotation) ap := NewParser(&resolver.Mock{}) if ap == nil { diff --git a/internal/ingress/annotations/serviceupstream/main.go b/internal/ingress/annotations/serviceupstream/main.go index 4a4879682c..9e5d00b35d 100644 --- a/internal/ingress/annotations/serviceupstream/main.go +++ b/internal/ingress/annotations/serviceupstream/main.go @@ -24,19 +24,39 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + serviceUpstreamAnnotation = "service-upstream" +) + +var serviceUpstreamAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + serviceUpstreamAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, // Critical, this annotation is not validated at all and allows arbitrary configutations + Documentation: `This annotation makes NGINX use Service's Cluster IP and Port instead of Endpoints as the backend endpoints`, + }, + }, +} + type serviceUpstream struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new serviceUpstream annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return serviceUpstream{r} + return serviceUpstream{ + r: r, + annotationConfig: serviceUpstreamAnnotations, + } } func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) { defBackend := s.r.GetDefaultBackend() - val, err := parser.GetBoolAnnotation("service-upstream", ing) + val, err := parser.GetBoolAnnotation(serviceUpstreamAnnotation, ing, s.annotationConfig.Annotations) // A missing annotation is not a problem, just use the default if err == errors.ErrMissingAnnotations { return defBackend.ServiceUpstream, nil @@ -44,3 +64,7 @@ func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) { return val, nil } + +func (s serviceUpstream) GetDocumentation() parser.AnnotationFields { + return s.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/serviceupstream/main_test.go b/internal/ingress/annotations/serviceupstream/main_test.go index b773e97233..2751208ecb 100644 --- a/internal/ingress/annotations/serviceupstream/main_test.go +++ b/internal/ingress/annotations/serviceupstream/main_test.go @@ -74,7 +74,7 @@ func TestIngressAnnotationServiceUpstreamEnabled(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("service-upstream")] = "true" + data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "true" ing.SetAnnotations(data) val, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -93,7 +93,7 @@ func TestIngressAnnotationServiceUpstreamSetFalse(t *testing.T) { // Test with explicitly set to false data := map[string]string{} - data[parser.GetAnnotationWithPrefix("service-upstream")] = "false" + data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false" ing.SetAnnotations(data) val, _ := NewParser(&resolver.Mock{}).Parse(ing) @@ -155,7 +155,7 @@ func TestParseAnnotationsOverridesDefaultConfig(t *testing.T) { ing := buildIngress() data := map[string]string{} - data[parser.GetAnnotationWithPrefix("service-upstream")] = "false" + data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false" ing.SetAnnotations(data) val, _ := NewParser(mockBackend{}).Parse(ing) diff --git a/internal/ingress/annotations/sessionaffinity/main.go b/internal/ingress/annotations/sessionaffinity/main.go index 98a0d64f8b..fc2dccad9f 100644 --- a/internal/ingress/annotations/sessionaffinity/main.go +++ b/internal/ingress/annotations/sessionaffinity/main.go @@ -65,6 +65,90 @@ const ( annotationAffinityCookieChangeOnFailure = "session-cookie-change-on-failure" ) +var sessionAffinityAnnotations = parser.Annotation{ + Group: "affinity", + Annotations: parser.AnnotationFields{ + annotationAffinityType: { + Validator: parser.ValidateOptions([]string{"cookie"}, true, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation enables and sets the affinity type in all Upstreams of an Ingress. This way, a request will always be directed to the same upstream server. The only affinity type available for NGINX is cookie`, + }, + annotationAffinityMode: { + Validator: parser.ValidateOptions([]string{"balanced", "persistent"}, true, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines the stickiness of a session. + Setting this to balanced (default) will redistribute some sessions if a deployment gets scaled up, therefore rebalancing the load on the servers. + Setting this to persistent will not rebalance sessions to new servers, therefore providing maximum stickiness.`, + }, + annotationAffinityCanaryBehavior: { + Validator: parser.ValidateOptions([]string{"sticky", "legacy"}, true, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation defines the behavior of canaries when session affinity is enabled. + Setting this to sticky (default) will ensure that users that were served by canaries, will continue to be served by canaries. + Setting this to legacy will restore original canary behavior, when session affinity was ignored.`, + }, + annotationAffinityCookieName: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation allows to specify the name of the cookie that will be used to route the requests`, + }, + annotationAffinityCookieSecure: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation set the cookie as secure regardless the protocol of the incoming request`, + }, + annotationAffinityCookieExpires: { + Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation is a legacy version of "session-cookie-max-age" for compatibility with older browsers, generates an "Expires" cookie directive by adding the seconds to the current date`, + }, + annotationAffinityCookieMaxAge: { + Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, false), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation sets the time until the cookie expires`, + }, + annotationAffinityCookiePath: { + Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines the Path that will be set on the cookie (required if your Ingress paths use regular expressions)`, + }, + annotationAffinityCookieDomain: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation defines the Domain attribute of the sticky cookie.`, + }, + annotationAffinityCookieSameSite: { + Validator: parser.ValidateOptions([]string{"None", "Lax", "Strict"}, false, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation is used to apply a SameSite attribute to the sticky cookie. + Browser accepted values are None, Lax, and Strict`, + }, + annotationAffinityCookieConditionalSameSiteNone: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation is used to omit SameSite=None from browsers with SameSite attribute incompatibilities`, + }, + annotationAffinityCookieChangeOnFailure: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation, when set to false will send request to upstream pointed by sticky cookie even if previous attempt failed. + When set to true and previous attempt failed, sticky cookie will be changed to point to another upstream.`, + }, + }, +} + var ( affinityCookieExpiresRegex = regexp.MustCompile(`(^0|-?[1-9]\d*$)`) ) @@ -109,50 +193,50 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie { cookie := &Cookie{} - cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing) + cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieName, "default", defaultAffinityCookieName) cookie.Name = defaultAffinityCookieName } - cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing) + cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing, a.annotationConfig.Annotations) if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.Expires) { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieExpires) cookie.Expires = "" } - cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing) + cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing, a.annotationConfig.Annotations) if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.MaxAge) { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieMaxAge) cookie.MaxAge = "" } - cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing) + cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookiePath) } - cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing) + cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieDomain) } - cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing) + cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSameSite) } - cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing) + cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSecure) } - cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing) + cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieConditionalSameSiteNone) } - cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing) + cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieChangeOnFailure) } @@ -162,11 +246,15 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie { // NewParser creates a new Affinity annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return affinity{r} + return affinity{ + r: r, + annotationConfig: sessionAffinityAnnotations, + } } type affinity struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // ParseAnnotations parses the annotations contained in the ingress @@ -174,18 +262,18 @@ type affinity struct { func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) { cookie := &Cookie{} // Check the type of affinity that will be used - at, err := parser.GetStringAnnotation(annotationAffinityType, ing) + at, err := parser.GetStringAnnotation(annotationAffinityType, ing, a.annotationConfig.Annotations) if err != nil { at = "" } // Check the affinity mode that will be used - am, err := parser.GetStringAnnotation(annotationAffinityMode, ing) + am, err := parser.GetStringAnnotation(annotationAffinityMode, ing, a.annotationConfig.Annotations) if err != nil { am = "" } - cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing) + cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing, a.annotationConfig.Annotations) if err != nil { cb = "" } @@ -205,3 +293,7 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) { Cookie: *cookie, }, nil } + +func (a affinity) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/snippet/main.go b/internal/ingress/annotations/snippet/main.go index 93ec70cf97..0dd948969c 100644 --- a/internal/ingress/annotations/snippet/main.go +++ b/internal/ingress/annotations/snippet/main.go @@ -23,18 +23,42 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + configurationSnippetAnnotation = "configuration-snippet" +) + +var configurationSnippetAnnotations = parser.Annotation{ + Group: "snippets", + Annotations: parser.AnnotationFields{ + configurationSnippetAnnotation: { + Validator: parser.ValidateNull, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations + Documentation: `This annotation allows setting a custom NGINX configuration on a location block. This annotation does not contain any validation and it's usage is not recommended!`, + }, + }, +} + type snippet struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new CORS annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return snippet{r} + return snippet{ + r: r, + annotationConfig: configurationSnippetAnnotations, + } } // Parse parses the annotations contained in the ingress rule // used to indicate if the location/s contains a fragment of // configuration to be included inside the paths of the rules func (a snippet) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetStringAnnotation("configuration-snippet", ing) + return parser.GetStringAnnotation(configurationSnippetAnnotation, ing, a.annotationConfig.Annotations) +} + +func (a snippet) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/snippet/main_test.go b/internal/ingress/annotations/snippet/main_test.go index 0defc3c1f8..921afeea8e 100644 --- a/internal/ingress/annotations/snippet/main_test.go +++ b/internal/ingress/annotations/snippet/main_test.go @@ -27,7 +27,7 @@ import ( ) func TestParse(t *testing.T) { - annotation := parser.GetAnnotationWithPrefix("configuration-snippet") + annotation := parser.GetAnnotationWithPrefix(configurationSnippetAnnotation) ap := NewParser(&resolver.Mock{}) if ap == nil { diff --git a/internal/ingress/annotations/sslcipher/main.go b/internal/ingress/annotations/sslcipher/main.go index e4e5baad20..ee391f92cb 100644 --- a/internal/ingress/annotations/sslcipher/main.go +++ b/internal/ingress/annotations/sslcipher/main.go @@ -17,14 +17,47 @@ limitations under the License. package sslcipher import ( + "regexp" + networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + sslPreferServerCipherAnnotation = "ssl-prefer-server-ciphers" + sslCipherAnnotation = "ssl-ciphers" +) + +var ( + // Should cover something like "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP" + regexValidSSLCipher = regexp.MustCompile(`^[A-Za-z0-9!:+\-]*$`) +) + +var sslCipherAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + sslPreferServerCipherAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `The following annotation will set the ssl_prefer_server_ciphers directive at the server level. + This configuration specifies that server ciphers should be preferred over client ciphers when using the SSLv3 and TLS protocols.`, + }, + sslCipherAnnotation: { + Validator: parser.ValidateRegex(*regexValidSSLCipher, true), + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `Using this annotation will set the ssl_ciphers directive at the server level. This configuration is active for all the paths in the host.`, + }, + }, +} + type sslCipher struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config contains the ssl-ciphers & ssl-prefer-server-ciphers configuration @@ -35,7 +68,10 @@ type Config struct { // NewParser creates a new sslCipher annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return sslCipher{r} + return sslCipher{ + r: r, + annotationConfig: sslCipherAnnotations, + } } // Parse parses the annotations contained in the ingress rule @@ -45,7 +81,7 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) { var err error var sslPreferServerCiphers bool - sslPreferServerCiphers, err = parser.GetBoolAnnotation("ssl-prefer-server-ciphers", ing) + sslPreferServerCiphers, err = parser.GetBoolAnnotation(sslPreferServerCipherAnnotation, ing, sc.annotationConfig.Annotations) if err != nil { config.SSLPreferServerCiphers = "" } else { @@ -56,7 +92,14 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) { } } - config.SSLCiphers, _ = parser.GetStringAnnotation("ssl-ciphers", ing) + config.SSLCiphers, err = parser.GetStringAnnotation(sslCipherAnnotation, ing, sc.annotationConfig.Annotations) + if err != nil && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) { + return config, err + } return config, nil } + +func (sc sslCipher) GetDocumentation() parser.AnnotationFields { + return sc.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/sslcipher/main_test.go b/internal/ingress/annotations/sslcipher/main_test.go index 6eb9ec0c2c..ac6808e068 100644 --- a/internal/ingress/annotations/sslcipher/main_test.go +++ b/internal/ingress/annotations/sslcipher/main_test.go @@ -33,22 +33,24 @@ func TestParse(t *testing.T) { t.Fatalf("expected a parser.IngressAnnotation but returned nil") } - annotationSSLCiphers := parser.GetAnnotationWithPrefix("ssl-ciphers") - annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix("ssl-prefer-server-ciphers") + annotationSSLCiphers := parser.GetAnnotationWithPrefix(sslCipherAnnotation) + annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix(sslPreferServerCipherAnnotation) testCases := []struct { annotations map[string]string expected Config + expectErr bool }{ - {map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}}, + {map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}, false}, {map[string]string{annotationSSLCiphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"}, - Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}}, - {map[string]string{annotationSSLCiphers: ""}, Config{"", ""}}, - {map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}}, - {map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}}, - {map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}}, - {map[string]string{}, Config{"", ""}}, - {nil, Config{"", ""}}, + Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}, false}, + {map[string]string{annotationSSLCiphers: ""}, Config{"", ""}, false}, + {map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}, false}, + {map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}, false}, + {map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}, false}, + {map[string]string{annotationSSLCiphers: "ALL:SOMETHING:;locationXPTO"}, Config{"", ""}, true}, + {map[string]string{}, Config{"", ""}, false}, + {nil, Config{"", ""}, false}, } ing := &networking.Ingress{ @@ -61,7 +63,10 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) - result, _ := ap.Parse(ing) + result, err := ap.Parse(ing) + if (err != nil) != testCase.expectErr { + t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations) + } if !reflect.DeepEqual(result, &testCase.expected) { t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) } diff --git a/internal/ingress/annotations/sslpassthrough/main.go b/internal/ingress/annotations/sslpassthrough/main.go index d1def7172a..ebeb4c4050 100644 --- a/internal/ingress/annotations/sslpassthrough/main.go +++ b/internal/ingress/annotations/sslpassthrough/main.go @@ -24,13 +24,32 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + sslPassthroughAnnotation = "ssl-passthrough" +) + +var sslPassthroughAnnotations = parser.Annotation{ + Group: "", // TBD + Annotations: parser.AnnotationFields{ + sslPassthroughAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set + Documentation: `This annotation instructs the controller to send TLS connections directly to the backend instead of letting NGINX decrypt the communication.`, + }, + }, +} + type sslpt struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new SSL passthrough annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return sslpt{r} + return sslpt{r: r, + annotationConfig: sslPassthroughAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress @@ -40,5 +59,9 @@ func (a sslpt) Parse(ing *networking.Ingress) (interface{}, error) { return false, ing_errors.ErrMissingAnnotations } - return parser.GetBoolAnnotation("ssl-passthrough", ing) + return parser.GetBoolAnnotation(sslPassthroughAnnotation, ing, a.annotationConfig.Annotations) +} + +func (a sslpt) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/sslpassthrough/main_test.go b/internal/ingress/annotations/sslpassthrough/main_test.go index 5cf2f979ad..b712fda199 100644 --- a/internal/ingress/annotations/sslpassthrough/main_test.go +++ b/internal/ingress/annotations/sslpassthrough/main_test.go @@ -54,7 +54,7 @@ func TestParseAnnotations(t *testing.T) { } data := map[string]string{} - data[parser.GetAnnotationWithPrefix("ssl-passthrough")] = "true" + data[parser.GetAnnotationWithPrefix(sslPassthroughAnnotation)] = "true" ing.SetAnnotations(data) // test ingress using the annotation without a TLS section _, err = NewParser(&resolver.Mock{}).Parse(ing) diff --git a/internal/ingress/annotations/streamsnippet/main.go b/internal/ingress/annotations/streamsnippet/main.go index fb22f754c1..6803977496 100644 --- a/internal/ingress/annotations/streamsnippet/main.go +++ b/internal/ingress/annotations/streamsnippet/main.go @@ -23,18 +23,42 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + streamSnippetAnnotation = "stream-snippet" +) + +var streamSnippetAnnotations = parser.Annotation{ + Group: "snippets", + Annotations: parser.AnnotationFields{ + streamSnippetAnnotation: { + Validator: parser.ValidateNull, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations + Documentation: `This annotation allows setting a custom NGINX configuration on a stream block. This annotation does not contain any validation and it's usage is not recommended!`, + }, + }, +} + type streamSnippet struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new server snippet annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return streamSnippet{r} + return streamSnippet{ + r: r, + annotationConfig: streamSnippetAnnotations, + } } // Parse parses the annotations contained in the ingress rule // used to indicate if the location/s contains a fragment of // configuration to be included inside the paths of the rules func (a streamSnippet) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetStringAnnotation("stream-snippet", ing) + return parser.GetStringAnnotation("stream-snippet", ing, a.annotationConfig.Annotations) +} + +func (a streamSnippet) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/streamsnippet/main_test.go b/internal/ingress/annotations/streamsnippet/main_test.go index 0b8e3e3aad..997b7be707 100644 --- a/internal/ingress/annotations/streamsnippet/main_test.go +++ b/internal/ingress/annotations/streamsnippet/main_test.go @@ -27,7 +27,7 @@ import ( ) func TestParse(t *testing.T) { - annotation := parser.GetAnnotationWithPrefix("stream-snippet") + annotation := parser.GetAnnotationWithPrefix(streamSnippetAnnotation) ap := NewParser(&resolver.Mock{}) if ap == nil { diff --git a/internal/ingress/annotations/upstreamhashby/main.go b/internal/ingress/annotations/upstreamhashby/main.go index e6bbca6c33..f3ac382ac0 100644 --- a/internal/ingress/annotations/upstreamhashby/main.go +++ b/internal/ingress/annotations/upstreamhashby/main.go @@ -17,14 +17,54 @@ limitations under the License. package upstreamhashby import ( + "regexp" + networking "k8s.io/api/networking/v1" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + upstreamHashByAnnotation = "upstream-hash-by" + upstreamHashBySubsetAnnotation = "upstream-hash-by-subset" + upstreamHashBySubsetSize = "upstream-hash-by-subset-size" +) + +var ( + specialChars = regexp.QuoteMeta("_${}") + hashByRegex = regexp.MustCompilePOSIX(`^[A-Za-z0-9\-` + specialChars + `]*$`) +) + +var upstreamHashByAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + upstreamHashByAnnotation: { + Validator: parser.ValidateRegex(*hashByRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskHigh, // High, this annotation allows accessing NGINX variables + Documentation: `This annotation defines the nginx variable, text value or any combination thereof to use for consistent hashing. + For example: nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri" or nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri$host" or nginx.ingress.kubernetes.io/upstream-hash-by: "${request_uri}-text-value" to consistently hash upstream requests by the current request URI.`, + }, + upstreamHashBySubsetAnnotation: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation maps requests to subset of nodes instead of a single one.`, + }, + upstreamHashBySubsetSize: { + Validator: parser.ValidateInt, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation determines the size of each subset (default 3)`, + }, + }, +} + type upstreamhashby struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config contains the Consistent hash configuration to be used in the Ingress @@ -36,14 +76,26 @@ type Config struct { // NewParser creates a new UpstreamHashBy annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return upstreamhashby{r} + return upstreamhashby{ + r: r, + annotationConfig: upstreamHashByAnnotations, + } } // Parse parses the annotations contained in the ingress rule func (a upstreamhashby) Parse(ing *networking.Ingress) (interface{}, error) { - upstreamHashBy, _ := parser.GetStringAnnotation("upstream-hash-by", ing) - upstreamHashBySubset, _ := parser.GetBoolAnnotation("upstream-hash-by-subset", ing) - upstreamHashbySubsetSize, _ := parser.GetIntAnnotation("upstream-hash-by-subset-size", ing) + upstreamHashBy, err := parser.GetStringAnnotation(upstreamHashByAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && !errors.IsMissingAnnotations(err) { + return nil, err + } + upstreamHashBySubset, err := parser.GetBoolAnnotation(upstreamHashBySubsetAnnotation, ing, a.annotationConfig.Annotations) + if err != nil && !errors.IsMissingAnnotations(err) { + return nil, err + } + upstreamHashbySubsetSize, err := parser.GetIntAnnotation(upstreamHashBySubsetSize, ing, a.annotationConfig.Annotations) + if err != nil && !errors.IsMissingAnnotations(err) { + return nil, err + } if upstreamHashbySubsetSize == 0 { upstreamHashbySubsetSize = 3 @@ -51,3 +103,7 @@ func (a upstreamhashby) Parse(ing *networking.Ingress) (interface{}, error) { return &Config{upstreamHashBy, upstreamHashBySubset, upstreamHashbySubsetSize}, nil } + +func (a upstreamhashby) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/upstreamhashby/main_test.go b/internal/ingress/annotations/upstreamhashby/main_test.go index d2c2644cac..bdbd9c3500 100644 --- a/internal/ingress/annotations/upstreamhashby/main_test.go +++ b/internal/ingress/annotations/upstreamhashby/main_test.go @@ -27,7 +27,7 @@ import ( ) func TestParse(t *testing.T) { - annotation := parser.GetAnnotationWithPrefix("upstream-hash-by") + annotation := parser.GetAnnotationWithPrefix(upstreamHashByAnnotation) ap := NewParser(&resolver.Mock{}) if ap == nil { @@ -37,12 +37,15 @@ func TestParse(t *testing.T) { testCases := []struct { annotations map[string]string expected string + expectErr bool }{ - {map[string]string{annotation: "$request_uri"}, "$request_uri"}, - {map[string]string{annotation: "$request_uri$scheme"}, "$request_uri$scheme"}, - {map[string]string{annotation: "false"}, "false"}, - {map[string]string{}, ""}, - {nil, ""}, + {map[string]string{annotation: "$request_URI"}, "$request_URI", false}, + {map[string]string{annotation: "$request_uri$scheme"}, "$request_uri$scheme", false}, + {map[string]string{annotation: "xpto;[]"}, "", true}, + {map[string]string{annotation: "lalal${scheme_test}"}, "lalal${scheme_test}", false}, + {map[string]string{annotation: "false"}, "false", false}, + {map[string]string{}, "", false}, + {nil, "", false}, } ing := &networking.Ingress{ @@ -55,14 +58,19 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) - result, _ := ap.Parse(ing) - uc, ok := result.(*Config) - if !ok { - t.Fatalf("expected a Config type") + result, err := ap.Parse(ing) + if (err != nil) != testCase.expectErr { + t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations) } + if !testCase.expectErr { + uc, ok := result.(*Config) + if !ok { + t.Fatalf("expected a Config type") + } - if uc.UpstreamHashBy != testCase.expected { - t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) + if uc.UpstreamHashBy != testCase.expected { + t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) + } } } } diff --git a/internal/ingress/annotations/upstreamvhost/main.go b/internal/ingress/annotations/upstreamvhost/main.go index 2eed5607e0..71090716c1 100644 --- a/internal/ingress/annotations/upstreamvhost/main.go +++ b/internal/ingress/annotations/upstreamvhost/main.go @@ -23,18 +23,43 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + upstreamVhostAnnotation = "upstream-vhost" +) + +var upstreamVhostAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + upstreamVhostAnnotation: { + Validator: parser.ValidateServerName, + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set + Documentation: `This configuration setting allows you to control the value for host in the following statement: proxy_set_header Host $host, which forms part of the location block. + This is useful if you need to call the upstream server by something other than $host`, + }, + }, +} + type upstreamVhost struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new upstream VHost annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return upstreamVhost{r} + return upstreamVhost{ + r: r, + annotationConfig: upstreamVhostAnnotations, + } } // Parse parses the annotations contained in the ingress rule // used to indicate if the location/s contains a fragment of // configuration to be included inside the paths of the rules func (a upstreamVhost) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetStringAnnotation("upstream-vhost", ing) + return parser.GetStringAnnotation(upstreamVhostAnnotation, ing, a.annotationConfig.Annotations) +} + +func (a upstreamVhost) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/upstreamvhost/main_test.go b/internal/ingress/annotations/upstreamvhost/main_test.go index 130d745ee3..87324b1810 100644 --- a/internal/ingress/annotations/upstreamvhost/main_test.go +++ b/internal/ingress/annotations/upstreamvhost/main_test.go @@ -36,7 +36,7 @@ func TestParse(t *testing.T) { } data := map[string]string{} - data[parser.GetAnnotationWithPrefix("upstream-vhost")] = "ok.com" + data[parser.GetAnnotationWithPrefix(upstreamVhostAnnotation)] = "ok.com" ing.SetAnnotations(data) diff --git a/internal/ingress/annotations/xforwardedprefix/main.go b/internal/ingress/annotations/xforwardedprefix/main.go index 60eed8773b..b45fb5c176 100644 --- a/internal/ingress/annotations/xforwardedprefix/main.go +++ b/internal/ingress/annotations/xforwardedprefix/main.go @@ -23,17 +23,41 @@ import ( "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + xForwardedForPrefixAnnotation = "x-forwarded-prefix" +) + +var xForwardedForAnnotations = parser.Annotation{ + Group: "backend", + Annotations: parser.AnnotationFields{ + xForwardedForPrefixAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set + Documentation: `This annotation can be used to add the non-standard X-Forwarded-Prefix header to the upstream request with a string value`, + }, + }, +} + type xforwardedprefix struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // NewParser creates a new xforwardedprefix annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return xforwardedprefix{r} + return xforwardedprefix{ + r: r, + annotationConfig: xForwardedForAnnotations, + } } // Parse parses the annotations contained in the ingress rule // used to add an x-forwarded-prefix header to the request func (cbbs xforwardedprefix) Parse(ing *networking.Ingress) (interface{}, error) { - return parser.GetStringAnnotation("x-forwarded-prefix", ing) + return parser.GetStringAnnotation(xForwardedForPrefixAnnotation, ing, cbbs.annotationConfig.Annotations) +} + +func (cbbs xforwardedprefix) GetDocumentation() parser.AnnotationFields { + return cbbs.annotationConfig.Annotations } diff --git a/internal/ingress/annotations/xforwardedprefix/main_test.go b/internal/ingress/annotations/xforwardedprefix/main_test.go index a78c63d049..d873a44128 100644 --- a/internal/ingress/annotations/xforwardedprefix/main_test.go +++ b/internal/ingress/annotations/xforwardedprefix/main_test.go @@ -27,7 +27,7 @@ import ( ) func TestParse(t *testing.T) { - annotation := parser.GetAnnotationWithPrefix("x-forwarded-prefix") + annotation := parser.GetAnnotationWithPrefix(xForwardedForPrefixAnnotation) ap := NewParser(&resolver.Mock{}) if ap == nil { t.Fatalf("expected a parser.IngressAnnotation but returned nil") diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index 4a44171301..7dc1a5292f 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -389,14 +389,19 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error { toCheck.ObjectMeta.Name == ing.ObjectMeta.Name } ings := store.FilterIngresses(allIngresses, filter) + parsed, err := annotations.NewAnnotationExtractor(n.store).Extract(ing) + if err != nil { + n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name) + return err + } ings = append(ings, &ingress.Ingress{ Ingress: *ing, - ParsedAnnotations: annotations.NewAnnotationExtractor(n.store).Extract(ing), + ParsedAnnotations: parsed, }) startTest := time.Now().UnixNano() / 1000000 _, servers, pcfg := n.getConfiguration(ings) - err := checkOverlap(ing, allIngresses, servers) + err = checkOverlap(ing, allIngresses, servers) if err != nil { n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name) return err @@ -1509,7 +1514,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress) loc.Rewrite = anns.Rewrite loc.UpstreamVhost = anns.UpstreamVhost loc.Denylist = anns.Denylist - loc.Whitelist = anns.Whitelist + loc.Allowlist = anns.Allowlist loc.Denied = anns.Denied loc.XForwardedPrefix = anns.XForwardedPrefix loc.UsePortInRedirects = anns.UsePortInRedirects @@ -1808,9 +1813,9 @@ func checkOverlap(ing *networking.Ingress, ingresses []*ingress.Ingress, servers } // path overlap. Check if one of the ingresses has a canary annotation - isCanaryEnabled, annotationErr := parser.GetBoolAnnotation("canary", ing) + isCanaryEnabled, annotationErr := parser.GetBoolAnnotation("canary", ing, canary.CanaryAnnotations.Annotations) for _, existing := range existingIngresses { - isExistingCanaryEnabled, existingAnnotationErr := parser.GetBoolAnnotation("canary", existing) + isExistingCanaryEnabled, existingAnnotationErr := parser.GetBoolAnnotation("canary", existing, canary.CanaryAnnotations.Annotations) if isCanaryEnabled && isExistingCanaryEnabled { return fmt.Errorf(`host "%s" and path "%s" is already defined in ingress %s/%s`, rule.Host, path.Path, existing.Namespace, existing.Name) diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go index 44184f6b9d..ad31103e0c 100644 --- a/internal/ingress/controller/controller_test.go +++ b/internal/ingress/controller/controller_test.go @@ -44,7 +44,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations" "k8s.io/ingress-nginx/internal/ingress/annotations/canary" - "k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist" + "k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" "k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl" "k8s.io/ingress-nginx/internal/ingress/annotations/sessionaffinity" @@ -2418,7 +2418,7 @@ func TestGetBackendServers(t *testing.T) { }, }, ParsedAnnotations: &annotations.Ingress{ - Whitelist: ipwhitelist.SourceRange{CIDR: []string{"10.0.0.0/24"}}, + Allowlist: ipallowlist.SourceRange{CIDR: []string{"10.0.0.0/24"}}, ServerSnippet: "bla", ConfigurationSnippet: "blo", }, @@ -2439,7 +2439,7 @@ func TestGetBackendServers(t *testing.T) { t.Errorf("config snippet should be empty, got '%s'", s.Locations[0].ConfigurationSnippet) } - if len(s.Locations[0].Whitelist.CIDR) != 1 || s.Locations[0].Whitelist.CIDR[0] != "10.0.0.0/24" { + if len(s.Locations[0].Allowlist.CIDR) != 1 || s.Locations[0].Allowlist.CIDR[0] != "10.0.0.0/24" { t.Errorf("allow list was incorrectly dropped, len should be 1 and contain 10.0.0.0/24") } diff --git a/internal/ingress/controller/store/store.go b/internal/ingress/controller/store/store.go index 9b37007391..03503ecef5 100644 --- a/internal/ingress/controller/store/store.go +++ b/internal/ingress/controller/store/store.go @@ -882,9 +882,14 @@ func (s *k8sStore) syncIngress(ing *networkingv1.Ingress) { k8s.SetDefaultNGINXPathType(copyIng) - err := s.listers.IngressWithAnnotation.Update(&ingress.Ingress{ + parsed, err := s.annotations.Extract(ing) + if err != nil { + klog.Error(err) + return + } + err = s.listers.IngressWithAnnotation.Update(&ingress.Ingress{ Ingress: *copyIng, - ParsedAnnotations: s.annotations.Extract(ing), + ParsedAnnotations: parsed, }) if err != nil { klog.Error(err) @@ -920,6 +925,7 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) { "proxy-ssl-secret", "secure-verify-ca-secret", } + for _, ann := range secretAnnotations { secrKey, err := objectRefAnnotationNsKey(ann, ing) if err != nil && !errors.IsMissingAnnotations(err) { @@ -938,7 +944,8 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) { // objectRefAnnotationNsKey returns an object reference formatted as a // 'namespace/name' key from the given annotation name. func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, error) { - annValue, err := parser.GetStringAnnotation(ann, ing) + // We pass nil fields, as this is an internal process and we don't need to validate it. + annValue, err := parser.GetStringAnnotation(ann, ing, nil) if err != nil { return "", err } @@ -951,6 +958,9 @@ func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, er if secrNs == "" { return fmt.Sprintf("%v/%v", ing.Namespace, secrName), nil } + if secrNs != ing.Namespace { + return "", fmt.Errorf("cross namespace secret is not supported") + } return annValue, nil } diff --git a/internal/ingress/controller/store/store_test.go b/internal/ingress/controller/store/store_test.go index b91cadc6cc..774a45676b 100644 --- a/internal/ingress/controller/store/store_test.go +++ b/internal/ingress/controller/store/store_test.go @@ -1414,18 +1414,31 @@ func TestUpdateSecretIngressMap(t *testing.T) { t.Run("with annotation in namespace/name format", func(t *testing.T) { ing := ingTpl.DeepCopy() ing.ObjectMeta.SetAnnotations(map[string]string{ - parser.GetAnnotationWithPrefix("auth-secret"): "otherns/auth", + parser.GetAnnotationWithPrefix("auth-secret"): "testns/auth", }) if err := s.listers.Ingress.Update(ing); err != nil { t.Errorf("error updating the Ingress: %v", err) } s.updateSecretIngressMap(ing) - if l := s.secretIngressMap.Len(); !(l == 1 && s.secretIngressMap.Has("otherns/auth")) { + if l := s.secretIngressMap.Len(); !(l == 1 && s.secretIngressMap.Has("testns/auth")) { t.Errorf("Expected \"otherns/auth\" to be the only referenced Secret (got %d)", l) } }) + t.Run("with annotation in namespace/name format should not be supported", func(t *testing.T) { + ing := ingTpl.DeepCopy() + ing.ObjectMeta.SetAnnotations(map[string]string{ + parser.GetAnnotationWithPrefix("auth-secret"): "anotherns/auth", + }) + s.listers.Ingress.Update(ing) + s.updateSecretIngressMap(ing) + + if l := s.secretIngressMap.Len(); l != 0 { + t.Errorf("Expected \"otherns/auth\" to be denied as it contains a different namespace (got %d)", l) + } + }) + t.Run("with annotation in invalid format", func(t *testing.T) { ing := ingTpl.DeepCopy() ing.ObjectMeta.SetAnnotations(map[string]string{ diff --git a/internal/ingress/errors/errors.go b/internal/ingress/errors/errors.go index 93c9ee5e05..5809c96ace 100644 --- a/internal/ingress/errors/errors.go +++ b/internal/ingress/errors/errors.go @@ -110,3 +110,25 @@ func New(m string) error { func Errorf(format string, args ...interface{}) error { return fmt.Errorf(format, args...) } + +type ValidationError struct { + Reason error +} + +func (e ValidationError) Error() string { + return e.Reason.Error() +} + +// NewValidationError returns a new LocationDenied error +func NewValidationError(annotation string) error { + return ValidationError{ + Reason: fmt.Errorf("annotation %s contains invalid value", annotation), + } +} + +// IsValidationError checks if the err is an error which +// indicates that some annotation value is invalid +func IsValidationError(e error) bool { + _, ok := e.(ValidationError) + return ok +} diff --git a/pkg/apis/ingress/types.go b/pkg/apis/ingress/types.go index e50666c18c..284e9b427f 100644 --- a/pkg/apis/ingress/types.go +++ b/pkg/apis/ingress/types.go @@ -29,8 +29,8 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/cors" "k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi" "k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit" + "k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist" "k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist" - "k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist" "k8s.io/ingress-nginx/internal/ingress/annotations/log" "k8s.io/ingress-nginx/internal/ingress/annotations/mirror" "k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity" @@ -224,7 +224,7 @@ type Server struct { // is required. // The chain in the execution order of annotations should be: // - Denylist -// - Whitelist +// - Allowlist // - RateLimit // - BasicDigestAuth // - ExternalAuth @@ -298,10 +298,10 @@ type Location struct { // addresses or networks are allowed. // +optional Denylist ipdenylist.SourceRange `json:"denylist,omitempty"` - // Whitelist indicates only connections from certain client + // Allowlist indicates only connections from certain client // addresses or networks are allowed. // +optional - Whitelist ipwhitelist.SourceRange `json:"whitelist,omitempty"` + Allowlist ipallowlist.SourceRange `json:"allowlist,omitempty"` // Proxy contains information about timeouts and buffer sizes // to be used in connections against endpoints // +optional diff --git a/pkg/apis/ingress/types_equals.go b/pkg/apis/ingress/types_equals.go index 84b1a186a4..c87f5ba3ef 100644 --- a/pkg/apis/ingress/types_equals.go +++ b/pkg/apis/ingress/types_equals.go @@ -400,7 +400,7 @@ func (l1 *Location) Equal(l2 *Location) bool { if !(&l1.Denylist).Equal(&l2.Denylist) { return false } - if !(&l1.Whitelist).Equal(&l2.Whitelist) { + if !(&l1.Allowlist).Equal(&l2.Allowlist) { return false } if !(&l1.Proxy).Equal(&l2.Proxy) { diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 189c13d6c7..02b5309cd2 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -548,14 +548,14 @@ http { {{ range $rl := (filterRateLimits $servers ) }} # Ratelimit {{ $rl.Name }} - geo $remote_addr $whitelist_{{ $rl.ID }} { + geo $remote_addr $allowlist_{{ $rl.ID }} { default 0; - {{ range $ip := $rl.Whitelist }} + {{ range $ip := $rl.Allowlist }} {{ $ip }} 1;{{ end }} } # Ratelimit {{ $rl.Name }} - map $whitelist_{{ $rl.ID }} $limit_{{ $rl.ID }} { + map $allowlist_{{ $rl.ID }} $limit_{{ $rl.ID }} { 0 {{ $cfg.LimitConnZoneVariable }}; 1 ""; } @@ -1312,8 +1312,8 @@ stream { {{ range $ip := $location.Denylist.CIDR }} deny {{ $ip }};{{ end }} {{ end }} - {{ if gt (len $location.Whitelist.CIDR) 0 }} - {{ range $ip := $location.Whitelist.CIDR }} + {{ if gt (len $location.Allowlist.CIDR) 0 }} + {{ range $ip := $location.Allowlist.CIDR }} allow {{ $ip }};{{ end }} deny all; {{ end }} diff --git a/test/e2e-image/e2e.sh b/test/e2e-image/e2e.sh index f8ecd5337d..5287b4a7f4 100755 --- a/test/e2e-image/e2e.sh +++ b/test/e2e-image/e2e.sh @@ -24,7 +24,7 @@ E2E_CHECK_LEAKS=${E2E_CHECK_LEAKS:-""} reportFile="report-e2e-test-suite.xml" ginkgo_args=( - "--fail-fast" +# "--fail-fast" "--flake-attempts=2" "--junit-report=${reportFile}" "--nodes=${E2E_NODES}" diff --git a/test/e2e/annotations/ipwhitelist.go b/test/e2e/annotations/ipallowlist.go similarity index 81% rename from test/e2e/annotations/ipwhitelist.go rename to test/e2e/annotations/ipallowlist.go index 71f026c7fd..79c77b4d00 100644 --- a/test/e2e/annotations/ipwhitelist.go +++ b/test/e2e/annotations/ipallowlist.go @@ -24,19 +24,19 @@ import ( "k8s.io/ingress-nginx/test/e2e/framework" ) -var _ = framework.DescribeAnnotation("whitelist-source-range", func() { - f := framework.NewDefaultFramework("ipwhitelist") +var _ = framework.DescribeAnnotation("allowlist-source-range", func() { + f := framework.NewDefaultFramework("ipallowlist") ginkgo.BeforeEach(func() { f.NewEchoDeployment() }) - ginkgo.It("should set valid ip whitelist range", func() { - host := "ipwhitelist.foo.com" + ginkgo.It("should set valid ip allowlist range", func() { + host := "ipallowlist.foo.com" nameSpace := f.Namespace annotations := map[string]string{ - "nginx.ingress.kubernetes.io/whitelist-source-range": "18.0.0.0/8, 56.0.0.0/8", + "nginx.ingress.kubernetes.io/allowlist-source-range": "18.0.0.0/8, 56.0.0.0/8", } ing := framework.NewSingleIngress(host, "/", host, nameSpace, framework.EchoService, 80, annotations) From b435ab13cf4a5e485b29956697c994ad9529f845 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Fri, 16 Jun 2023 12:41:17 +0000 Subject: [PATCH 02/14] Add annotation validation for fcgi --- internal/ingress/annotations/fastcgi/main.go | 62 ++++++++- .../ingress/annotations/fastcgi/main_test.go | 124 +++++++++++++++++- .../ingress/annotations/parser/validators.go | 3 +- 3 files changed, 182 insertions(+), 7 deletions(-) diff --git a/internal/ingress/annotations/fastcgi/main.go b/internal/ingress/annotations/fastcgi/main.go index 84bac4109b..591ba65f08 100644 --- a/internal/ingress/annotations/fastcgi/main.go +++ b/internal/ingress/annotations/fastcgi/main.go @@ -19,17 +19,49 @@ package fastcgi import ( "fmt" "reflect" + "regexp" networking "k8s.io/api/networking/v1" "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) +const ( + fastCGIIndexAnnotation = "fastcgi-index" + fastCGIParamsAnnotation = "fastcgi-params-configmap" +) + +var ( + // fast-cgi valid parameters is just a single file name (like index.php) + regexValidIndexAnnotationAndKey = regexp.MustCompile(`^[A-Za-z0-9\.\-\_]+$`) +) + +var fastCGIAnnotations = parser.Annotation{ + Group: "fastcgi", + Annotations: parser.AnnotationFields{ + fastCGIIndexAnnotation: { + Validator: parser.ValidateRegex(*regexValidIndexAnnotationAndKey, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation can be used to specify an index file`, + }, + fastCGIParamsAnnotation: { + Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true), + Scope: parser.AnnotationScopeLocation, + Risk: parser.AnnotationRiskMedium, + Documentation: `This annotation can be used to specify a ConfigMap containing the fastcgi parameters as a key/value. + Only ConfigMaps on the same namespace of ingress can be used. They key and value from ConfigMap are validated for unauthorized characters.`, + }, + }, +} + type fastcgi struct { - r resolver.Resolver + r resolver.Resolver + annotationConfig parser.Annotation } // Config describes the per location fastcgi config @@ -57,7 +89,10 @@ func (l1 *Config) Equal(l2 *Config) bool { // NewParser creates a new fastcgiConfig protocol annotation parser func NewParser(r resolver.Resolver) parser.IngressAnnotation { - return fastcgi{r} + return fastcgi{ + r: r, + annotationConfig: fastCGIAnnotations, + } } // ParseAnnotations parses the annotations contained in the ingress @@ -70,14 +105,21 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) { return fcgiConfig, nil } - index, err := parser.GetStringAnnotation("fastcgi-index", ing) + index, err := parser.GetStringAnnotation(fastCGIIndexAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if ing_errors.IsValidationError(err) { + return fcgiConfig, err + } index = "" } + fcgiConfig.Index = index - cm, err := parser.GetStringAnnotation("fastcgi-params-configmap", ing) + cm, err := parser.GetStringAnnotation(fastCGIParamsAnnotation, ing, a.annotationConfig.Annotations) if err != nil { + if ing_errors.IsValidationError(err) { + return fcgiConfig, err + } return fcgiConfig, nil } @@ -100,7 +142,19 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) { } } + for k, v := range cmap.Data { + if !regexValidIndexAnnotationAndKey.MatchString(k) || !parser.NGINXVariable.MatchString(v) { + klog.ErrorS(fmt.Errorf("fcgi contains invalid key or value"), "fcgi annotation error", "configmap", cmap.Name, "namespace", cmap.Namespace, "key", k, "value", v) + return fcgiConfig, ing_errors.NewValidationError(fastCGIParamsAnnotation) + } + } + + fcgiConfig.Index = index fcgiConfig.Params = cmap.Data return fcgiConfig, nil } + +func (a fastcgi) GetDocumentation() parser.AnnotationFields { + return a.annotationConfig.Annotations +} diff --git a/internal/ingress/annotations/fastcgi/main_test.go b/internal/ingress/annotations/fastcgi/main_test.go index 35c5bbc122..3296ded655 100644 --- a/internal/ingress/annotations/fastcgi/main_test.go +++ b/internal/ingress/annotations/fastcgi/main_test.go @@ -18,6 +18,7 @@ package fastcgi import ( "fmt" + "reflect" "testing" api "k8s.io/api/core/v1" @@ -49,10 +50,16 @@ func buildIngress() *networking.Ingress { type mockConfigMap struct { resolver.Mock + extraConfigMap map[string]map[string]string } func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) { - if name != "default/demo-configmap" && name != "otherns/demo-configmap" { + if m.extraConfigMap == nil { + m.extraConfigMap = make(map[string]map[string]string) + } + cmdata, ok := m.extraConfigMap[name] + + if name != "default/demo-configmap" && name != "otherns/demo-configmap" && !ok { return nil, fmt.Errorf("there is no configmap with name %v", name) } @@ -61,12 +68,17 @@ func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) { return nil, fmt.Errorf("invalid configmap name") } + data := map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"} + if ok { + data = cmdata + } + return &api.ConfigMap{ ObjectMeta: meta_v1.ObjectMeta{ Namespace: cmns, Name: cmn, }, - Data: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"}, + Data: data, }, nil } @@ -283,3 +295,111 @@ func TestConfigEquality(t *testing.T) { t.Errorf("config4 should be equal to config") } } + +func Test_fastcgi_Parse(t *testing.T) { + + tests := []struct { + name string + index string + configmapname string + configmap map[string]string + want interface{} + wantErr bool + }{ + { + name: "valid configuration", + index: "indexxpto-92123.php", + configmapname: "default/fcgiconfig", + configmap: map[string]string{ + "REQUEST_METHOD": "$request_method", + "SCRIPT_FILENAME": "$document_root$fastcgi_script_name", + }, + want: Config{ + Index: "indexxpto-92123.php", + Params: map[string]string{ + "REQUEST_METHOD": "$request_method", + "SCRIPT_FILENAME": "$document_root$fastcgi_script_name", + }, + }, + }, + { + name: "invalid index name", + index: "indexxpto-92123$xx.php", + configmapname: "default/fcgiconfig", + configmap: map[string]string{ + "REQUEST_METHOD": "$request_method", + "SCRIPT_FILENAME": "$document_root$fastcgi_script_name", + }, + want: Config{}, + wantErr: true, + }, + { + name: "invalid configmap namespace", + index: "indexxpto-92123.php", + configmapname: "otherns/fcgiconfig", + configmap: map[string]string{ + "REQUEST_METHOD": "$request_method", + "SCRIPT_FILENAME": "$document_root$fastcgi_script_name", + }, + want: Config{Index: "indexxpto-92123.php"}, + wantErr: true, + }, + { + name: "invalid configmap namespace name", + index: "indexxpto-92123.php", + configmapname: "otherns/fcgicon;{fig", + configmap: map[string]string{ + "REQUEST_METHOD": "$request_method", + "SCRIPT_FILENAME": "$document_root$fastcgi_script_name", + }, + want: Config{Index: "indexxpto-92123.php"}, + wantErr: true, + }, + { + name: "invalid configmap values key", + index: "indexxpto-92123.php", + configmapname: "default/fcgiconfig", + configmap: map[string]string{ + "REQUEST_METHOD$XPTO": "$request_method", + }, + want: Config{Index: "indexxpto-92123.php"}, + wantErr: true, + }, + { + name: "invalid configmap values val", + index: "indexxpto-92123.php", + configmapname: "default/fcgiconfig", + configmap: map[string]string{ + "REQUEST_METHOD_XPTO": "$request_method{test};a", + }, + want: Config{Index: "indexxpto-92123.php"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("fastcgi-index")] = tt.index + data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = tt.configmapname + ing.SetAnnotations(data) + + m := &mockConfigMap{ + extraConfigMap: map[string]map[string]string{ + tt.configmapname: tt.configmap, + }, + } + + got, err := NewParser(m).Parse(ing) + if (err != nil) != tt.wantErr { + t.Errorf("fastcgi.Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("fastcgi.Parse() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go index e0d4951cbf..0baf6d9d35 100644 --- a/internal/ingress/annotations/parser/validators.go +++ b/internal/ingress/annotations/parser/validators.go @@ -59,7 +59,8 @@ var SizeRegex = regexp.MustCompile("^(?i)[0-9]+[bkmg]?$") var ( // URLIsValidRegex is used on full URLs, containing query strings (:, ? and &) URLIsValidRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "]*$") - // BasicChars is alphanumeric and ".", "-", "_", "~" and ":", usually used on simple host:port/path composition + // BasicChars is alphanumeric and ".", "-", "_", "~" and ":", usually used on simple host:port/path composition. + // This combination can also be used on fields that may contain characters like / (as ns/name) BasicCharsRegex = regexp.MustCompile("^[/" + alphaNumericChars + "]*$") // ExtendedChars is alphanumeric and ".", "-", "_", "~" and ":" plus "," and spaces, usually used on simple host:port/path composition ExtendedChars = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$") From cdbf699b9f57396ffd8ed5ec5f8d8b0f4b5efb3a Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Sun, 18 Jun 2023 13:56:48 +0000 Subject: [PATCH 03/14] Fix reviews and fcgi e2e --- internal/ingress/annotations/authreq/main.go | 19 +++++++++---------- .../ingress/annotations/authreqglobal/main.go | 2 +- .../ingress/annotations/parser/validators.go | 2 +- internal/ingress/annotations/proxyssl/main.go | 2 +- test/e2e/annotations/fastcgi.go | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/internal/ingress/annotations/authreq/main.go b/internal/ingress/annotations/authreq/main.go index c9e4a10086..cbbe55e94a 100644 --- a/internal/ingress/annotations/authreq/main.go +++ b/internal/ingress/annotations/authreq/main.go @@ -27,7 +27,6 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" - "k8s.io/ingress-nginx/internal/ingress/errors" ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" "k8s.io/ingress-nginx/pkg/util/sets" @@ -111,7 +110,7 @@ var authReqAnnotations = parser.Annotation{ Documentation: `This annotation specifies a duration in seconds which an idle keepalive connection to an upstream server will stay open`, }, authReqCacheDuration: { - Validator: parser.ValidateRegex(*parser.ExtendedChars, false), + Validator: parser.ValidateRegex(*parser.ExtendedCharsRegex, false), Scope: parser.AnnotationScopeLocation, Risk: parser.AnnotationRiskMedium, Documentation: `This annotation allows to specify a caching time for auth responses based on their response codes, e.g. 200 202 30m`, @@ -306,7 +305,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { authMethod, err := parser.GetStringAnnotation(authReqMethodAnnotation, ing, a.annotationConfig.Annotations) if err != nil { - if errors.IsValidationError(err) { + if ing_errors.IsValidationError(err) { return nil, ing_errors.NewLocationDenied("invalid HTTP method") } } @@ -314,7 +313,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { // Optional Parameters signIn, err := parser.GetStringAnnotation(authReqSigninAnnotation, ing, a.annotationConfig.Annotations) if err != nil { - if errors.IsValidationError(err) { + if ing_errors.IsValidationError(err) { klog.Warningf("%s value is invalid: %s", authReqSigninAnnotation, err) } klog.V(3).InfoS("auth-signin annotation is undefined and will not be set") @@ -322,7 +321,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { signInRedirectParam, err := parser.GetStringAnnotation(authReqSigninRedirParamAnnotation, ing, a.annotationConfig.Annotations) if err != nil { - if errors.IsValidationError(err) { + if ing_errors.IsValidationError(err) { klog.Warningf("%s value is invalid: %s", authReqSigninRedirParamAnnotation, err) } klog.V(3).Infof("auth-signin-redirect-param annotation is undefined and will not be set") @@ -335,7 +334,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { authCacheKey, err := parser.GetStringAnnotation(authReqCacheKeyAnnotation, ing, a.annotationConfig.Annotations) if err != nil { - if errors.IsValidationError(err) { + if ing_errors.IsValidationError(err) { klog.Warningf("%s value is invalid: %s", authReqCacheKeyAnnotation, err) } klog.V(3).InfoS("auth-cache-key annotation is undefined and will not be set") @@ -379,7 +378,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { } durstr, err := parser.GetStringAnnotation(authReqCacheDuration, ing, a.annotationConfig.Annotations) - if err != nil && errors.IsValidationError(err) { + if err != nil && ing_errors.IsValidationError(err) { return nil, fmt.Errorf("%s contains invalid value", authReqCacheDuration) } authCacheDuration, err := ParseStringToCacheDurations(durstr) @@ -389,7 +388,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { responseHeaders := []string{} hstr, err := parser.GetStringAnnotation(authReqResponseHeadersAnnotation, ing, a.annotationConfig.Annotations) - if err != nil && errors.IsValidationError(err) { + if err != nil && ing_errors.IsValidationError(err) { return nil, ing_errors.NewLocationDenied("validation error") } if len(hstr) != 0 { @@ -445,12 +444,12 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { } requestRedirect, err := parser.GetStringAnnotation(authReqRequestRedirectAnnotation, ing, a.annotationConfig.Annotations) - if err != nil && errors.IsValidationError(err) { + if err != nil && ing_errors.IsValidationError(err) { return nil, fmt.Errorf("%s is invalid: %w", authReqRequestRedirectAnnotation, err) } alwaysSetCookie, err := parser.GetBoolAnnotation(authReqAlwaysSetCookieAnnotation, ing, a.annotationConfig.Annotations) - if err != nil && errors.IsValidationError(err) { + if err != nil && ing_errors.IsValidationError(err) { return nil, fmt.Errorf("%s is invalid: %w", authReqAlwaysSetCookieAnnotation, err) } diff --git a/internal/ingress/annotations/authreqglobal/main.go b/internal/ingress/annotations/authreqglobal/main.go index 429588e5a5..03e0b88b3d 100644 --- a/internal/ingress/annotations/authreqglobal/main.go +++ b/internal/ingress/annotations/authreqglobal/main.go @@ -34,7 +34,7 @@ var globalAuthAnnotations = parser.Annotation{ Validator: parser.ValidateBool, Scope: parser.AnnotationScopeLocation, Risk: parser.AnnotationRiskLow, - Documentation: `Defines if the gloabl external authentication should be enabled.`, + Documentation: `Defines if the global external authentication should be enabled.`, }, }, } diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go index 0baf6d9d35..36cf1f8ef7 100644 --- a/internal/ingress/annotations/parser/validators.go +++ b/internal/ingress/annotations/parser/validators.go @@ -63,7 +63,7 @@ var ( // This combination can also be used on fields that may contain characters like / (as ns/name) BasicCharsRegex = regexp.MustCompile("^[/" + alphaNumericChars + "]*$") // ExtendedChars is alphanumeric and ".", "-", "_", "~" and ":" plus "," and spaces, usually used on simple host:port/path composition - ExtendedChars = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$") + ExtendedCharsRegex = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$") // CharsWithSpace is like basic chars, but includes the space character CharsWithSpace = regexp.MustCompile("^[/" + alphaNumericChars + " ]*$") // NGINXVariable allows entries with alphanumeric characters, -, _ and the special "$" diff --git a/internal/ingress/annotations/proxyssl/main.go b/internal/ingress/annotations/proxyssl/main.go index 5b9a91bf3f..66a70da0a1 100644 --- a/internal/ingress/annotations/proxyssl/main.go +++ b/internal/ingress/annotations/proxyssl/main.go @@ -221,7 +221,7 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) { config.Protocols, err = parser.GetStringAnnotation(proxySSLProtocolsAnnotation, ing, p.annotationConfig.Annotations) if err != nil { if errors.IsValidationError(err) { - klog.Warningf("invalid value passed to proxy-ssl-protocolos, defaulting to %s", defaultProxySSLProtocols) + klog.Warningf("invalid value passed to proxy-ssl-protocols, defaulting to %s", defaultProxySSLProtocols) } config.Protocols = defaultProxySSLProtocols } else { diff --git a/test/e2e/annotations/fastcgi.go b/test/e2e/annotations/fastcgi.go index 572eca5481..7ed35cb769 100644 --- a/test/e2e/annotations/fastcgi.go +++ b/test/e2e/annotations/fastcgi.go @@ -75,7 +75,7 @@ var _ = framework.DescribeAnnotation("backend-protocol - FastCGI", func() { Namespace: f.Namespace, }, Data: map[string]string{ - "SCRIPT_FILENAME": "/home/www/scripts/php$fastcgi_script_name", + "SCRIPT_FILENAME": "$fastcgi_script_name", "REDIRECT_STATUS": "200", }, } @@ -94,7 +94,7 @@ var _ = framework.DescribeAnnotation("backend-protocol - FastCGI", func() { f.WaitForNginxServer(host, func(server string) bool { - return strings.Contains(server, "fastcgi_param SCRIPT_FILENAME \"/home/www/scripts/php$fastcgi_script_name\";") && + return strings.Contains(server, "fastcgi_param SCRIPT_FILENAME \"$fastcgi_script_name\";") && strings.Contains(server, "fastcgi_param REDIRECT_STATUS \"200\";") }) }) From 10b7baafe8d90bcb20e203358bb3f40213cadfdd Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Sun, 18 Jun 2023 21:40:25 +0000 Subject: [PATCH 04/14] Add flag to disable cross namespace validation --- internal/ingress/annotations/auth/main.go | 3 +- .../ingress/annotations/auth/main_test.go | 29 ++++- internal/ingress/annotations/authreq/main.go | 4 +- internal/ingress/annotations/authtls/main.go | 4 +- internal/ingress/annotations/fastcgi/main.go | 4 +- internal/ingress/annotations/parser/main.go | 2 +- .../ingress/annotations/parser/validators.go | 8 +- internal/ingress/annotations/proxyssl/main.go | 4 +- internal/ingress/controller/config/config.go | 13 +++ .../ingress/controller/controller_test.go | 7 ++ internal/ingress/controller/store/store.go | 21 +++- internal/ingress/defaults/main.go | 12 ++ internal/ingress/resolver/main.go | 3 + internal/ingress/resolver/mock.go | 15 ++- test/e2e/e2e.go | 2 + test/e2e/settings/validations/validations.go | 110 ++++++++++++++++++ 16 files changed, 225 insertions(+), 16 deletions(-) create mode 100644 test/e2e/settings/validations/validations.go diff --git a/internal/ingress/annotations/auth/main.go b/internal/ingress/annotations/auth/main.go index ccdcfd7300..e7c0ddaa62 100644 --- a/internal/ingress/annotations/auth/main.go +++ b/internal/ingress/annotations/auth/main.go @@ -182,8 +182,9 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) { if sns == "" { sns = ing.Namespace } + secCfg := a.r.GetSecurityConfiguration() // We don't accept different namespaces for secrets. - if sns != ing.Namespace { + if !secCfg.AllowCrossNamespaceResources && sns != ing.Namespace { return nil, ing_errors.LocationDenied{ Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"), } diff --git a/internal/ingress/annotations/auth/main_test.go b/internal/ingress/annotations/auth/main_test.go index 412ddb7119..2a9dc7c721 100644 --- a/internal/ingress/annotations/auth/main_test.go +++ b/internal/ingress/annotations/auth/main_test.go @@ -26,6 +26,7 @@ import ( api "k8s.io/api/core/v1" networking "k8s.io/api/networking/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" @@ -79,13 +80,18 @@ type mockSecret struct { } func (m mockSecret) GetSecret(name string) (*api.Secret, error) { - if name != "default/demo-secret" { + if name != "default/demo-secret" && name != "otherns/demo-secret" { return nil, fmt.Errorf("there is no secret with name %v", name) } + ns, _, err := cache.SplitMetaNamespaceKey(name) + if err != nil { + return nil, err + } + return &api.Secret{ ObjectMeta: meta_v1.ObjectMeta{ - Namespace: api.NamespaceDefault, + Namespace: ns, Name: "demo-secret", }, Data: map[string][]byte{"auth": []byte("foo:$apr1$OFG3Xybp$ckL0FHDAkoXYIlH9.cysT0")}, @@ -158,6 +164,25 @@ func TestIngressInvalidDifferentNamespace(t *testing.T) { } } +func TestIngressInvalidDifferentNamespaceAllowed(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic" + data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "otherns/demo-secret" + ing.SetAnnotations(data) + + _, dir, _ := dummySecretContent(t) + defer os.RemoveAll(dir) + + r := mockSecret{} + r.AllowCrossNamespace = true + _, err := NewParser(dir, r).Parse(ing) + if err != nil { + t.Errorf("not expecting an error") + } +} + func TestIngressInvalidSecretName(t *testing.T) { ing := buildIngress() diff --git a/internal/ingress/annotations/authreq/main.go b/internal/ingress/annotations/authreq/main.go index cbbe55e94a..2b4330b6f2 100644 --- a/internal/ingress/annotations/authreq/main.go +++ b/internal/ingress/annotations/authreq/main.go @@ -419,8 +419,10 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) { if cns == "" { cns = ing.Namespace } + + secCfg := a.r.GetSecurityConfiguration() // We don't accept different namespaces for secrets. - if cns != ing.Namespace { + if !secCfg.AllowCrossNamespaceResources && cns != ing.Namespace { return nil, ing_errors.LocationDenied{ Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"), } diff --git a/internal/ingress/annotations/authtls/main.go b/internal/ingress/annotations/authtls/main.go index 25c292e29c..cc383004e2 100644 --- a/internal/ingress/annotations/authtls/main.go +++ b/internal/ingress/annotations/authtls/main.go @@ -160,7 +160,9 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) { if ns == "" { ns = ing.Namespace } - if ns != ing.Namespace { + secCfg := a.r.GetSecurityConfiguration() + // We don't accept different namespaces for secrets. + if !secCfg.AllowCrossNamespaceResources && ns != ing.Namespace { return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported") } diff --git a/internal/ingress/annotations/fastcgi/main.go b/internal/ingress/annotations/fastcgi/main.go index 591ba65f08..7fa61e3dbb 100644 --- a/internal/ingress/annotations/fastcgi/main.go +++ b/internal/ingress/annotations/fastcgi/main.go @@ -129,8 +129,10 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) { Reason: fmt.Errorf("error reading configmap name from annotation: %w", err), } } + secCfg := a.r.GetSecurityConfiguration() - if cmns != "" && cmns != ing.Namespace { + // We don't accept different namespaces for secrets. + if cmns != "" && !secCfg.AllowCrossNamespaceResources && cmns != ing.Namespace { return fcgiConfig, fmt.Errorf("different namespace is not supported on fast_cgi param configmap") } diff --git a/internal/ingress/annotations/parser/main.go b/internal/ingress/annotations/parser/main.go index 9fcc77f0b3..af19b64200 100644 --- a/internal/ingress/annotations/parser/main.go +++ b/internal/ingress/annotations/parser/main.go @@ -54,7 +54,7 @@ var ( // AnnotationRisk is a subset of risk that an annotation may represent. // Based on the Risk, the admin will be able to allow or disallow users to set it // on their ingress objects -type AnnotationRisk string +type AnnotationRisk int type AnnotationFields map[string]AnnotationConfig diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go index 36cf1f8ef7..45ce6911ea 100644 --- a/internal/ingress/annotations/parser/validators.go +++ b/internal/ingress/annotations/parser/validators.go @@ -33,10 +33,10 @@ import ( type AnnotationValidator func(string) error const ( - AnnotationRiskLow AnnotationRisk = "Low" - AnnotationRiskMedium AnnotationRisk = "Medium" - AnnotationRiskHigh AnnotationRisk = "High" - AnnotationRiskCritical AnnotationRisk = "Critical" + AnnotationRiskLow AnnotationRisk = iota + AnnotationRiskMedium + AnnotationRiskHigh + AnnotationRiskCritical ) var ( diff --git a/internal/ingress/annotations/proxyssl/main.go b/internal/ingress/annotations/proxyssl/main.go index 66a70da0a1..411dc66e03 100644 --- a/internal/ingress/annotations/proxyssl/main.go +++ b/internal/ingress/annotations/proxyssl/main.go @@ -199,7 +199,9 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) { return &Config{}, ing_errors.NewLocationDenied(err.Error()) } - if ns != ing.Namespace { + secCfg := p.r.GetSecurityConfiguration() + // We don't accept different namespaces for secrets. + if !secCfg.AllowCrossNamespaceResources && ns != ing.Namespace { return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported") } diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go index 345ce89b46..f3ea6d5a65 100644 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -97,6 +97,17 @@ type Configuration struct { // If disabled, only snippets added via ConfigMap are added to ingress. AllowSnippetAnnotations bool `json:"allow-snippet-annotations"` + // AllowCrossNamespaceResources enables users to consume cross namespace resource on annotations + // Case disabled, attempts to use secrets or configmaps from a namespace different from Ingress will + // be denied + // This valid will default to `false` on future releases + AllowCrossNamespaceResources bool `json:"allow-cross-namespace-resources"` + + // AnnotationsRisk represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations + // with risk High and Critical will not be accepted. + // Default Risk is Critical by default, but this may be changed in future releases + AnnotationsRisk string `json:"annotations-risk"` + // AnnotationValueWordBlocklist defines words that should not be part of an user annotation value // (can be used to run arbitrary code or configs, for example) and that should be dropped. // This list should be separated by "," character @@ -853,8 +864,10 @@ func NewDefault() Configuration { cfg := Configuration{ AllowSnippetAnnotations: true, + AllowCrossNamespaceResources: true, AllowBackendServerHeader: false, AnnotationValueWordBlocklist: "", + AnnotationsRisk: "Critical", AccessLogPath: "/var/log/nginx/access.log", AccessLogParams: "", EnableAccessLogForDefaultBackend: false, diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go index ad31103e0c..2e3b44406a 100644 --- a/internal/ingress/controller/controller_test.go +++ b/internal/ingress/controller/controller_test.go @@ -73,6 +73,13 @@ func (fis fakeIngressStore) GetBackendConfiguration() ngx_config.Configuration { return fis.configuration } +func (fis fakeIngressStore) GetSecurityConfiguration() defaults.SecurityConfiguration { + return defaults.SecurityConfiguration{ + AnnotationsRisk: fis.configuration.AnnotationsRisk, + AllowCrossNamespaceResources: fis.configuration.AllowCrossNamespaceResources, + } +} + func (fakeIngressStore) GetConfigMap(key string) (*corev1.ConfigMap, error) { return nil, fmt.Errorf("test error") } diff --git a/internal/ingress/controller/store/store.go b/internal/ingress/controller/store/store.go index 03503ecef5..6f37414c27 100644 --- a/internal/ingress/controller/store/store.go +++ b/internal/ingress/controller/store/store.go @@ -69,6 +69,9 @@ type Storer interface { // GetBackendConfiguration returns the nginx configuration stored in a configmap GetBackendConfiguration() ngx_config.Configuration + // GetSecurityConfiguration returns the configuration options from Ingress + GetSecurityConfiguration() defaults.SecurityConfiguration + // GetConfigMap returns the ConfigMap matching key. GetConfigMap(key string) (*corev1.ConfigMap, error) @@ -926,8 +929,9 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) { "secure-verify-ca-secret", } + secConfig := s.GetSecurityConfiguration().AllowCrossNamespaceResources for _, ann := range secretAnnotations { - secrKey, err := objectRefAnnotationNsKey(ann, ing) + secrKey, err := objectRefAnnotationNsKey(ann, ing, secConfig) if err != nil && !errors.IsMissingAnnotations(err) { klog.Errorf("error reading secret reference in annotation %q: %s", ann, err) continue @@ -943,7 +947,7 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) { // objectRefAnnotationNsKey returns an object reference formatted as a // 'namespace/name' key from the given annotation name. -func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, error) { +func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress, allowCrossNamespace bool) (string, error) { // We pass nil fields, as this is an internal process and we don't need to validate it. annValue, err := parser.GetStringAnnotation(ann, ing, nil) if err != nil { @@ -958,7 +962,7 @@ func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, er if secrNs == "" { return fmt.Sprintf("%v/%v", ing.Namespace, secrName), nil } - if secrNs != ing.Namespace { + if !allowCrossNamespace && secrNs != ing.Namespace { return "", fmt.Errorf("cross namespace secret is not supported") } return annValue, nil @@ -1135,6 +1139,17 @@ func (s *k8sStore) GetBackendConfiguration() ngx_config.Configuration { return s.backendConfig } +func (s *k8sStore) GetSecurityConfiguration() defaults.SecurityConfiguration { + s.backendConfigMu.RLock() + defer s.backendConfigMu.RUnlock() + + secConfig := defaults.SecurityConfiguration{ + AllowCrossNamespaceResources: s.backendConfig.AllowCrossNamespaceResources, + AnnotationsRisk: s.backendConfig.AnnotationsRisk, + } + return secConfig +} + func (s *k8sStore) setConfig(cmap *corev1.ConfigMap) { s.backendConfigMu.Lock() defer s.backendConfigMu.Unlock() diff --git a/internal/ingress/defaults/main.go b/internal/ingress/defaults/main.go index 0aab2ff478..078d50e63b 100644 --- a/internal/ingress/defaults/main.go +++ b/internal/ingress/defaults/main.go @@ -170,3 +170,15 @@ type Backend struct { // It disables that behavior and instead uses a single upstream in NGINX, the service's Cluster IP and port. ServiceUpstream bool `json:"service-upstream"` } + +type SecurityConfiguration struct { + // AllowCrossNamespaceResources enables users to consume cross namespace resource on annotations + // Case disabled, attempts to use secrets or configmaps from a namespace different from Ingress will + // be denied + // This valid will default to `false` on future releases + AllowCrossNamespaceResources bool `json:"allow-cross-namespace-resources"` + + // AnnotationsRisk represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations + // with risk High and Critical will not be accepted + AnnotationsRisk string `json:"annotations-risk"` +} diff --git a/internal/ingress/resolver/main.go b/internal/ingress/resolver/main.go index e05a2aaaeb..7d17f4e16f 100644 --- a/internal/ingress/resolver/main.go +++ b/internal/ingress/resolver/main.go @@ -26,6 +26,9 @@ type Resolver interface { // GetDefaultBackend returns the backend that must be used as default GetDefaultBackend() defaults.Backend + // GetSecurityConfiguration returns the configuration options from Ingress + GetSecurityConfiguration() defaults.SecurityConfiguration + // GetConfigMap searches for configmap containing the namespace and name usting the character / GetConfigMap(string) (*apiv1.ConfigMap, error) diff --git a/internal/ingress/resolver/mock.go b/internal/ingress/resolver/mock.go index 62c5c6db9b..2a1c265e7d 100644 --- a/internal/ingress/resolver/mock.go +++ b/internal/ingress/resolver/mock.go @@ -26,7 +26,9 @@ import ( // Mock implements the Resolver interface type Mock struct { - ConfigMaps map[string]*apiv1.ConfigMap + ConfigMaps map[string]*apiv1.ConfigMap + AnnotationRisk string + AllowCrossNamespace bool } // GetDefaultBackend returns the backend that must be used as default @@ -34,6 +36,17 @@ func (m Mock) GetDefaultBackend() defaults.Backend { return defaults.Backend{} } +func (m Mock) GetSecurityConfiguration() defaults.SecurityConfiguration { + defRisk := m.AnnotationRisk + if defRisk == "" { + defRisk = "Critical" + } + return defaults.SecurityConfiguration{ + AnnotationsRisk: defRisk, + AllowCrossNamespaceResources: m.AllowCrossNamespace, + } +} + // GetSecret searches for secrets contenating the namespace and name using a the character / func (m Mock) GetSecret(string) (*apiv1.Secret, error) { return nil, nil diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index 614dd166ab..f9bfb18a46 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -47,6 +47,8 @@ import ( _ "k8s.io/ingress-nginx/test/e2e/settings" _ "k8s.io/ingress-nginx/test/e2e/settings/modsecurity" _ "k8s.io/ingress-nginx/test/e2e/settings/ocsp" + // _ "k8s.io/ingress-nginx/test/e2e/settings/validations" // Test is not working, need to check the cross namespace stuff + _ "k8s.io/ingress-nginx/test/e2e/ssl" _ "k8s.io/ingress-nginx/test/e2e/status" _ "k8s.io/ingress-nginx/test/e2e/tcpudp" diff --git a/test/e2e/settings/validations/validations.go b/test/e2e/settings/validations/validations.go new file mode 100644 index 0000000000..47d5ff4396 --- /dev/null +++ b/test/e2e/settings/validations/validations.go @@ -0,0 +1,110 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package annotations + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/bcrypt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/ingress-nginx/test/e2e/framework" +) + +func buildSecret(username, password, name, namespace string) *corev1.Secret { + //out, err := exec.Command("openssl", "passwd", "-crypt", password).CombinedOutput() + out, err := bcrypt.GenerateFromPassword([]byte(password), 14) + encpass := fmt.Sprintf("%v:%s\n", username, out) + assert.Nil(ginkgo.GinkgoT(), err) + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionGracePeriodSeconds: framework.NewInt64(1), + }, + Data: map[string][]byte{ + "auth": []byte(encpass), + }, + Type: corev1.SecretTypeOpaque, + } +} + +var _ = framework.DescribeAnnotation("annotation validations", func() { + f := framework.NewDefaultFramework("annotations-validations") + + ginkgo.BeforeEach(func() { + f.NewEchoDeployment() + otherns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "otherns", + }, + } + _, err := f.KubeClientSet.CoreV1().Namespaces().Create(context.Background(), otherns, metav1.CreateOptions{}) + assert.Nil(ginkgo.GinkgoT(), err, "creating namespace") + }) + + ginkgo.AfterEach(func() { + err := f.KubeClientSet.CoreV1().Namespaces().Delete(context.Background(), "otherns", metav1.DeleteOptions{}) + assert.Nil(ginkgo.GinkgoT(), err, "deleting namespace") + }) + + ginkgo.It("should return status code 401 when authentication is configured but Authorization header is not configured", func() { + host := "annotation-validations" + // Allow cross namespace consumption + f.UpdateNginxConfigMapData("allow-cross-namespace-resources", "true") + // Sleep a while just to guarantee that the configmap is applied + framework.Sleep() + + s := f.EnsureSecret(buildSecret("foo", "bar", "test", "otherns")) + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/auth-type": "basic", + "nginx.ingress.kubernetes.io/auth-secret": fmt.Sprintf("%s/%s", s.Namespace, s.Name), + "nginx.ingress.kubernetes.io/auth-realm": "test auth", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, "server_name annotation-validations") + }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusUnauthorized). + Body().Contains("401 Authorization Required") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithBasicAuth("foo", "bar"). + Expect(). + Status(http.StatusOK) + }) +}) From 8e8e44e065bafbc51162487b9d460bdf70258867 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Mon, 19 Jun 2023 00:28:27 +0000 Subject: [PATCH 05/14] Add risk, flag for validation, tests --- internal/ingress/annotations/alias/main.go | 7 +- internal/ingress/annotations/annotations.go | 3 + internal/ingress/annotations/auth/main.go | 5 ++ internal/ingress/annotations/authreq/main.go | 5 ++ .../ingress/annotations/authreqglobal/main.go | 5 ++ internal/ingress/annotations/authtls/main.go | 5 ++ .../annotations/backendprotocol/main.go | 5 ++ internal/ingress/annotations/canary/main.go | 5 ++ .../annotations/clientbodybuffersize/main.go | 5 ++ .../ingress/annotations/connection/main.go | 5 ++ internal/ingress/annotations/cors/main.go | 5 ++ .../annotations/customhttperrors/main.go | 7 +- .../annotations/defaultbackend/main.go | 5 ++ internal/ingress/annotations/fastcgi/main.go | 5 ++ .../annotations/globalratelimit/main.go | 5 ++ .../annotations/http2pushpreload/main.go | 5 ++ .../ingress/annotations/ipallowlist/main.go | 5 ++ .../ingress/annotations/ipdenylist/main.go | 5 ++ .../ingress/annotations/loadbalancing/main.go | 5 ++ internal/ingress/annotations/log/main.go | 5 ++ internal/ingress/annotations/mirror/main.go | 5 ++ .../ingress/annotations/modsecurity/main.go | 5 ++ .../ingress/annotations/opentelemetry/main.go | 5 ++ .../ingress/annotations/opentracing/main.go | 5 ++ internal/ingress/annotations/parser/main.go | 23 ++++- .../ingress/annotations/parser/validators.go | 22 +++-- .../annotations/parser/validators_test.go | 87 +++++++++++++++++++ .../annotations/portinredirect/main.go | 5 ++ internal/ingress/annotations/proxy/main.go | 5 ++ internal/ingress/annotations/proxyssl/main.go | 5 ++ .../ingress/annotations/ratelimit/main.go | 5 ++ .../ingress/annotations/redirect/redirect.go | 5 ++ internal/ingress/annotations/rewrite/main.go | 5 ++ internal/ingress/annotations/satisfy/main.go | 5 ++ .../ingress/annotations/serversnippet/main.go | 5 ++ .../annotations/serviceupstream/main.go | 5 ++ .../annotations/sessionaffinity/main.go | 5 ++ internal/ingress/annotations/snippet/main.go | 5 ++ .../ingress/annotations/sslcipher/main.go | 5 ++ .../annotations/sslpassthrough/main.go | 5 ++ .../ingress/annotations/streamsnippet/main.go | 5 ++ .../annotations/upstreamhashby/main.go | 5 ++ .../ingress/annotations/upstreamvhost/main.go | 5 ++ .../annotations/xforwardedprefix/main.go | 5 ++ 44 files changed, 331 insertions(+), 8 deletions(-) diff --git a/internal/ingress/annotations/alias/main.go b/internal/ingress/annotations/alias/main.go index 902478af0d..409ed7a772 100644 --- a/internal/ingress/annotations/alias/main.go +++ b/internal/ingress/annotations/alias/main.go @@ -37,7 +37,7 @@ var aliasAnnotation = parser.Annotation{ serverAliasAnnotation: { Validator: parser.ValidateArrayOfServerName, Scope: parser.AnnotationScopeIngress, - Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set + Risk: parser.AnnotationRiskHigh, // High as this allows regex chars Documentation: `this annotation can be used to define additional server aliases for this Ingress`, }, @@ -86,3 +86,8 @@ func (a alias) Parse(ing *networking.Ingress) (interface{}, error) { return l, nil } + +func (a alias) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, aliasAnnotation.Annotations) +} diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go index 5a9cbd4f94..5371e6eb74 100644 --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -180,6 +180,9 @@ func (e Extractor) Extract(ing *networking.Ingress) (*Ingress, error) { data := make(map[string]interface{}) for name, annotationParser := range e.annotations { + if err := annotationParser.Validate(ing.GetAnnotations()); err != nil { + return nil, errors.NewRiskyAnnotations(name) + } val, err := annotationParser.Parse(ing) klog.V(5).InfoS("Parsing Ingress annotation", "name", name, "ingress", klog.KObj(ing), "value", val) if err != nil { diff --git a/internal/ingress/annotations/auth/main.go b/internal/ingress/annotations/auth/main.go index e7c0ddaa62..37c6a0d31c 100644 --- a/internal/ingress/annotations/auth/main.go +++ b/internal/ingress/annotations/auth/main.go @@ -275,3 +275,8 @@ func dumpSecretAuthMap(filename string, secret *api.Secret) error { func (a auth) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a auth) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, authSecretAnnotations.Annotations) +} diff --git a/internal/ingress/annotations/authreq/main.go b/internal/ingress/annotations/authreq/main.go index 2b4330b6f2..099e1ec6d5 100644 --- a/internal/ingress/annotations/authreq/main.go +++ b/internal/ingress/annotations/authreq/main.go @@ -502,3 +502,8 @@ func ParseStringToCacheDurations(input string) ([]string, error) { func (a authReq) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a authReq) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, authReqAnnotations.Annotations) +} diff --git a/internal/ingress/annotations/authreqglobal/main.go b/internal/ingress/annotations/authreqglobal/main.go index 03e0b88b3d..a931d0bbd2 100644 --- a/internal/ingress/annotations/authreqglobal/main.go +++ b/internal/ingress/annotations/authreqglobal/main.go @@ -67,3 +67,8 @@ func (a authReqGlobal) Parse(ing *networking.Ingress) (interface{}, error) { func (a authReqGlobal) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a authReqGlobal) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, globalAuthAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/authtls/main.go b/internal/ingress/annotations/authtls/main.go index cc383004e2..2c47a14ff4 100644 --- a/internal/ingress/annotations/authtls/main.go +++ b/internal/ingress/annotations/authtls/main.go @@ -215,3 +215,8 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) { func (a authTLS) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a authTLS) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, authTLSAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/backendprotocol/main.go b/internal/ingress/annotations/backendprotocol/main.go index a8f7d144aa..f5d287f024 100644 --- a/internal/ingress/annotations/backendprotocol/main.go +++ b/internal/ingress/annotations/backendprotocol/main.go @@ -81,3 +81,8 @@ func (a backendProtocol) Parse(ing *networking.Ingress) (interface{}, error) { return proto, nil } + +func (a backendProtocol) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, backendProtocolConfig.Annotations) +} diff --git a/internal/ingress/annotations/canary/main.go b/internal/ingress/annotations/canary/main.go index e999018033..866f3a8349 100644 --- a/internal/ingress/annotations/canary/main.go +++ b/internal/ingress/annotations/canary/main.go @@ -188,3 +188,8 @@ func (c canary) Parse(ing *networking.Ingress) (interface{}, error) { func (c canary) GetDocumentation() parser.AnnotationFields { return c.annotationConfig.Annotations } + +func (a canary) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, CanaryAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/clientbodybuffersize/main.go b/internal/ingress/annotations/clientbodybuffersize/main.go index fd82e85b05..025db96f18 100644 --- a/internal/ingress/annotations/clientbodybuffersize/main.go +++ b/internal/ingress/annotations/clientbodybuffersize/main.go @@ -64,3 +64,8 @@ func (cbbs clientBodyBufferSize) GetDocumentation() parser.AnnotationFields { func (cbbs clientBodyBufferSize) Parse(ing *networking.Ingress) (interface{}, error) { return parser.GetStringAnnotation(clientBodyBufferSizeAnnotation, ing, cbbs.annotationConfig.Annotations) } + +func (a clientBodyBufferSize) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, clientBodyBufferSizeConfig.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/connection/main.go b/internal/ingress/annotations/connection/main.go index 8b10145a6a..4cca6a94f1 100644 --- a/internal/ingress/annotations/connection/main.go +++ b/internal/ingress/annotations/connection/main.go @@ -100,3 +100,8 @@ func (r1 *Config) Equal(r2 *Config) bool { func (a connection) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a connection) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, connectionHeadersAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/cors/main.go b/internal/ingress/annotations/cors/main.go index 8ee0b4f38c..8698588d47 100644 --- a/internal/ingress/annotations/cors/main.go +++ b/internal/ingress/annotations/cors/main.go @@ -259,3 +259,8 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) { func (c cors) GetDocumentation() parser.AnnotationFields { return c.annotationConfig.Annotations } + +func (a cors) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, corsAnnotation.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/customhttperrors/main.go b/internal/ingress/annotations/customhttperrors/main.go index 2eb19ed8bb..b2623b0236 100644 --- a/internal/ingress/annotations/customhttperrors/main.go +++ b/internal/ingress/annotations/customhttperrors/main.go @@ -66,7 +66,7 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation { // Parse parses the annotations contained in the ingress to use // custom http errors func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) { - c, err := parser.GetStringAnnotation("custom-http-errors", ing, e.annotationConfig.Annotations) + c, err := parser.GetStringAnnotation(customHTTPErrorsAnnotation, ing, e.annotationConfig.Annotations) if err != nil { return nil, err } @@ -87,3 +87,8 @@ func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) { func (e customhttperrors) GetDocumentation() parser.AnnotationFields { return e.annotationConfig.Annotations } + +func (a customhttperrors) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, customHTTPErrorsAnnotations.Annotations) +} diff --git a/internal/ingress/annotations/defaultbackend/main.go b/internal/ingress/annotations/defaultbackend/main.go index d73b7fa942..f413b4bab3 100644 --- a/internal/ingress/annotations/defaultbackend/main.go +++ b/internal/ingress/annotations/defaultbackend/main.go @@ -75,3 +75,8 @@ func (db backend) Parse(ing *networking.Ingress) (interface{}, error) { func (db backend) GetDocumentation() parser.AnnotationFields { return db.annotationConfig.Annotations } + +func (a backend) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, defaultBackendAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/fastcgi/main.go b/internal/ingress/annotations/fastcgi/main.go index 7fa61e3dbb..2cc20444c1 100644 --- a/internal/ingress/annotations/fastcgi/main.go +++ b/internal/ingress/annotations/fastcgi/main.go @@ -160,3 +160,8 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) { func (a fastcgi) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a fastcgi) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, fastCGIAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/globalratelimit/main.go b/internal/ingress/annotations/globalratelimit/main.go index 97e820edf6..0bd9dad9cc 100644 --- a/internal/ingress/annotations/globalratelimit/main.go +++ b/internal/ingress/annotations/globalratelimit/main.go @@ -173,3 +173,8 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) { func (a globalratelimit) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a globalratelimit) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, globalRateLimitAnnotationConfig.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/http2pushpreload/main.go b/internal/ingress/annotations/http2pushpreload/main.go index 263d7a7b2f..f9bc2f0f8d 100644 --- a/internal/ingress/annotations/http2pushpreload/main.go +++ b/internal/ingress/annotations/http2pushpreload/main.go @@ -61,3 +61,8 @@ func (h2pp http2PushPreload) Parse(ing *networking.Ingress) (interface{}, error) func (h2pp http2PushPreload) GetDocumentation() parser.AnnotationFields { return h2pp.annotationConfig.Annotations } + +func (a http2PushPreload) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, http2PushPreloadAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/ipallowlist/main.go b/internal/ingress/annotations/ipallowlist/main.go index 94ea0a7f1d..270e265c02 100644 --- a/internal/ingress/annotations/ipallowlist/main.go +++ b/internal/ingress/annotations/ipallowlist/main.go @@ -126,3 +126,8 @@ func (a ipallowlist) Parse(ing *networking.Ingress) (interface{}, error) { func (a ipallowlist) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a ipallowlist) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, allowlistAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/ipdenylist/main.go b/internal/ingress/annotations/ipdenylist/main.go index 6d09578be4..0d8885401b 100644 --- a/internal/ingress/annotations/ipdenylist/main.go +++ b/internal/ingress/annotations/ipdenylist/main.go @@ -123,3 +123,8 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) { func (a ipdenylist) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a ipdenylist) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, denylistAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/loadbalancing/main.go b/internal/ingress/annotations/loadbalancing/main.go index 4555def4d8..80bbdb4890 100644 --- a/internal/ingress/annotations/loadbalancing/main.go +++ b/internal/ingress/annotations/loadbalancing/main.go @@ -67,3 +67,8 @@ func (a loadbalancing) Parse(ing *networking.Ingress) (interface{}, error) { func (a loadbalancing) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a loadbalancing) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, loadBalanceAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/log/main.go b/internal/ingress/annotations/log/main.go index e42de2bd18..97779475e9 100644 --- a/internal/ingress/annotations/log/main.go +++ b/internal/ingress/annotations/log/main.go @@ -100,3 +100,8 @@ func (l log) Parse(ing *networking.Ingress) (interface{}, error) { func (l log) GetDocumentation() parser.AnnotationFields { return l.annotationConfig.Annotations } + +func (a log) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, logAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/mirror/main.go b/internal/ingress/annotations/mirror/main.go index 33133bedca..bd4effa71c 100644 --- a/internal/ingress/annotations/mirror/main.go +++ b/internal/ingress/annotations/mirror/main.go @@ -161,3 +161,8 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) { func (a mirror) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a mirror) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, mirrorAnnotation.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/modsecurity/main.go b/internal/ingress/annotations/modsecurity/main.go index 607d98d466..004b9ae6e9 100644 --- a/internal/ingress/annotations/modsecurity/main.go +++ b/internal/ingress/annotations/modsecurity/main.go @@ -153,3 +153,8 @@ func (a modSecurity) Parse(ing *networking.Ingress) (interface{}, error) { func (a modSecurity) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a modSecurity) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, modsecurityAnnotation.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/opentelemetry/main.go b/internal/ingress/annotations/opentelemetry/main.go index 89ab4b3c8d..3add51a34f 100644 --- a/internal/ingress/annotations/opentelemetry/main.go +++ b/internal/ingress/annotations/opentelemetry/main.go @@ -149,3 +149,8 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) { func (c opentelemetry) GetDocumentation() parser.AnnotationFields { return c.annotationConfig.Annotations } + +func (a opentelemetry) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, otelAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/opentracing/main.go b/internal/ingress/annotations/opentracing/main.go index 4723a0e352..4bce34cba5 100644 --- a/internal/ingress/annotations/opentracing/main.go +++ b/internal/ingress/annotations/opentracing/main.go @@ -106,3 +106,8 @@ func (s opentracing) Parse(ing *networking.Ingress) (interface{}, error) { func (s opentracing) GetDocumentation() parser.AnnotationFields { return s.annotationConfig.Annotations } + +func (a opentracing) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, opentracingAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/parser/main.go b/internal/ingress/annotations/parser/main.go index af19b64200..e10c373f16 100644 --- a/internal/ingress/annotations/parser/main.go +++ b/internal/ingress/annotations/parser/main.go @@ -30,12 +30,15 @@ import ( // DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller const ( - DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io" + DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io" + DefaultDisableAnnotationValidation = true ) var ( // AnnotationsPrefix is the mutable attribute that the controller explicitly refers to AnnotationsPrefix = DefaultAnnotationsPrefix + // DisableAnnotationValidation is the mutable attribute for enabling or disabling the validation functions + DisableAnnotationValidation = DefaultDisableAnnotationValidation ) // AnnotationGroup defines the group that this annotation may belong @@ -92,6 +95,7 @@ type Annotation struct { type IngressAnnotation interface { Parse(ing *networking.Ingress) (interface{}, error) GetDocumentation() AnnotationFields + Validate(anns map[string]string) error } type ingAnnotations map[string]string @@ -189,6 +193,23 @@ func GetAnnotationWithPrefix(suffix string) string { return fmt.Sprintf("%v/%v", AnnotationsPrefix, suffix) } +func TrimAnnotationPrefix(annotation string) string { + return strings.TrimPrefix(annotation, AnnotationsPrefix+"/") +} + +func StringRiskToRisk(risk string) AnnotationRisk { + switch strings.ToLower(risk) { + case "critical": + return AnnotationRiskCritical + case "high": + return AnnotationRiskHigh + case "medium": + return AnnotationRiskMedium + default: + return AnnotationRiskLow + } +} + func normalizeString(input string) string { trimmedContent := []string{} for _, line := range strings.Split(input, "\n") { diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go index 45ce6911ea..f9ac5f6d0a 100644 --- a/internal/ingress/annotations/parser/validators.go +++ b/internal/ingress/annotations/parser/validators.go @@ -17,6 +17,7 @@ limitations under the License. package parser import ( + "errors" "fmt" "regexp" "strconv" @@ -25,7 +26,7 @@ import ( networking "k8s.io/api/networking/v1" machineryvalidation "k8s.io/apimachinery/pkg/api/validation" - "k8s.io/ingress-nginx/internal/ingress/errors" + ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/net" "k8s.io/klog/v2" ) @@ -189,12 +190,12 @@ func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFiel } if ing == nil || len(ing.GetAnnotations()) == 0 { - return "", errors.ErrMissingAnnotations + return "", ing_errors.ErrMissingAnnotations } annotationFullName := GetAnnotationWithPrefix(name) if annotationFullName == "" { - return "", errors.ErrInvalidAnnotationName + return "", ing_errors.ErrInvalidAnnotationName } annotationValue := ing.GetAnnotations()[annotationFullName] @@ -213,13 +214,24 @@ func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFiel } } // We don't run validation against empty values - if annotationValue != "" { + if annotationValue != "" && !DisableAnnotationValidation { if err := validateFunc(annotationValue); err != nil { klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue) - return "", errors.NewValidationError(annotationFullName) + return "", ing_errors.NewValidationError(annotationFullName) } } } return annotationFullName, nil } + +func CheckAnnotationRisk(annotations map[string]string, maxrisk AnnotationRisk, config AnnotationFields) error { + var err error + for annotation := range annotations { + annPure := TrimAnnotationPrefix(annotation) + if cfg, ok := config[annPure]; ok && cfg.Risk > maxrisk { + err = errors.Join(err, fmt.Errorf("annotation %s is too risky for environment", annotation)) + } + } + return err +} diff --git a/internal/ingress/annotations/parser/validators_test.go b/internal/ingress/annotations/parser/validators_test.go index d37f83b1ec..2aa6cec375 100644 --- a/internal/ingress/annotations/parser/validators_test.go +++ b/internal/ingress/annotations/parser/validators_test.go @@ -221,3 +221,90 @@ func Test_checkAnnotation(t *testing.T) { }) } } + +func TestCheckAnnotationRisk(t *testing.T) { + + tests := []struct { + name string + annotations map[string]string + maxrisk AnnotationRisk + config AnnotationFields + wantErr bool + }{ + { + name: "high risk should not be accepted with maximum medium", + maxrisk: AnnotationRiskMedium, + annotations: map[string]string{ + "nginx.ingress.kubernetes.io/bla": "blo", + "nginx.ingress.kubernetes.io/bli": "bl3", + }, + config: AnnotationFields{ + "bla": { + Risk: AnnotationRiskHigh, + }, + "bli": { + Risk: AnnotationRiskMedium, + }, + }, + wantErr: true, + }, + { + name: "high risk should be accepted with maximum critical", + maxrisk: AnnotationRiskCritical, + annotations: map[string]string{ + "nginx.ingress.kubernetes.io/bla": "blo", + "nginx.ingress.kubernetes.io/bli": "bl3", + }, + config: AnnotationFields{ + "bla": { + Risk: AnnotationRiskHigh, + }, + "bli": { + Risk: AnnotationRiskMedium, + }, + }, + wantErr: false, + }, + { + name: "low risk should be accepted with maximum low", + maxrisk: AnnotationRiskLow, + annotations: map[string]string{ + "nginx.ingress.kubernetes.io/bla": "blo", + "nginx.ingress.kubernetes.io/bli": "bl3", + }, + config: AnnotationFields{ + "bla": { + Risk: AnnotationRiskLow, + }, + "bli": { + Risk: AnnotationRiskLow, + }, + }, + wantErr: false, + }, + { + name: "critical risk should be accepted with maximum critical", + maxrisk: AnnotationRiskCritical, + annotations: map[string]string{ + "nginx.ingress.kubernetes.io/bla": "blo", + "nginx.ingress.kubernetes.io/bli": "bl3", + }, + config: AnnotationFields{ + "bla": { + Risk: AnnotationRiskCritical, + }, + "bli": { + Risk: AnnotationRiskCritical, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CheckAnnotationRisk(tt.annotations, tt.maxrisk, tt.config); (err != nil) != tt.wantErr { + t.Errorf("CheckAnnotationRisk() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/ingress/annotations/portinredirect/main.go b/internal/ingress/annotations/portinredirect/main.go index a8f8dc2bf0..10f2c3d792 100644 --- a/internal/ingress/annotations/portinredirect/main.go +++ b/internal/ingress/annotations/portinredirect/main.go @@ -66,3 +66,8 @@ func (a portInRedirect) Parse(ing *networking.Ingress) (interface{}, error) { func (a portInRedirect) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a portInRedirect) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, portsInRedirectAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/proxy/main.go b/internal/ingress/annotations/proxy/main.go index f24f352c7c..fe7f66cbe9 100644 --- a/internal/ingress/annotations/proxy/main.go +++ b/internal/ingress/annotations/proxy/main.go @@ -357,3 +357,8 @@ func (a proxy) Parse(ing *networking.Ingress) (interface{}, error) { func (a proxy) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a proxy) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, proxyAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/proxyssl/main.go b/internal/ingress/annotations/proxyssl/main.go index 411dc66e03..07b5dbcfc6 100644 --- a/internal/ingress/annotations/proxyssl/main.go +++ b/internal/ingress/annotations/proxyssl/main.go @@ -259,3 +259,8 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) { func (p proxySSL) GetDocumentation() parser.AnnotationFields { return p.annotationConfig.Annotations } + +func (a proxySSL) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, proxySSLAnnotation.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/ratelimit/main.go b/internal/ingress/annotations/ratelimit/main.go index 6283434078..12b426eb7d 100644 --- a/internal/ingress/annotations/ratelimit/main.go +++ b/internal/ingress/annotations/ratelimit/main.go @@ -294,3 +294,8 @@ func encode(s string) string { func (a ratelimit) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a ratelimit) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, rateLimitAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/redirect/redirect.go b/internal/ingress/annotations/redirect/redirect.go index a7081da2f7..1cbcc4e8ce 100644 --- a/internal/ingress/annotations/redirect/redirect.go +++ b/internal/ingress/annotations/redirect/redirect.go @@ -177,3 +177,8 @@ func isValidURL(s string) error { func (a redirect) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a redirect) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, redirectAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/rewrite/main.go b/internal/ingress/annotations/rewrite/main.go index b430a22183..debd6e0f77 100644 --- a/internal/ingress/annotations/rewrite/main.go +++ b/internal/ingress/annotations/rewrite/main.go @@ -208,3 +208,8 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) { func (a rewrite) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a rewrite) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, rewriteAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/satisfy/main.go b/internal/ingress/annotations/satisfy/main.go index e9eefa7b08..dc0b25a6ac 100644 --- a/internal/ingress/annotations/satisfy/main.go +++ b/internal/ingress/annotations/satisfy/main.go @@ -68,3 +68,8 @@ func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) { func (s satisfy) GetDocumentation() parser.AnnotationFields { return s.annotationConfig.Annotations } + +func (a satisfy) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, satisfyAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/serversnippet/main.go b/internal/ingress/annotations/serversnippet/main.go index e44675f35a..3360a7dc35 100644 --- a/internal/ingress/annotations/serversnippet/main.go +++ b/internal/ingress/annotations/serversnippet/main.go @@ -62,3 +62,8 @@ func (a serverSnippet) Parse(ing *networking.Ingress) (interface{}, error) { func (a serverSnippet) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a serverSnippet) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, serverSnippetAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/serviceupstream/main.go b/internal/ingress/annotations/serviceupstream/main.go index 9e5d00b35d..ca069dc1c7 100644 --- a/internal/ingress/annotations/serviceupstream/main.go +++ b/internal/ingress/annotations/serviceupstream/main.go @@ -68,3 +68,8 @@ func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) { func (s serviceUpstream) GetDocumentation() parser.AnnotationFields { return s.annotationConfig.Annotations } + +func (a serviceUpstream) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, serviceUpstreamAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/sessionaffinity/main.go b/internal/ingress/annotations/sessionaffinity/main.go index fc2dccad9f..ea9fe9924e 100644 --- a/internal/ingress/annotations/sessionaffinity/main.go +++ b/internal/ingress/annotations/sessionaffinity/main.go @@ -297,3 +297,8 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) { func (a affinity) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a affinity) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, sessionAffinityAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/snippet/main.go b/internal/ingress/annotations/snippet/main.go index 0dd948969c..8519625441 100644 --- a/internal/ingress/annotations/snippet/main.go +++ b/internal/ingress/annotations/snippet/main.go @@ -62,3 +62,8 @@ func (a snippet) Parse(ing *networking.Ingress) (interface{}, error) { func (a snippet) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a snippet) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, configurationSnippetAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/sslcipher/main.go b/internal/ingress/annotations/sslcipher/main.go index ee391f92cb..30f9cf8229 100644 --- a/internal/ingress/annotations/sslcipher/main.go +++ b/internal/ingress/annotations/sslcipher/main.go @@ -103,3 +103,8 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) { func (sc sslCipher) GetDocumentation() parser.AnnotationFields { return sc.annotationConfig.Annotations } + +func (a sslCipher) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, sslCipherAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/sslpassthrough/main.go b/internal/ingress/annotations/sslpassthrough/main.go index ebeb4c4050..2635136ed8 100644 --- a/internal/ingress/annotations/sslpassthrough/main.go +++ b/internal/ingress/annotations/sslpassthrough/main.go @@ -65,3 +65,8 @@ func (a sslpt) Parse(ing *networking.Ingress) (interface{}, error) { func (a sslpt) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a sslpt) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, sslPassthroughAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/streamsnippet/main.go b/internal/ingress/annotations/streamsnippet/main.go index 6803977496..2d69c4b2cb 100644 --- a/internal/ingress/annotations/streamsnippet/main.go +++ b/internal/ingress/annotations/streamsnippet/main.go @@ -62,3 +62,8 @@ func (a streamSnippet) Parse(ing *networking.Ingress) (interface{}, error) { func (a streamSnippet) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a streamSnippet) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, streamSnippetAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/upstreamhashby/main.go b/internal/ingress/annotations/upstreamhashby/main.go index f3ac382ac0..a111ffd415 100644 --- a/internal/ingress/annotations/upstreamhashby/main.go +++ b/internal/ingress/annotations/upstreamhashby/main.go @@ -107,3 +107,8 @@ func (a upstreamhashby) Parse(ing *networking.Ingress) (interface{}, error) { func (a upstreamhashby) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a upstreamhashby) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, upstreamHashByAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/upstreamvhost/main.go b/internal/ingress/annotations/upstreamvhost/main.go index 71090716c1..1c779366d3 100644 --- a/internal/ingress/annotations/upstreamvhost/main.go +++ b/internal/ingress/annotations/upstreamvhost/main.go @@ -63,3 +63,8 @@ func (a upstreamVhost) Parse(ing *networking.Ingress) (interface{}, error) { func (a upstreamVhost) GetDocumentation() parser.AnnotationFields { return a.annotationConfig.Annotations } + +func (a upstreamVhost) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, upstreamVhostAnnotations.Annotations) +} \ No newline at end of file diff --git a/internal/ingress/annotations/xforwardedprefix/main.go b/internal/ingress/annotations/xforwardedprefix/main.go index b45fb5c176..2b60cb3cff 100644 --- a/internal/ingress/annotations/xforwardedprefix/main.go +++ b/internal/ingress/annotations/xforwardedprefix/main.go @@ -61,3 +61,8 @@ func (cbbs xforwardedprefix) Parse(ing *networking.Ingress) (interface{}, error) func (cbbs xforwardedprefix) GetDocumentation() parser.AnnotationFields { return cbbs.annotationConfig.Annotations } + +func (a xforwardedprefix) Validate(anns map[string]string) error { + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + return parser.CheckAnnotationRisk(anns, maxrisk, xForwardedForAnnotations.Annotations) +} \ No newline at end of file From 8497484908cd2ca24bfa4ec2a1f0e03fd7f7dd0e Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Mon, 19 Jun 2023 00:32:25 +0000 Subject: [PATCH 06/14] Add missing formating --- .../ingress/annotations/authreqglobal/main.go | 2 +- internal/ingress/annotations/authtls/main.go | 2 +- internal/ingress/annotations/canary/main.go | 2 +- .../annotations/clientbodybuffersize/main.go | 2 +- .../ingress/annotations/connection/main.go | 2 +- internal/ingress/annotations/cors/main.go | 2 +- .../annotations/defaultbackend/main.go | 2 +- internal/ingress/annotations/fastcgi/main.go | 2 +- .../annotations/globalratelimit/main.go | 2 +- .../annotations/http2pushpreload/main.go | 2 +- .../ingress/annotations/ipallowlist/main.go | 2 +- .../ingress/annotations/ipdenylist/main.go | 2 +- .../ingress/annotations/loadbalancing/main.go | 2 +- internal/ingress/annotations/log/main.go | 2 +- internal/ingress/annotations/mirror/main.go | 2 +- .../ingress/annotations/modsecurity/main.go | 2 +- .../ingress/annotations/opentelemetry/main.go | 2 +- .../ingress/annotations/opentracing/main.go | 2 +- .../annotations/portinredirect/main.go | 2 +- internal/ingress/annotations/proxy/main.go | 2 +- internal/ingress/annotations/proxyssl/main.go | 2 +- .../ingress/annotations/ratelimit/main.go | 2 +- .../ingress/annotations/redirect/redirect.go | 2 +- internal/ingress/annotations/rewrite/main.go | 2 +- internal/ingress/annotations/satisfy/main.go | 2 +- .../ingress/annotations/serversnippet/main.go | 2 +- .../annotations/serviceupstream/main.go | 2 +- .../annotations/sessionaffinity/main.go | 2 +- internal/ingress/annotations/snippet/main.go | 2 +- .../ingress/annotations/sslcipher/main.go | 2 +- .../annotations/sslpassthrough/main.go | 2 +- .../ingress/annotations/streamsnippet/main.go | 2 +- .../annotations/upstreamhashby/main.go | 2 +- .../ingress/annotations/upstreamvhost/main.go | 2 +- .../annotations/xforwardedprefix/main.go | 2 +- internal/ingress/errors/errors.go | 22 ++++ pkg/flags/flags.go | 4 + .../validations/values.yaml | 38 ++++++ test/e2e/e2e.go | 2 +- test/e2e/settings/validations/validations.go | 110 +++++++----------- 40 files changed, 143 insertions(+), 103 deletions(-) create mode 100644 test/e2e-image/namespace-overlays/validations/values.yaml diff --git a/internal/ingress/annotations/authreqglobal/main.go b/internal/ingress/annotations/authreqglobal/main.go index a931d0bbd2..6032dc5953 100644 --- a/internal/ingress/annotations/authreqglobal/main.go +++ b/internal/ingress/annotations/authreqglobal/main.go @@ -71,4 +71,4 @@ func (a authReqGlobal) GetDocumentation() parser.AnnotationFields { func (a authReqGlobal) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, globalAuthAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/authtls/main.go b/internal/ingress/annotations/authtls/main.go index 2c47a14ff4..53ee83fa80 100644 --- a/internal/ingress/annotations/authtls/main.go +++ b/internal/ingress/annotations/authtls/main.go @@ -219,4 +219,4 @@ func (a authTLS) GetDocumentation() parser.AnnotationFields { func (a authTLS) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, authTLSAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/canary/main.go b/internal/ingress/annotations/canary/main.go index 866f3a8349..9e688a118e 100644 --- a/internal/ingress/annotations/canary/main.go +++ b/internal/ingress/annotations/canary/main.go @@ -192,4 +192,4 @@ func (c canary) GetDocumentation() parser.AnnotationFields { func (a canary) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, CanaryAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/clientbodybuffersize/main.go b/internal/ingress/annotations/clientbodybuffersize/main.go index 025db96f18..61094d0c6e 100644 --- a/internal/ingress/annotations/clientbodybuffersize/main.go +++ b/internal/ingress/annotations/clientbodybuffersize/main.go @@ -68,4 +68,4 @@ func (cbbs clientBodyBufferSize) Parse(ing *networking.Ingress) (interface{}, er func (a clientBodyBufferSize) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, clientBodyBufferSizeConfig.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/connection/main.go b/internal/ingress/annotations/connection/main.go index 4cca6a94f1..eaf83b4e00 100644 --- a/internal/ingress/annotations/connection/main.go +++ b/internal/ingress/annotations/connection/main.go @@ -104,4 +104,4 @@ func (a connection) GetDocumentation() parser.AnnotationFields { func (a connection) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, connectionHeadersAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/cors/main.go b/internal/ingress/annotations/cors/main.go index 8698588d47..442e70158a 100644 --- a/internal/ingress/annotations/cors/main.go +++ b/internal/ingress/annotations/cors/main.go @@ -263,4 +263,4 @@ func (c cors) GetDocumentation() parser.AnnotationFields { func (a cors) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, corsAnnotation.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/defaultbackend/main.go b/internal/ingress/annotations/defaultbackend/main.go index f413b4bab3..0dae649055 100644 --- a/internal/ingress/annotations/defaultbackend/main.go +++ b/internal/ingress/annotations/defaultbackend/main.go @@ -79,4 +79,4 @@ func (db backend) GetDocumentation() parser.AnnotationFields { func (a backend) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, defaultBackendAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/fastcgi/main.go b/internal/ingress/annotations/fastcgi/main.go index 2cc20444c1..ec4feec660 100644 --- a/internal/ingress/annotations/fastcgi/main.go +++ b/internal/ingress/annotations/fastcgi/main.go @@ -164,4 +164,4 @@ func (a fastcgi) GetDocumentation() parser.AnnotationFields { func (a fastcgi) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, fastCGIAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/globalratelimit/main.go b/internal/ingress/annotations/globalratelimit/main.go index 0bd9dad9cc..c8803dada6 100644 --- a/internal/ingress/annotations/globalratelimit/main.go +++ b/internal/ingress/annotations/globalratelimit/main.go @@ -177,4 +177,4 @@ func (a globalratelimit) GetDocumentation() parser.AnnotationFields { func (a globalratelimit) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, globalRateLimitAnnotationConfig.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/http2pushpreload/main.go b/internal/ingress/annotations/http2pushpreload/main.go index f9bc2f0f8d..36441c3013 100644 --- a/internal/ingress/annotations/http2pushpreload/main.go +++ b/internal/ingress/annotations/http2pushpreload/main.go @@ -65,4 +65,4 @@ func (h2pp http2PushPreload) GetDocumentation() parser.AnnotationFields { func (a http2PushPreload) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, http2PushPreloadAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/ipallowlist/main.go b/internal/ingress/annotations/ipallowlist/main.go index 270e265c02..f2fb2cb81d 100644 --- a/internal/ingress/annotations/ipallowlist/main.go +++ b/internal/ingress/annotations/ipallowlist/main.go @@ -130,4 +130,4 @@ func (a ipallowlist) GetDocumentation() parser.AnnotationFields { func (a ipallowlist) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, allowlistAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/ipdenylist/main.go b/internal/ingress/annotations/ipdenylist/main.go index 0d8885401b..37bd7ad50f 100644 --- a/internal/ingress/annotations/ipdenylist/main.go +++ b/internal/ingress/annotations/ipdenylist/main.go @@ -127,4 +127,4 @@ func (a ipdenylist) GetDocumentation() parser.AnnotationFields { func (a ipdenylist) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, denylistAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/loadbalancing/main.go b/internal/ingress/annotations/loadbalancing/main.go index 80bbdb4890..0191981a45 100644 --- a/internal/ingress/annotations/loadbalancing/main.go +++ b/internal/ingress/annotations/loadbalancing/main.go @@ -71,4 +71,4 @@ func (a loadbalancing) GetDocumentation() parser.AnnotationFields { func (a loadbalancing) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, loadBalanceAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/log/main.go b/internal/ingress/annotations/log/main.go index 97779475e9..3649ecd6a7 100644 --- a/internal/ingress/annotations/log/main.go +++ b/internal/ingress/annotations/log/main.go @@ -104,4 +104,4 @@ func (l log) GetDocumentation() parser.AnnotationFields { func (a log) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, logAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/mirror/main.go b/internal/ingress/annotations/mirror/main.go index bd4effa71c..14a14e3242 100644 --- a/internal/ingress/annotations/mirror/main.go +++ b/internal/ingress/annotations/mirror/main.go @@ -165,4 +165,4 @@ func (a mirror) GetDocumentation() parser.AnnotationFields { func (a mirror) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, mirrorAnnotation.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/modsecurity/main.go b/internal/ingress/annotations/modsecurity/main.go index 004b9ae6e9..97120eac7d 100644 --- a/internal/ingress/annotations/modsecurity/main.go +++ b/internal/ingress/annotations/modsecurity/main.go @@ -157,4 +157,4 @@ func (a modSecurity) GetDocumentation() parser.AnnotationFields { func (a modSecurity) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, modsecurityAnnotation.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/opentelemetry/main.go b/internal/ingress/annotations/opentelemetry/main.go index 3add51a34f..24f34ae4ba 100644 --- a/internal/ingress/annotations/opentelemetry/main.go +++ b/internal/ingress/annotations/opentelemetry/main.go @@ -153,4 +153,4 @@ func (c opentelemetry) GetDocumentation() parser.AnnotationFields { func (a opentelemetry) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, otelAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/opentracing/main.go b/internal/ingress/annotations/opentracing/main.go index 4bce34cba5..c8fc71d659 100644 --- a/internal/ingress/annotations/opentracing/main.go +++ b/internal/ingress/annotations/opentracing/main.go @@ -110,4 +110,4 @@ func (s opentracing) GetDocumentation() parser.AnnotationFields { func (a opentracing) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, opentracingAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/portinredirect/main.go b/internal/ingress/annotations/portinredirect/main.go index 10f2c3d792..0e51ee0f4f 100644 --- a/internal/ingress/annotations/portinredirect/main.go +++ b/internal/ingress/annotations/portinredirect/main.go @@ -70,4 +70,4 @@ func (a portInRedirect) GetDocumentation() parser.AnnotationFields { func (a portInRedirect) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, portsInRedirectAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/proxy/main.go b/internal/ingress/annotations/proxy/main.go index fe7f66cbe9..120a998301 100644 --- a/internal/ingress/annotations/proxy/main.go +++ b/internal/ingress/annotations/proxy/main.go @@ -361,4 +361,4 @@ func (a proxy) GetDocumentation() parser.AnnotationFields { func (a proxy) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, proxyAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/proxyssl/main.go b/internal/ingress/annotations/proxyssl/main.go index 07b5dbcfc6..254996926a 100644 --- a/internal/ingress/annotations/proxyssl/main.go +++ b/internal/ingress/annotations/proxyssl/main.go @@ -263,4 +263,4 @@ func (p proxySSL) GetDocumentation() parser.AnnotationFields { func (a proxySSL) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, proxySSLAnnotation.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/ratelimit/main.go b/internal/ingress/annotations/ratelimit/main.go index 12b426eb7d..38940203ad 100644 --- a/internal/ingress/annotations/ratelimit/main.go +++ b/internal/ingress/annotations/ratelimit/main.go @@ -298,4 +298,4 @@ func (a ratelimit) GetDocumentation() parser.AnnotationFields { func (a ratelimit) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, rateLimitAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/redirect/redirect.go b/internal/ingress/annotations/redirect/redirect.go index 1cbcc4e8ce..9300271eb5 100644 --- a/internal/ingress/annotations/redirect/redirect.go +++ b/internal/ingress/annotations/redirect/redirect.go @@ -181,4 +181,4 @@ func (a redirect) GetDocumentation() parser.AnnotationFields { func (a redirect) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, redirectAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/rewrite/main.go b/internal/ingress/annotations/rewrite/main.go index debd6e0f77..1134e27b19 100644 --- a/internal/ingress/annotations/rewrite/main.go +++ b/internal/ingress/annotations/rewrite/main.go @@ -212,4 +212,4 @@ func (a rewrite) GetDocumentation() parser.AnnotationFields { func (a rewrite) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, rewriteAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/satisfy/main.go b/internal/ingress/annotations/satisfy/main.go index dc0b25a6ac..d74fb33f52 100644 --- a/internal/ingress/annotations/satisfy/main.go +++ b/internal/ingress/annotations/satisfy/main.go @@ -72,4 +72,4 @@ func (s satisfy) GetDocumentation() parser.AnnotationFields { func (a satisfy) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, satisfyAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/serversnippet/main.go b/internal/ingress/annotations/serversnippet/main.go index 3360a7dc35..b6693c1d9b 100644 --- a/internal/ingress/annotations/serversnippet/main.go +++ b/internal/ingress/annotations/serversnippet/main.go @@ -66,4 +66,4 @@ func (a serverSnippet) GetDocumentation() parser.AnnotationFields { func (a serverSnippet) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, serverSnippetAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/serviceupstream/main.go b/internal/ingress/annotations/serviceupstream/main.go index ca069dc1c7..7692ffe867 100644 --- a/internal/ingress/annotations/serviceupstream/main.go +++ b/internal/ingress/annotations/serviceupstream/main.go @@ -72,4 +72,4 @@ func (s serviceUpstream) GetDocumentation() parser.AnnotationFields { func (a serviceUpstream) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, serviceUpstreamAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/sessionaffinity/main.go b/internal/ingress/annotations/sessionaffinity/main.go index ea9fe9924e..07d737de38 100644 --- a/internal/ingress/annotations/sessionaffinity/main.go +++ b/internal/ingress/annotations/sessionaffinity/main.go @@ -301,4 +301,4 @@ func (a affinity) GetDocumentation() parser.AnnotationFields { func (a affinity) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, sessionAffinityAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/snippet/main.go b/internal/ingress/annotations/snippet/main.go index 8519625441..6b75ff440c 100644 --- a/internal/ingress/annotations/snippet/main.go +++ b/internal/ingress/annotations/snippet/main.go @@ -66,4 +66,4 @@ func (a snippet) GetDocumentation() parser.AnnotationFields { func (a snippet) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, configurationSnippetAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/sslcipher/main.go b/internal/ingress/annotations/sslcipher/main.go index 30f9cf8229..a1a316d574 100644 --- a/internal/ingress/annotations/sslcipher/main.go +++ b/internal/ingress/annotations/sslcipher/main.go @@ -107,4 +107,4 @@ func (sc sslCipher) GetDocumentation() parser.AnnotationFields { func (a sslCipher) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, sslCipherAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/sslpassthrough/main.go b/internal/ingress/annotations/sslpassthrough/main.go index 2635136ed8..45007b7f77 100644 --- a/internal/ingress/annotations/sslpassthrough/main.go +++ b/internal/ingress/annotations/sslpassthrough/main.go @@ -69,4 +69,4 @@ func (a sslpt) GetDocumentation() parser.AnnotationFields { func (a sslpt) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, sslPassthroughAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/streamsnippet/main.go b/internal/ingress/annotations/streamsnippet/main.go index 2d69c4b2cb..da5a9a142e 100644 --- a/internal/ingress/annotations/streamsnippet/main.go +++ b/internal/ingress/annotations/streamsnippet/main.go @@ -66,4 +66,4 @@ func (a streamSnippet) GetDocumentation() parser.AnnotationFields { func (a streamSnippet) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, streamSnippetAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/upstreamhashby/main.go b/internal/ingress/annotations/upstreamhashby/main.go index a111ffd415..8259618831 100644 --- a/internal/ingress/annotations/upstreamhashby/main.go +++ b/internal/ingress/annotations/upstreamhashby/main.go @@ -111,4 +111,4 @@ func (a upstreamhashby) GetDocumentation() parser.AnnotationFields { func (a upstreamhashby) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, upstreamHashByAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/upstreamvhost/main.go b/internal/ingress/annotations/upstreamvhost/main.go index 1c779366d3..028e629398 100644 --- a/internal/ingress/annotations/upstreamvhost/main.go +++ b/internal/ingress/annotations/upstreamvhost/main.go @@ -67,4 +67,4 @@ func (a upstreamVhost) GetDocumentation() parser.AnnotationFields { func (a upstreamVhost) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, upstreamVhostAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/annotations/xforwardedprefix/main.go b/internal/ingress/annotations/xforwardedprefix/main.go index 2b60cb3cff..0986295b33 100644 --- a/internal/ingress/annotations/xforwardedprefix/main.go +++ b/internal/ingress/annotations/xforwardedprefix/main.go @@ -65,4 +65,4 @@ func (cbbs xforwardedprefix) GetDocumentation() parser.AnnotationFields { func (a xforwardedprefix) Validate(anns map[string]string) error { maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) return parser.CheckAnnotationRisk(anns, maxrisk, xForwardedForAnnotations.Annotations) -} \ No newline at end of file +} diff --git a/internal/ingress/errors/errors.go b/internal/ingress/errors/errors.go index 5809c96ace..e6f7fb52cf 100644 --- a/internal/ingress/errors/errors.go +++ b/internal/ingress/errors/errors.go @@ -115,6 +115,10 @@ type ValidationError struct { Reason error } +type RiskyAnnotationError struct { + Reason error +} + func (e ValidationError) Error() string { return e.Reason.Error() } @@ -132,3 +136,21 @@ func IsValidationError(e error) bool { _, ok := e.(ValidationError) return ok } + +// NewValidationError returns a new LocationDenied error +func NewRiskyAnnotations(name string) error { + return RiskyAnnotationError{ + Reason: fmt.Errorf("annotation group %s contains risky annotation based on ingress configuration", name), + } +} + +// IsRiskyAnnotationError checks if the err is an error which +// indicates that some annotation value is invalid +func IsRiskyAnnotationError(e error) bool { + _, ok := e.(ValidationError) + return ok +} + +func (e RiskyAnnotationError) Error() string { + return e.Reason.Error() +} diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index 489e248865..d5f95597cb 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -152,6 +152,9 @@ Requires the update-status parameter.`) annotationsPrefix = flags.String("annotations-prefix", parser.DefaultAnnotationsPrefix, `Prefix of the Ingress annotations specific to the NGINX controller.`) + disableAnnotationValidation = flags.Bool("disable-annotation-validation", parser.DefaultDisableAnnotationValidation, + `Prefix of the Ingress annotations specific to the NGINX controller.`) + enableSSLChainCompletion = flags.Bool("enable-ssl-chain-completion", false, `Autocomplete SSL certificate chains with missing intermediate CA certificates. Certificates uploaded to Kubernetes must have the "Authority Information Access" X.509 v3 @@ -249,6 +252,7 @@ https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-g } parser.AnnotationsPrefix = *annotationsPrefix + parser.DisableAnnotationValidation = *disableAnnotationValidation // check port collisions if !ing_net.IsPortAvailable(*httpPort) { diff --git a/test/e2e-image/namespace-overlays/validations/values.yaml b/test/e2e-image/namespace-overlays/validations/values.yaml new file mode 100644 index 0000000000..d423217dbb --- /dev/null +++ b/test/e2e-image/namespace-overlays/validations/values.yaml @@ -0,0 +1,38 @@ +# TODO: remove the need to use fullnameOverride +fullnameOverride: nginx-ingress +controller: + image: + repository: ingress-controller/controller + chroot: true + tag: 1.0.0-dev + digest: + digestChroot: + containerPort: + http: "1080" + https: "1443" + + extraArgs: + http-port: "1080" + https-port: "1443" + # e2e tests do not require information about ingress status + update-status: "false" + + scope: + enabled: true + + config: + worker-processes: "1" + service: + type: NodePort + + admissionWebhooks: + enabled: true + certificate: "/usr/local/certificates/cert" + key: "/usr/local/certificates/key" + +defaultBackend: + enabled: false + +rbac: + create: true + scope: true diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index f9bfb18a46..28adf297d6 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -47,7 +47,7 @@ import ( _ "k8s.io/ingress-nginx/test/e2e/settings" _ "k8s.io/ingress-nginx/test/e2e/settings/modsecurity" _ "k8s.io/ingress-nginx/test/e2e/settings/ocsp" - // _ "k8s.io/ingress-nginx/test/e2e/settings/validations" // Test is not working, need to check the cross namespace stuff + _ "k8s.io/ingress-nginx/test/e2e/settings/validations" _ "k8s.io/ingress-nginx/test/e2e/ssl" _ "k8s.io/ingress-nginx/test/e2e/status" diff --git a/test/e2e/settings/validations/validations.go b/test/e2e/settings/validations/validations.go index 47d5ff4396..2163ff3e17 100644 --- a/test/e2e/settings/validations/validations.go +++ b/test/e2e/settings/validations/validations.go @@ -18,93 +18,69 @@ package annotations import ( "context" - "fmt" - "net/http" - "strings" "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/assert" - "golang.org/x/crypto/bcrypt" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/ingress-nginx/test/e2e/framework" ) -func buildSecret(username, password, name, namespace string) *corev1.Secret { - //out, err := exec.Command("openssl", "passwd", "-crypt", password).CombinedOutput() - out, err := bcrypt.GenerateFromPassword([]byte(password), 14) - encpass := fmt.Sprintf("%v:%s\n", username, out) - assert.Nil(ginkgo.GinkgoT(), err) - - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionGracePeriodSeconds: framework.NewInt64(1), - }, - Data: map[string][]byte{ - "auth": []byte(encpass), - }, - Type: corev1.SecretTypeOpaque, - } -} - -var _ = framework.DescribeAnnotation("annotation validations", func() { - f := framework.NewDefaultFramework("annotations-validations") - - ginkgo.BeforeEach(func() { - f.NewEchoDeployment() - otherns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "otherns", - }, +var _ = framework.IngressNginxDescribeSerial("annotation validations", func() { + f := framework.NewDefaultFramework("validations") + + ginkgo.It("should allow ingress based on their risk on webhooks", func() { + host := "annotation-validations" + + // Low and Medium Risk annotations should be allowed, the rest should be denied + f.UpdateNginxConfigMapData("annotations-risk", "Medium") + // Sleep a while just to guarantee that the configmap is applied + framework.Sleep() + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/default-backend": "default/bla", // low risk + "nginx.ingress.kubernetes.io/denylist-source-range": "1.1.1.1/32", // medium risk } - _, err := f.KubeClientSet.CoreV1().Namespaces().Create(context.Background(), otherns, metav1.CreateOptions{}) - assert.Nil(ginkgo.GinkgoT(), err, "creating namespace") - }) - ginkgo.AfterEach(func() { - err := f.KubeClientSet.CoreV1().Namespaces().Delete(context.Background(), "otherns", metav1.DeleteOptions{}) - assert.Nil(ginkgo.GinkgoT(), err, "deleting namespace") + ginkgo.By("allow ingress with low/medium risk annotations") + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + _, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), ing, metav1.CreateOptions{}) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress with allowed annotations should not trigger an error") + + ginkgo.By("block ingress with risky annotations") + annotations["nginx.ingress.kubernetes.io/modsecurity-transaction-id"] = "bla123" // High should be blocked + annotations["nginx.ingress.kubernetes.io/modsecurity-snippet"] = "some random stuff;" // High should be blocked + ing = framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + _, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Update(context.TODO(), ing, metav1.UpdateOptions{}) + assert.NotNil(ginkgo.GinkgoT(), err, "creating ingress with risky annotations should trigger an error") + }) - ginkgo.It("should return status code 401 when authentication is configured but Authorization header is not configured", func() { + ginkgo.It("should allow ingress based on their risk on webhooks", func() { host := "annotation-validations" - // Allow cross namespace consumption - f.UpdateNginxConfigMapData("allow-cross-namespace-resources", "true") + + // Low and Medium Risk annotations should be allowed, the rest should be denied + f.UpdateNginxConfigMapData("annotations-risk", "Medium") // Sleep a while just to guarantee that the configmap is applied framework.Sleep() - s := f.EnsureSecret(buildSecret("foo", "bar", "test", "otherns")) - annotations := map[string]string{ - "nginx.ingress.kubernetes.io/auth-type": "basic", - "nginx.ingress.kubernetes.io/auth-secret": fmt.Sprintf("%s/%s", s.Namespace, s.Name), - "nginx.ingress.kubernetes.io/auth-realm": "test auth", + "nginx.ingress.kubernetes.io/default-backend": "default/bla", // low risk + "nginx.ingress.kubernetes.io/denylist-source-range": "1.1.1.1/32", // medium risk } + ginkgo.By("allow ingress with low/medium risk annotations") ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) - f.EnsureIngress(ing) - - f.WaitForNginxServer(host, - func(server string) bool { - return strings.Contains(server, "server_name annotation-validations") - }) - - f.HTTPTestClient(). - GET("/"). - WithHeader("Host", host). - Expect(). - Status(http.StatusUnauthorized). - Body().Contains("401 Authorization Required") - - f.HTTPTestClient(). - GET("/"). - WithHeader("Host", host). - WithBasicAuth("foo", "bar"). - Expect(). - Status(http.StatusOK) + _, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), ing, metav1.CreateOptions{}) + assert.Nil(ginkgo.GinkgoT(), err, "creating ingress with allowed annotations should not trigger an error") + + ginkgo.By("block ingress with risky annotations") + annotations["nginx.ingress.kubernetes.io/modsecurity-transaction-id"] = "bla123" // High should be blocked + annotations["nginx.ingress.kubernetes.io/modsecurity-snippet"] = "some random stuff;" // High should be blocked + ing = framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + _, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Update(context.TODO(), ing, metav1.UpdateOptions{}) + assert.NotNil(ginkgo.GinkgoT(), err, "creating ingress with risky annotations should trigger an error") + }) }) From 30bd4522559bdbe39ceeb8b8f1ffffb17f347c01 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Tue, 20 Jun 2023 06:44:04 -0300 Subject: [PATCH 07/14] Enable validation by default on tests --- internal/ingress/annotations/parser/main.go | 2 +- pkg/flags/flags.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ingress/annotations/parser/main.go b/internal/ingress/annotations/parser/main.go index e10c373f16..d517cbc573 100644 --- a/internal/ingress/annotations/parser/main.go +++ b/internal/ingress/annotations/parser/main.go @@ -31,7 +31,7 @@ import ( // DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller const ( DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io" - DefaultDisableAnnotationValidation = true + DefaultDisableAnnotationValidation = false ) var ( diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index d5f95597cb..b629b9332a 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -152,7 +152,7 @@ Requires the update-status parameter.`) annotationsPrefix = flags.String("annotations-prefix", parser.DefaultAnnotationsPrefix, `Prefix of the Ingress annotations specific to the NGINX controller.`) - disableAnnotationValidation = flags.Bool("disable-annotation-validation", parser.DefaultDisableAnnotationValidation, + disableAnnotationValidation = flags.Bool("disable-annotation-validation", true, `Prefix of the Ingress annotations specific to the NGINX controller.`) enableSSLChainCompletion = flags.Bool("enable-ssl-chain-completion", false, From 35748e95d278ec660411e063238bd5cf8bf35da7 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Tue, 20 Jun 2023 09:36:52 -0300 Subject: [PATCH 08/14] Test validation flag --- .../ingress/annotations/alias/main_test.go | 30 ++++++++++++------- .../ingress/annotations/parser/validators.go | 2 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/internal/ingress/annotations/alias/main_test.go b/internal/ingress/annotations/alias/main_test.go index 6860c6cb69..e212f3cf4d 100644 --- a/internal/ingress/annotations/alias/main_test.go +++ b/internal/ingress/annotations/alias/main_test.go @@ -36,18 +36,20 @@ func TestParse(t *testing.T) { } testCases := []struct { - annotations map[string]string - expected []string - wantErr bool + annotations map[string]string + expected []string + skipValidation bool + wantErr bool }{ - {map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}, false}, - {map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}, false}, - {map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}, false}, - {map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}, false}, - {map[string]string{annotation: `www.xpto;lala`}, []string{}, true}, - {map[string]string{annotation: ""}, []string{}, true}, - {map[string]string{}, []string{}, true}, - {nil, []string{}, true}, + {map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}, false, false}, + {map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}, false, false}, + {map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}, false, false}, + {map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}, false, false}, + {map[string]string{annotation: `www.xpto;lala`}, []string{}, false, true}, + {map[string]string{annotation: `www.xpto;lala`}, []string{"www.xpto;lala"}, true, false}, // When we skip validation no error should happen + {map[string]string{annotation: ""}, []string{}, false, true}, + {map[string]string{}, []string{}, false, true}, + {nil, []string{}, false, true}, } ing := &networking.Ingress{ @@ -60,6 +62,12 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) + if testCase.skipValidation { + parser.DisableAnnotationValidation = true + } + defer func() { + parser.DisableAnnotationValidation = false + }() result, err := ap.Parse(ing) if (err != nil) != testCase.wantErr { t.Errorf("ParseAliasAnnotation() annotation: %s, error = %v, wantErr %v", testCase.annotations, err, testCase.wantErr) diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go index f9ac5f6d0a..73154cf0a5 100644 --- a/internal/ingress/annotations/parser/validators.go +++ b/internal/ingress/annotations/parser/validators.go @@ -214,7 +214,7 @@ func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFiel } } // We don't run validation against empty values - if annotationValue != "" && !DisableAnnotationValidation { + if !DisableAnnotationValidation && annotationValue != "" { if err := validateFunc(annotationValue); err != nil { klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue) return "", ing_errors.NewValidationError(annotationFullName) From ecfe183d7a766f83961a2863080496a06e95fa17 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Thu, 6 Jul 2023 11:31:56 +0000 Subject: [PATCH 09/14] remove ajp from list --- internal/ingress/annotations/backendprotocol/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ingress/annotations/backendprotocol/main.go b/internal/ingress/annotations/backendprotocol/main.go index f5d287f024..7060e2acda 100644 --- a/internal/ingress/annotations/backendprotocol/main.go +++ b/internal/ingress/annotations/backendprotocol/main.go @@ -26,7 +26,7 @@ import ( ) var ( - validProtocols = []string{"auto_http", "http", "https", "ajp", "grpc", "grpcs", "fcgi"} + validProtocols = []string{"auto_http", "http", "https", "grpc", "grpcs", "fcgi"} ) const ( From 490d0f41962146bcaffaf9f3f3c194db05faa11e Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Thu, 6 Jul 2023 12:14:53 +0000 Subject: [PATCH 10/14] Finalize validation changes --- docs/user-guide/cli-arguments.md | 1 + .../nginx-configuration/configmap.md | 26 +++++++++++++++++++ .../ingress/annotations/parser/validators.go | 4 ++- internal/ingress/controller/config/config.go | 2 +- pkg/flags/flags.go | 2 +- test/e2e-image/e2e.sh | 2 +- 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/user-guide/cli-arguments.md b/docs/user-guide/cli-arguments.md index bc0894a52f..bbd69f3595 100644 --- a/docs/user-guide/cli-arguments.md +++ b/docs/user-guide/cli-arguments.md @@ -15,6 +15,7 @@ They are set in the container spec of the `ingress-nginx-controller` Deployment | `--default-backend-service` | Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form "namespace/name". The controller configures NGINX to forward requests to the first port of this Service. | | `--default-server-port` | Port to use for exposing the default server (catch-all). (default 8181) | | `--default-ssl-certificate` | Secret containing a SSL certificate to be used by the default HTTPS server (catch-all). Takes the form "namespace/name". | +| `--disable-annotation-validation` | If true, will disable the annotation validation feature. This value will be defaulted to false on a future release. | | `--disable-catch-all` | Disable support for catch-all Ingresses. (default false) | | `--disable-full-test` | Disable full test of all merged ingresses at the admission stage and tests the template of the ingress being created or updated (full test of all ingresses is enabled by default). | | `--disable-svc-external-name` | Disable support for Services of type ExternalName. (default false) | diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md index c55b7502a5..07da0bb772 100644 --- a/docs/user-guide/nginx-configuration/configmap.md +++ b/docs/user-guide/nginx-configuration/configmap.md @@ -29,7 +29,9 @@ The following table shows a configuration option's name, type, and the default v |:---|:---|:------|:----| |[add-headers](#add-headers)|string|""|| |[allow-backend-server-header](#allow-backend-server-header)|bool|"false"|| +|[allow-cross-namespace-resources](#allow-cross-namespace-resources)|bool|"true"|| |[allow-snippet-annotations](#allow-snippet-annotations)|bool|true|| +|[annotation-risk](#annotation-risk)|string|Critical|| |[annotation-value-word-blocklist](#annotation-value-word-blocklist)|string array|""|| |[hide-headers](#hide-headers)|string array|empty|| |[access-log-params](#access-log-params)|string|""|| @@ -239,6 +241,20 @@ Sets custom headers from named configmap before sending traffic to the client. S Enables the return of the header Server from the backend instead of the generic nginx string. _**default:**_ is disabled +## allow-cross-namespace-resources + +Enables users to consume cross namespace resource on annotations, when was previously enabled . _**default:**_ true + +**Annotations that may be impacted with this change**: +* `auth-secret` +* `auth-proxy-set-header` +* `auth-tls-secret` +* `fastcgi-params-configmap` +* `proxy-ssl-secret` + + +**This option will be defaulted to false in the next major release** + ## allow-snippet-annotations Enables Ingress to parse and add *-snippet annotations/directives created by the user. _**default:**_ `true` @@ -246,6 +262,16 @@ Enables Ingress to parse and add *-snippet annotations/directives created by the Warning: We recommend enabling this option only if you TRUST users with permission to create Ingress objects, as this may allow a user to add restricted configurations to the final nginx.conf file +**This option will be defaulted to false in the next major release** + +## annotation-risk + +Represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations with risk High and Critical will not be accepted. + +Accepted values are `Critical`, `High`, `Medium` and `Low`. + +Defaults to `Critical` but will be changed to `High` on the next minor release + ## annotation-value-word-blocklist Contains a comma-separated value of chars/words that are well known of being used to abuse Ingress configuration diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go index 73154cf0a5..8ecf18adda 100644 --- a/internal/ingress/annotations/parser/validators.go +++ b/internal/ingress/annotations/parser/validators.go @@ -90,6 +90,8 @@ func ValidateArrayOfServerName(value string) error { return nil } +// ValidateServerName validates if the passed value is an acceptable server name. The server name +// can contain regex characters, as those are accepted values on nginx configuration func ValidateServerName(value string) error { value = strings.TrimSpace(value) if !IsValidRegex.MatchString(value) { @@ -144,7 +146,7 @@ func ValidateInt(value string) error { return err } -// ValidateInt validates if the specified value is an array of IPs and CIDRs +// ValidateCIDRs validates if the specified value is an array of IPs and CIDRs func ValidateCIDRs(value string) error { _, err := net.ParseCIDRs(value) return err diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go index f3ea6d5a65..adecd5ddff 100644 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -100,7 +100,7 @@ type Configuration struct { // AllowCrossNamespaceResources enables users to consume cross namespace resource on annotations // Case disabled, attempts to use secrets or configmaps from a namespace different from Ingress will // be denied - // This valid will default to `false` on future releases + // This value will default to `false` on future releases AllowCrossNamespaceResources bool `json:"allow-cross-namespace-resources"` // AnnotationsRisk represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index b629b9332a..7402218e34 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -153,7 +153,7 @@ Requires the update-status parameter.`) `Prefix of the Ingress annotations specific to the NGINX controller.`) disableAnnotationValidation = flags.Bool("disable-annotation-validation", true, - `Prefix of the Ingress annotations specific to the NGINX controller.`) + `If true, will disable the annotation validation feature. This value will be defaulted to false on a future release`) enableSSLChainCompletion = flags.Bool("enable-ssl-chain-completion", false, `Autocomplete SSL certificate chains with missing intermediate CA certificates. diff --git a/test/e2e-image/e2e.sh b/test/e2e-image/e2e.sh index 5287b4a7f4..f8ecd5337d 100755 --- a/test/e2e-image/e2e.sh +++ b/test/e2e-image/e2e.sh @@ -24,7 +24,7 @@ E2E_CHECK_LEAKS=${E2E_CHECK_LEAKS:-""} reportFile="report-e2e-test-suite.xml" ginkgo_args=( -# "--fail-fast" + "--fail-fast" "--flake-attempts=2" "--junit-report=${reportFile}" "--nodes=${E2E_NODES}" From 68d8d643c3c15113f144f09a31acd991746d1e15 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Sun, 9 Jul 2023 17:45:43 +0000 Subject: [PATCH 11/14] Add validations to CI --- .github/workflows/ci.yaml | 49 +++++++++++++++++++ charts/ingress-nginx/templates/_params.tpl | 3 ++ charts/ingress-nginx/values.yaml | 1 + docs/user-guide/cli-arguments.md | 2 +- .../ingress/annotations/alias/main_test.go | 4 +- internal/ingress/annotations/parser/main.go | 8 +-- .../ingress/annotations/parser/validators.go | 2 +- pkg/flags/flags.go | 6 +-- test/e2e/framework/exec.go | 7 ++- test/e2e/run-e2e-suite.sh | 1 + test/e2e/run-kind-e2e.sh | 1 + test/e2e/wait-for-nginx.sh | 2 + 12 files changed, 74 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 097e77276e..4537753798 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -319,6 +319,55 @@ jobs: name: e2e-test-reports-${{ matrix.k8s }} path: 'test/junitreports/report*.xml' + kubernetes-validations: + name: Kubernetes with Validations + runs-on: ubuntu-latest + needs: + - changes + - build + if: | + (needs.changes.outputs.go == 'true') || ${{ inputs.run_e2e }} + + strategy: + matrix: + k8s: [v1.27.1] + + steps: + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: cache + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: docker.tar.gz + + - name: Create Kubernetes ${{ matrix.k8s }} cluster + id: kind + run: | + kind create cluster --image=kindest/node:${{ matrix.k8s }} --config test/e2e/kind.yaml + + - name: Load images from cache + run: | + echo "loading docker images..." + pigz -dc docker.tar.gz | docker load + + - name: Run e2e tests + env: + KIND_CLUSTER_NAME: kind + SKIP_CLUSTER_CREATION: true + SKIP_IMAGE_CREATION: true + ENABLE_VALIDATIONS: true + run: | + kind get kubeconfig > $HOME/.kube/kind-config-kind + make kind-e2e-test + + - name: Upload e2e junit-reports + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + if: success() || failure() + with: + name: e2e-test-reports-${{ matrix.k8s }} + path: 'test/junitreports/report*.xml' + kubernetes-chroot: name: Kubernetes chroot diff --git a/charts/ingress-nginx/templates/_params.tpl b/charts/ingress-nginx/templates/_params.tpl index a1aef01aeb..6358b49f74 100644 --- a/charts/ingress-nginx/templates/_params.tpl +++ b/charts/ingress-nginx/templates/_params.tpl @@ -1,5 +1,8 @@ {{- define "ingress-nginx.params" -}} - /nginx-ingress-controller +{{- if .Values.controller.enableValidations }} +- --enable-annotation-validation=true +{{- end }} {{- if .Values.defaultBackend.enabled }} - --default-backend-service=$(POD_NAMESPACE)/{{ include "ingress-nginx.defaultBackend.fullname" . }} {{- end }} diff --git a/charts/ingress-nginx/values.yaml b/charts/ingress-nginx/values.yaml index d091391a8c..90edac84ca 100644 --- a/charts/ingress-nginx/values.yaml +++ b/charts/ingress-nginx/values.yaml @@ -15,6 +15,7 @@ commonLabels: {} controller: name: controller + enableValidations: false image: ## Keep false as default for now! chroot: false diff --git a/docs/user-guide/cli-arguments.md b/docs/user-guide/cli-arguments.md index bbd69f3595..be6e1dd504 100644 --- a/docs/user-guide/cli-arguments.md +++ b/docs/user-guide/cli-arguments.md @@ -15,7 +15,7 @@ They are set in the container spec of the `ingress-nginx-controller` Deployment | `--default-backend-service` | Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form "namespace/name". The controller configures NGINX to forward requests to the first port of this Service. | | `--default-server-port` | Port to use for exposing the default server (catch-all). (default 8181) | | `--default-ssl-certificate` | Secret containing a SSL certificate to be used by the default HTTPS server (catch-all). Takes the form "namespace/name". | -| `--disable-annotation-validation` | If true, will disable the annotation validation feature. This value will be defaulted to false on a future release. | +| `--enable-annotation-validation` | If true, will enable the annotation validation feature. This value will be defaulted to true on a future release. | | `--disable-catch-all` | Disable support for catch-all Ingresses. (default false) | | `--disable-full-test` | Disable full test of all merged ingresses at the admission stage and tests the template of the ingress being created or updated (full test of all ingresses is enabled by default). | | `--disable-svc-external-name` | Disable support for Services of type ExternalName. (default false) | diff --git a/internal/ingress/annotations/alias/main_test.go b/internal/ingress/annotations/alias/main_test.go index e212f3cf4d..1965f2630b 100644 --- a/internal/ingress/annotations/alias/main_test.go +++ b/internal/ingress/annotations/alias/main_test.go @@ -63,10 +63,10 @@ func TestParse(t *testing.T) { for _, testCase := range testCases { ing.SetAnnotations(testCase.annotations) if testCase.skipValidation { - parser.DisableAnnotationValidation = true + parser.EnableAnnotationValidation = false } defer func() { - parser.DisableAnnotationValidation = false + parser.EnableAnnotationValidation = true }() result, err := ap.Parse(ing) if (err != nil) != testCase.wantErr { diff --git a/internal/ingress/annotations/parser/main.go b/internal/ingress/annotations/parser/main.go index d517cbc573..951970e270 100644 --- a/internal/ingress/annotations/parser/main.go +++ b/internal/ingress/annotations/parser/main.go @@ -30,15 +30,15 @@ import ( // DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller const ( - DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io" - DefaultDisableAnnotationValidation = false + DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io" + DefaultEnableAnnotationValidation = true ) var ( // AnnotationsPrefix is the mutable attribute that the controller explicitly refers to AnnotationsPrefix = DefaultAnnotationsPrefix - // DisableAnnotationValidation is the mutable attribute for enabling or disabling the validation functions - DisableAnnotationValidation = DefaultDisableAnnotationValidation + // Enable is the mutable attribute for enabling or disabling the validation functions + EnableAnnotationValidation = DefaultEnableAnnotationValidation ) // AnnotationGroup defines the group that this annotation may belong diff --git a/internal/ingress/annotations/parser/validators.go b/internal/ingress/annotations/parser/validators.go index 8ecf18adda..e14b486eb1 100644 --- a/internal/ingress/annotations/parser/validators.go +++ b/internal/ingress/annotations/parser/validators.go @@ -216,7 +216,7 @@ func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFiel } } // We don't run validation against empty values - if !DisableAnnotationValidation && annotationValue != "" { + if EnableAnnotationValidation && annotationValue != "" { if err := validateFunc(annotationValue); err != nil { klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue) return "", ing_errors.NewValidationError(annotationFullName) diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index 7402218e34..2c926a35f2 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -152,8 +152,8 @@ Requires the update-status parameter.`) annotationsPrefix = flags.String("annotations-prefix", parser.DefaultAnnotationsPrefix, `Prefix of the Ingress annotations specific to the NGINX controller.`) - disableAnnotationValidation = flags.Bool("disable-annotation-validation", true, - `If true, will disable the annotation validation feature. This value will be defaulted to false on a future release`) + enableAnnotationValidation = flags.Bool("enable-annotation-validation", false, + `If true, will enable the annotation validation feature. This value will be defaulted to true on a future release`) enableSSLChainCompletion = flags.Bool("enable-ssl-chain-completion", false, `Autocomplete SSL certificate chains with missing intermediate CA certificates. @@ -252,7 +252,7 @@ https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-g } parser.AnnotationsPrefix = *annotationsPrefix - parser.DisableAnnotationValidation = *disableAnnotationValidation + parser.EnableAnnotationValidation = *enableAnnotationValidation // check port collisions if !ing_net.IsPortAvailable(*httpPort) { diff --git a/test/e2e/framework/exec.go b/test/e2e/framework/exec.go index d91d36551f..84371e4a39 100644 --- a/test/e2e/framework/exec.go +++ b/test/e2e/framework/exec.go @@ -116,7 +116,12 @@ func (f *Framework) newIngressController(namespace string, namespaceOverlay stri if !ok { isChroot = "false" } - cmd := exec.Command("./wait-for-nginx.sh", namespace, namespaceOverlay, isChroot) + + enableValidations, ok := os.LookupEnv("ENABLE_VALIDATIONS") + if !ok { + enableValidations = "false" + } + cmd := exec.Command("./wait-for-nginx.sh", namespace, namespaceOverlay, isChroot, enableValidations) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("unexpected error waiting for ingress controller deployment: %v.\nLogs:\n%v", err, string(out)) diff --git a/test/e2e/run-e2e-suite.sh b/test/e2e/run-e2e-suite.sh index b56312afd5..015895e568 100755 --- a/test/e2e/run-e2e-suite.sh +++ b/test/e2e/run-e2e-suite.sh @@ -78,6 +78,7 @@ kubectl run --rm \ --env="E2E_NODES=${E2E_NODES}" \ --env="FOCUS=${FOCUS}" \ --env="IS_CHROOT=${IS_CHROOT:-false}"\ + --env="ENABLE_VALIDATIONS=${ENABLE_VALIDATIONS:-false}"\ --env="E2E_CHECK_LEAKS=${E2E_CHECK_LEAKS}" \ --env="NGINX_BASE_IMAGE=${NGINX_BASE_IMAGE}" \ --env="HTTPBUN_IMAGE=${HTTPBUN_IMAGE}" \ diff --git a/test/e2e/run-kind-e2e.sh b/test/e2e/run-kind-e2e.sh index e6e4e086bd..4dc1bddd02 100755 --- a/test/e2e/run-kind-e2e.sh +++ b/test/e2e/run-kind-e2e.sh @@ -39,6 +39,7 @@ fi KIND_LOG_LEVEL="1" IS_CHROOT="${IS_CHROOT:-false}" +ENABLE_VALIDATIONS="${ENABLE_VALIDATIONS:-false}" export KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME:-ingress-nginx-dev} DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Use 1.0.0-dev to make sure we use the latest configuration in the helm template diff --git a/test/e2e/wait-for-nginx.sh b/test/e2e/wait-for-nginx.sh index 153d348c2b..7ed97bb9cc 100755 --- a/test/e2e/wait-for-nginx.sh +++ b/test/e2e/wait-for-nginx.sh @@ -24,6 +24,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export NAMESPACE=$1 export NAMESPACE_OVERLAY=$2 export IS_CHROOT=$3 +export ENABLE_VALIDATIONS=$4 echo "deploying NGINX Ingress controller in namespace $NAMESPACE" @@ -68,6 +69,7 @@ else # TODO: remove the need to use fullnameOverride fullnameOverride: nginx-ingress controller: + enableValidations: ${ENABLE_VALIDATIONS} image: repository: ingress-controller/controller chroot: ${IS_CHROOT} From 007e18bc6638934710758fec0ba45673bd26eb07 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Sun, 9 Jul 2023 18:54:38 +0000 Subject: [PATCH 12/14] Update helm docs --- charts/ingress-nginx/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/ingress-nginx/README.md b/charts/ingress-nginx/README.md index 9550918732..a28c658aac 100644 --- a/charts/ingress-nginx/README.md +++ b/charts/ingress-nginx/README.md @@ -296,6 +296,7 @@ As of version `1.26.0` of this chart, by simply not providing any clusterIP valu | controller.electionID | string | `""` | Election ID to use for status update, by default it uses the controller name combined with a suffix of 'leader' | | controller.enableMimalloc | bool | `true` | Enable mimalloc as a drop-in replacement for malloc. # ref: https://github.com/microsoft/mimalloc # | | controller.enableTopologyAwareRouting | bool | `false` | This configuration enables Topology Aware Routing feature, used together with service annotation service.kubernetes.io/topology-aware-hints="auto" Defaults to false | +| controller.enableValidations | bool | `false` | | | controller.existingPsp | string | `""` | Use an existing PSP instead of creating one | | controller.extraArgs | object | `{}` | Additional command line arguments to pass to Ingress-Nginx Controller E.g. to specify the default SSL certificate you can use | | controller.extraContainers | list | `[]` | Additional containers to be added to the controller pod. See https://github.com/lemonldap-ng-controller/lemonldap-ng-controller as example. | From 65997ac6d029059a9a6b3e1b122ba182eb97fa2c Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Sun, 16 Jul 2023 19:51:25 +0000 Subject: [PATCH 13/14] Fix code review --- charts/ingress-nginx/README.md | 2 +- charts/ingress-nginx/templates/_params.tpl | 2 +- charts/ingress-nginx/values.yaml | 2 +- internal/ingress/annotations/http2pushpreload/main.go | 2 +- test/e2e/framework/exec.go | 6 +++--- test/e2e/wait-for-nginx.sh | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/ingress-nginx/README.md b/charts/ingress-nginx/README.md index a28c658aac..950137d36f 100644 --- a/charts/ingress-nginx/README.md +++ b/charts/ingress-nginx/README.md @@ -294,9 +294,9 @@ As of version `1.26.0` of this chart, by simply not providing any clusterIP valu | controller.dnsConfig | object | `{}` | Optionally customize the pod dnsConfig. | | controller.dnsPolicy | string | `"ClusterFirst"` | Optionally change this to ClusterFirstWithHostNet in case you have 'hostNetwork: true'. By default, while using host network, name resolution uses the host's DNS. If you wish nginx-controller to keep resolving names inside the k8s network, use ClusterFirstWithHostNet. | | controller.electionID | string | `""` | Election ID to use for status update, by default it uses the controller name combined with a suffix of 'leader' | +| controller.enableAnnotationValidations | bool | `false` | | | controller.enableMimalloc | bool | `true` | Enable mimalloc as a drop-in replacement for malloc. # ref: https://github.com/microsoft/mimalloc # | | controller.enableTopologyAwareRouting | bool | `false` | This configuration enables Topology Aware Routing feature, used together with service annotation service.kubernetes.io/topology-aware-hints="auto" Defaults to false | -| controller.enableValidations | bool | `false` | | | controller.existingPsp | string | `""` | Use an existing PSP instead of creating one | | controller.extraArgs | object | `{}` | Additional command line arguments to pass to Ingress-Nginx Controller E.g. to specify the default SSL certificate you can use | | controller.extraContainers | list | `[]` | Additional containers to be added to the controller pod. See https://github.com/lemonldap-ng-controller/lemonldap-ng-controller as example. | diff --git a/charts/ingress-nginx/templates/_params.tpl b/charts/ingress-nginx/templates/_params.tpl index 6358b49f74..47d024e857 100644 --- a/charts/ingress-nginx/templates/_params.tpl +++ b/charts/ingress-nginx/templates/_params.tpl @@ -1,6 +1,6 @@ {{- define "ingress-nginx.params" -}} - /nginx-ingress-controller -{{- if .Values.controller.enableValidations }} +{{- if .Values.controller.enableAnnotationValidations }} - --enable-annotation-validation=true {{- end }} {{- if .Values.defaultBackend.enabled }} diff --git a/charts/ingress-nginx/values.yaml b/charts/ingress-nginx/values.yaml index 90edac84ca..1f5c9180da 100644 --- a/charts/ingress-nginx/values.yaml +++ b/charts/ingress-nginx/values.yaml @@ -15,7 +15,7 @@ commonLabels: {} controller: name: controller - enableValidations: false + enableAnnotationValidations: false image: ## Keep false as default for now! chroot: false diff --git a/internal/ingress/annotations/http2pushpreload/main.go b/internal/ingress/annotations/http2pushpreload/main.go index 36441c3013..b7c3287e37 100644 --- a/internal/ingress/annotations/http2pushpreload/main.go +++ b/internal/ingress/annotations/http2pushpreload/main.go @@ -28,7 +28,7 @@ const ( ) var http2PushPreloadAnnotations = parser.Annotation{ - Group: "", // TODO: TBD + Group: "http2", Annotations: parser.AnnotationFields{ http2PushPreloadAnnotation: { Validator: parser.ValidateBool, diff --git a/test/e2e/framework/exec.go b/test/e2e/framework/exec.go index 84371e4a39..07bf09be80 100644 --- a/test/e2e/framework/exec.go +++ b/test/e2e/framework/exec.go @@ -117,11 +117,11 @@ func (f *Framework) newIngressController(namespace string, namespaceOverlay stri isChroot = "false" } - enableValidations, ok := os.LookupEnv("ENABLE_VALIDATIONS") + enableAnnotationValidations, ok := os.LookupEnv("ENABLE_VALIDATIONS") if !ok { - enableValidations = "false" + enableAnnotationValidations = "false" } - cmd := exec.Command("./wait-for-nginx.sh", namespace, namespaceOverlay, isChroot, enableValidations) + cmd := exec.Command("./wait-for-nginx.sh", namespace, namespaceOverlay, isChroot, enableAnnotationValidations) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("unexpected error waiting for ingress controller deployment: %v.\nLogs:\n%v", err, string(out)) diff --git a/test/e2e/wait-for-nginx.sh b/test/e2e/wait-for-nginx.sh index 7ed97bb9cc..0726bde109 100755 --- a/test/e2e/wait-for-nginx.sh +++ b/test/e2e/wait-for-nginx.sh @@ -69,7 +69,7 @@ else # TODO: remove the need to use fullnameOverride fullnameOverride: nginx-ingress controller: - enableValidations: ${ENABLE_VALIDATIONS} + enableAnnotationValidations: ${ENABLE_VALIDATIONS} image: repository: ingress-controller/controller chroot: ${IS_CHROOT} From 7ce93ae0c490ea590e2b1c8c722a946fcfaeabf7 Mon Sep 17 00:00:00 2001 From: Ricardo Katz Date: Thu, 20 Jul 2023 15:05:03 +0000 Subject: [PATCH 14/14] Use a better name for annotation risk --- docs/user-guide/nginx-configuration/configmap.md | 4 ++-- internal/ingress/annotations/alias/main.go | 2 +- internal/ingress/annotations/auth/main.go | 2 +- internal/ingress/annotations/authreq/main.go | 2 +- internal/ingress/annotations/authreqglobal/main.go | 2 +- internal/ingress/annotations/authtls/main.go | 2 +- internal/ingress/annotations/backendprotocol/main.go | 2 +- internal/ingress/annotations/canary/main.go | 2 +- .../ingress/annotations/clientbodybuffersize/main.go | 2 +- internal/ingress/annotations/connection/main.go | 2 +- internal/ingress/annotations/cors/main.go | 2 +- internal/ingress/annotations/customhttperrors/main.go | 2 +- internal/ingress/annotations/defaultbackend/main.go | 2 +- internal/ingress/annotations/fastcgi/main.go | 2 +- internal/ingress/annotations/globalratelimit/main.go | 2 +- internal/ingress/annotations/http2pushpreload/main.go | 2 +- internal/ingress/annotations/ipallowlist/main.go | 2 +- internal/ingress/annotations/ipdenylist/main.go | 2 +- internal/ingress/annotations/loadbalancing/main.go | 2 +- internal/ingress/annotations/log/main.go | 2 +- internal/ingress/annotations/mirror/main.go | 2 +- internal/ingress/annotations/modsecurity/main.go | 2 +- internal/ingress/annotations/opentelemetry/main.go | 2 +- internal/ingress/annotations/opentracing/main.go | 2 +- internal/ingress/annotations/portinredirect/main.go | 2 +- internal/ingress/annotations/proxy/main.go | 2 +- internal/ingress/annotations/proxyssl/main.go | 2 +- internal/ingress/annotations/ratelimit/main.go | 2 +- internal/ingress/annotations/redirect/redirect.go | 2 +- internal/ingress/annotations/rewrite/main.go | 2 +- internal/ingress/annotations/satisfy/main.go | 2 +- internal/ingress/annotations/serversnippet/main.go | 2 +- internal/ingress/annotations/serviceupstream/main.go | 2 +- internal/ingress/annotations/sessionaffinity/main.go | 2 +- internal/ingress/annotations/snippet/main.go | 2 +- internal/ingress/annotations/sslcipher/main.go | 2 +- internal/ingress/annotations/sslpassthrough/main.go | 2 +- internal/ingress/annotations/streamsnippet/main.go | 2 +- internal/ingress/annotations/upstreamhashby/main.go | 2 +- internal/ingress/annotations/upstreamvhost/main.go | 2 +- internal/ingress/annotations/xforwardedprefix/main.go | 2 +- internal/ingress/controller/config/config.go | 8 ++++---- internal/ingress/controller/controller_test.go | 2 +- internal/ingress/controller/store/store.go | 2 +- internal/ingress/defaults/main.go | 4 ++-- internal/ingress/resolver/mock.go | 10 +++++----- test/e2e/settings/validations/validations.go | 4 ++-- 47 files changed, 57 insertions(+), 57 deletions(-) diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md index 07da0bb772..0a7e44dce4 100644 --- a/docs/user-guide/nginx-configuration/configmap.md +++ b/docs/user-guide/nginx-configuration/configmap.md @@ -31,7 +31,7 @@ The following table shows a configuration option's name, type, and the default v |[allow-backend-server-header](#allow-backend-server-header)|bool|"false"|| |[allow-cross-namespace-resources](#allow-cross-namespace-resources)|bool|"true"|| |[allow-snippet-annotations](#allow-snippet-annotations)|bool|true|| -|[annotation-risk](#annotation-risk)|string|Critical|| +|[annotations-risk-level](#annotations-risk-level)|string|Critical|| |[annotation-value-word-blocklist](#annotation-value-word-blocklist)|string array|""|| |[hide-headers](#hide-headers)|string array|empty|| |[access-log-params](#access-log-params)|string|""|| @@ -264,7 +264,7 @@ may allow a user to add restricted configurations to the final nginx.conf file **This option will be defaulted to false in the next major release** -## annotation-risk +## annotations-risk-level Represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations with risk High and Critical will not be accepted. diff --git a/internal/ingress/annotations/alias/main.go b/internal/ingress/annotations/alias/main.go index 409ed7a772..4a5e6f1883 100644 --- a/internal/ingress/annotations/alias/main.go +++ b/internal/ingress/annotations/alias/main.go @@ -88,6 +88,6 @@ func (a alias) Parse(ing *networking.Ingress) (interface{}, error) { } func (a alias) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, aliasAnnotation.Annotations) } diff --git a/internal/ingress/annotations/auth/main.go b/internal/ingress/annotations/auth/main.go index 37c6a0d31c..beecebdb1b 100644 --- a/internal/ingress/annotations/auth/main.go +++ b/internal/ingress/annotations/auth/main.go @@ -277,6 +277,6 @@ func (a auth) GetDocumentation() parser.AnnotationFields { } func (a auth) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, authSecretAnnotations.Annotations) } diff --git a/internal/ingress/annotations/authreq/main.go b/internal/ingress/annotations/authreq/main.go index 099e1ec6d5..2ab98ace0b 100644 --- a/internal/ingress/annotations/authreq/main.go +++ b/internal/ingress/annotations/authreq/main.go @@ -504,6 +504,6 @@ func (a authReq) GetDocumentation() parser.AnnotationFields { } func (a authReq) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, authReqAnnotations.Annotations) } diff --git a/internal/ingress/annotations/authreqglobal/main.go b/internal/ingress/annotations/authreqglobal/main.go index 6032dc5953..a1641e085c 100644 --- a/internal/ingress/annotations/authreqglobal/main.go +++ b/internal/ingress/annotations/authreqglobal/main.go @@ -69,6 +69,6 @@ func (a authReqGlobal) GetDocumentation() parser.AnnotationFields { } func (a authReqGlobal) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, globalAuthAnnotations.Annotations) } diff --git a/internal/ingress/annotations/authtls/main.go b/internal/ingress/annotations/authtls/main.go index 53ee83fa80..5d6763e8bc 100644 --- a/internal/ingress/annotations/authtls/main.go +++ b/internal/ingress/annotations/authtls/main.go @@ -217,6 +217,6 @@ func (a authTLS) GetDocumentation() parser.AnnotationFields { } func (a authTLS) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, authTLSAnnotations.Annotations) } diff --git a/internal/ingress/annotations/backendprotocol/main.go b/internal/ingress/annotations/backendprotocol/main.go index 7060e2acda..2704ce9f62 100644 --- a/internal/ingress/annotations/backendprotocol/main.go +++ b/internal/ingress/annotations/backendprotocol/main.go @@ -83,6 +83,6 @@ func (a backendProtocol) Parse(ing *networking.Ingress) (interface{}, error) { } func (a backendProtocol) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, backendProtocolConfig.Annotations) } diff --git a/internal/ingress/annotations/canary/main.go b/internal/ingress/annotations/canary/main.go index 9e688a118e..119f091819 100644 --- a/internal/ingress/annotations/canary/main.go +++ b/internal/ingress/annotations/canary/main.go @@ -190,6 +190,6 @@ func (c canary) GetDocumentation() parser.AnnotationFields { } func (a canary) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, CanaryAnnotations.Annotations) } diff --git a/internal/ingress/annotations/clientbodybuffersize/main.go b/internal/ingress/annotations/clientbodybuffersize/main.go index 61094d0c6e..aa1485df29 100644 --- a/internal/ingress/annotations/clientbodybuffersize/main.go +++ b/internal/ingress/annotations/clientbodybuffersize/main.go @@ -66,6 +66,6 @@ func (cbbs clientBodyBufferSize) Parse(ing *networking.Ingress) (interface{}, er } func (a clientBodyBufferSize) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, clientBodyBufferSizeConfig.Annotations) } diff --git a/internal/ingress/annotations/connection/main.go b/internal/ingress/annotations/connection/main.go index eaf83b4e00..9e96b6ab10 100644 --- a/internal/ingress/annotations/connection/main.go +++ b/internal/ingress/annotations/connection/main.go @@ -102,6 +102,6 @@ func (a connection) GetDocumentation() parser.AnnotationFields { } func (a connection) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, connectionHeadersAnnotations.Annotations) } diff --git a/internal/ingress/annotations/cors/main.go b/internal/ingress/annotations/cors/main.go index 442e70158a..cc30b8405d 100644 --- a/internal/ingress/annotations/cors/main.go +++ b/internal/ingress/annotations/cors/main.go @@ -261,6 +261,6 @@ func (c cors) GetDocumentation() parser.AnnotationFields { } func (a cors) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, corsAnnotation.Annotations) } diff --git a/internal/ingress/annotations/customhttperrors/main.go b/internal/ingress/annotations/customhttperrors/main.go index b2623b0236..c3c9b5be3f 100644 --- a/internal/ingress/annotations/customhttperrors/main.go +++ b/internal/ingress/annotations/customhttperrors/main.go @@ -89,6 +89,6 @@ func (e customhttperrors) GetDocumentation() parser.AnnotationFields { } func (a customhttperrors) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, customHTTPErrorsAnnotations.Annotations) } diff --git a/internal/ingress/annotations/defaultbackend/main.go b/internal/ingress/annotations/defaultbackend/main.go index 0dae649055..f3ca004dd8 100644 --- a/internal/ingress/annotations/defaultbackend/main.go +++ b/internal/ingress/annotations/defaultbackend/main.go @@ -77,6 +77,6 @@ func (db backend) GetDocumentation() parser.AnnotationFields { } func (a backend) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, defaultBackendAnnotations.Annotations) } diff --git a/internal/ingress/annotations/fastcgi/main.go b/internal/ingress/annotations/fastcgi/main.go index ec4feec660..96dbc71597 100644 --- a/internal/ingress/annotations/fastcgi/main.go +++ b/internal/ingress/annotations/fastcgi/main.go @@ -162,6 +162,6 @@ func (a fastcgi) GetDocumentation() parser.AnnotationFields { } func (a fastcgi) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, fastCGIAnnotations.Annotations) } diff --git a/internal/ingress/annotations/globalratelimit/main.go b/internal/ingress/annotations/globalratelimit/main.go index c8803dada6..41f58fd57a 100644 --- a/internal/ingress/annotations/globalratelimit/main.go +++ b/internal/ingress/annotations/globalratelimit/main.go @@ -175,6 +175,6 @@ func (a globalratelimit) GetDocumentation() parser.AnnotationFields { } func (a globalratelimit) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, globalRateLimitAnnotationConfig.Annotations) } diff --git a/internal/ingress/annotations/http2pushpreload/main.go b/internal/ingress/annotations/http2pushpreload/main.go index b7c3287e37..af9f90aa94 100644 --- a/internal/ingress/annotations/http2pushpreload/main.go +++ b/internal/ingress/annotations/http2pushpreload/main.go @@ -63,6 +63,6 @@ func (h2pp http2PushPreload) GetDocumentation() parser.AnnotationFields { } func (a http2PushPreload) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, http2PushPreloadAnnotations.Annotations) } diff --git a/internal/ingress/annotations/ipallowlist/main.go b/internal/ingress/annotations/ipallowlist/main.go index f2fb2cb81d..d9d454c978 100644 --- a/internal/ingress/annotations/ipallowlist/main.go +++ b/internal/ingress/annotations/ipallowlist/main.go @@ -128,6 +128,6 @@ func (a ipallowlist) GetDocumentation() parser.AnnotationFields { } func (a ipallowlist) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, allowlistAnnotations.Annotations) } diff --git a/internal/ingress/annotations/ipdenylist/main.go b/internal/ingress/annotations/ipdenylist/main.go index 37bd7ad50f..f17ce079af 100644 --- a/internal/ingress/annotations/ipdenylist/main.go +++ b/internal/ingress/annotations/ipdenylist/main.go @@ -125,6 +125,6 @@ func (a ipdenylist) GetDocumentation() parser.AnnotationFields { } func (a ipdenylist) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, denylistAnnotations.Annotations) } diff --git a/internal/ingress/annotations/loadbalancing/main.go b/internal/ingress/annotations/loadbalancing/main.go index 0191981a45..ee89d2c1b7 100644 --- a/internal/ingress/annotations/loadbalancing/main.go +++ b/internal/ingress/annotations/loadbalancing/main.go @@ -69,6 +69,6 @@ func (a loadbalancing) GetDocumentation() parser.AnnotationFields { } func (a loadbalancing) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, loadBalanceAnnotations.Annotations) } diff --git a/internal/ingress/annotations/log/main.go b/internal/ingress/annotations/log/main.go index 3649ecd6a7..ec08292a9e 100644 --- a/internal/ingress/annotations/log/main.go +++ b/internal/ingress/annotations/log/main.go @@ -102,6 +102,6 @@ func (l log) GetDocumentation() parser.AnnotationFields { } func (a log) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, logAnnotations.Annotations) } diff --git a/internal/ingress/annotations/mirror/main.go b/internal/ingress/annotations/mirror/main.go index 14a14e3242..2d417dece7 100644 --- a/internal/ingress/annotations/mirror/main.go +++ b/internal/ingress/annotations/mirror/main.go @@ -163,6 +163,6 @@ func (a mirror) GetDocumentation() parser.AnnotationFields { } func (a mirror) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, mirrorAnnotation.Annotations) } diff --git a/internal/ingress/annotations/modsecurity/main.go b/internal/ingress/annotations/modsecurity/main.go index 97120eac7d..5a9aaa7295 100644 --- a/internal/ingress/annotations/modsecurity/main.go +++ b/internal/ingress/annotations/modsecurity/main.go @@ -155,6 +155,6 @@ func (a modSecurity) GetDocumentation() parser.AnnotationFields { } func (a modSecurity) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, modsecurityAnnotation.Annotations) } diff --git a/internal/ingress/annotations/opentelemetry/main.go b/internal/ingress/annotations/opentelemetry/main.go index 24f34ae4ba..a029087dad 100644 --- a/internal/ingress/annotations/opentelemetry/main.go +++ b/internal/ingress/annotations/opentelemetry/main.go @@ -151,6 +151,6 @@ func (c opentelemetry) GetDocumentation() parser.AnnotationFields { } func (a opentelemetry) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, otelAnnotations.Annotations) } diff --git a/internal/ingress/annotations/opentracing/main.go b/internal/ingress/annotations/opentracing/main.go index c8fc71d659..7c8671f9dc 100644 --- a/internal/ingress/annotations/opentracing/main.go +++ b/internal/ingress/annotations/opentracing/main.go @@ -108,6 +108,6 @@ func (s opentracing) GetDocumentation() parser.AnnotationFields { } func (a opentracing) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, opentracingAnnotations.Annotations) } diff --git a/internal/ingress/annotations/portinredirect/main.go b/internal/ingress/annotations/portinredirect/main.go index 0e51ee0f4f..7392ea3a69 100644 --- a/internal/ingress/annotations/portinredirect/main.go +++ b/internal/ingress/annotations/portinredirect/main.go @@ -68,6 +68,6 @@ func (a portInRedirect) GetDocumentation() parser.AnnotationFields { } func (a portInRedirect) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, portsInRedirectAnnotations.Annotations) } diff --git a/internal/ingress/annotations/proxy/main.go b/internal/ingress/annotations/proxy/main.go index 120a998301..a2d10ca909 100644 --- a/internal/ingress/annotations/proxy/main.go +++ b/internal/ingress/annotations/proxy/main.go @@ -359,6 +359,6 @@ func (a proxy) GetDocumentation() parser.AnnotationFields { } func (a proxy) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, proxyAnnotations.Annotations) } diff --git a/internal/ingress/annotations/proxyssl/main.go b/internal/ingress/annotations/proxyssl/main.go index 254996926a..40ee18aa0f 100644 --- a/internal/ingress/annotations/proxyssl/main.go +++ b/internal/ingress/annotations/proxyssl/main.go @@ -261,6 +261,6 @@ func (p proxySSL) GetDocumentation() parser.AnnotationFields { } func (a proxySSL) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, proxySSLAnnotation.Annotations) } diff --git a/internal/ingress/annotations/ratelimit/main.go b/internal/ingress/annotations/ratelimit/main.go index 38940203ad..39161a2c09 100644 --- a/internal/ingress/annotations/ratelimit/main.go +++ b/internal/ingress/annotations/ratelimit/main.go @@ -296,6 +296,6 @@ func (a ratelimit) GetDocumentation() parser.AnnotationFields { } func (a ratelimit) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, rateLimitAnnotations.Annotations) } diff --git a/internal/ingress/annotations/redirect/redirect.go b/internal/ingress/annotations/redirect/redirect.go index 9300271eb5..89513c83c2 100644 --- a/internal/ingress/annotations/redirect/redirect.go +++ b/internal/ingress/annotations/redirect/redirect.go @@ -179,6 +179,6 @@ func (a redirect) GetDocumentation() parser.AnnotationFields { } func (a redirect) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, redirectAnnotations.Annotations) } diff --git a/internal/ingress/annotations/rewrite/main.go b/internal/ingress/annotations/rewrite/main.go index 1134e27b19..84dc93bf03 100644 --- a/internal/ingress/annotations/rewrite/main.go +++ b/internal/ingress/annotations/rewrite/main.go @@ -210,6 +210,6 @@ func (a rewrite) GetDocumentation() parser.AnnotationFields { } func (a rewrite) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, rewriteAnnotations.Annotations) } diff --git a/internal/ingress/annotations/satisfy/main.go b/internal/ingress/annotations/satisfy/main.go index d74fb33f52..45187fe5c4 100644 --- a/internal/ingress/annotations/satisfy/main.go +++ b/internal/ingress/annotations/satisfy/main.go @@ -70,6 +70,6 @@ func (s satisfy) GetDocumentation() parser.AnnotationFields { } func (a satisfy) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, satisfyAnnotations.Annotations) } diff --git a/internal/ingress/annotations/serversnippet/main.go b/internal/ingress/annotations/serversnippet/main.go index b6693c1d9b..aa15608d01 100644 --- a/internal/ingress/annotations/serversnippet/main.go +++ b/internal/ingress/annotations/serversnippet/main.go @@ -64,6 +64,6 @@ func (a serverSnippet) GetDocumentation() parser.AnnotationFields { } func (a serverSnippet) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, serverSnippetAnnotations.Annotations) } diff --git a/internal/ingress/annotations/serviceupstream/main.go b/internal/ingress/annotations/serviceupstream/main.go index 7692ffe867..e662f73c3d 100644 --- a/internal/ingress/annotations/serviceupstream/main.go +++ b/internal/ingress/annotations/serviceupstream/main.go @@ -70,6 +70,6 @@ func (s serviceUpstream) GetDocumentation() parser.AnnotationFields { } func (a serviceUpstream) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, serviceUpstreamAnnotations.Annotations) } diff --git a/internal/ingress/annotations/sessionaffinity/main.go b/internal/ingress/annotations/sessionaffinity/main.go index 07d737de38..0a4a59dbcb 100644 --- a/internal/ingress/annotations/sessionaffinity/main.go +++ b/internal/ingress/annotations/sessionaffinity/main.go @@ -299,6 +299,6 @@ func (a affinity) GetDocumentation() parser.AnnotationFields { } func (a affinity) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, sessionAffinityAnnotations.Annotations) } diff --git a/internal/ingress/annotations/snippet/main.go b/internal/ingress/annotations/snippet/main.go index 6b75ff440c..2406093c5d 100644 --- a/internal/ingress/annotations/snippet/main.go +++ b/internal/ingress/annotations/snippet/main.go @@ -64,6 +64,6 @@ func (a snippet) GetDocumentation() parser.AnnotationFields { } func (a snippet) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, configurationSnippetAnnotations.Annotations) } diff --git a/internal/ingress/annotations/sslcipher/main.go b/internal/ingress/annotations/sslcipher/main.go index a1a316d574..c30f12424b 100644 --- a/internal/ingress/annotations/sslcipher/main.go +++ b/internal/ingress/annotations/sslcipher/main.go @@ -105,6 +105,6 @@ func (sc sslCipher) GetDocumentation() parser.AnnotationFields { } func (a sslCipher) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, sslCipherAnnotations.Annotations) } diff --git a/internal/ingress/annotations/sslpassthrough/main.go b/internal/ingress/annotations/sslpassthrough/main.go index 45007b7f77..1557d4243d 100644 --- a/internal/ingress/annotations/sslpassthrough/main.go +++ b/internal/ingress/annotations/sslpassthrough/main.go @@ -67,6 +67,6 @@ func (a sslpt) GetDocumentation() parser.AnnotationFields { } func (a sslpt) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, sslPassthroughAnnotations.Annotations) } diff --git a/internal/ingress/annotations/streamsnippet/main.go b/internal/ingress/annotations/streamsnippet/main.go index da5a9a142e..71ff3b1404 100644 --- a/internal/ingress/annotations/streamsnippet/main.go +++ b/internal/ingress/annotations/streamsnippet/main.go @@ -64,6 +64,6 @@ func (a streamSnippet) GetDocumentation() parser.AnnotationFields { } func (a streamSnippet) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, streamSnippetAnnotations.Annotations) } diff --git a/internal/ingress/annotations/upstreamhashby/main.go b/internal/ingress/annotations/upstreamhashby/main.go index 8259618831..bc07f70fbb 100644 --- a/internal/ingress/annotations/upstreamhashby/main.go +++ b/internal/ingress/annotations/upstreamhashby/main.go @@ -109,6 +109,6 @@ func (a upstreamhashby) GetDocumentation() parser.AnnotationFields { } func (a upstreamhashby) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, upstreamHashByAnnotations.Annotations) } diff --git a/internal/ingress/annotations/upstreamvhost/main.go b/internal/ingress/annotations/upstreamvhost/main.go index 028e629398..052ca23442 100644 --- a/internal/ingress/annotations/upstreamvhost/main.go +++ b/internal/ingress/annotations/upstreamvhost/main.go @@ -65,6 +65,6 @@ func (a upstreamVhost) GetDocumentation() parser.AnnotationFields { } func (a upstreamVhost) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, upstreamVhostAnnotations.Annotations) } diff --git a/internal/ingress/annotations/xforwardedprefix/main.go b/internal/ingress/annotations/xforwardedprefix/main.go index 0986295b33..fc4d5798d3 100644 --- a/internal/ingress/annotations/xforwardedprefix/main.go +++ b/internal/ingress/annotations/xforwardedprefix/main.go @@ -63,6 +63,6 @@ func (cbbs xforwardedprefix) GetDocumentation() parser.AnnotationFields { } func (a xforwardedprefix) Validate(anns map[string]string) error { - maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRisk) + maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel) return parser.CheckAnnotationRisk(anns, maxrisk, xForwardedForAnnotations.Annotations) } diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go index adecd5ddff..6e78964ede 100644 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -103,10 +103,10 @@ type Configuration struct { // This value will default to `false` on future releases AllowCrossNamespaceResources bool `json:"allow-cross-namespace-resources"` - // AnnotationsRisk represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations + // AnnotationsRiskLevel represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations // with risk High and Critical will not be accepted. // Default Risk is Critical by default, but this may be changed in future releases - AnnotationsRisk string `json:"annotations-risk"` + AnnotationsRiskLevel string `json:"annotations-risk-level"` // AnnotationValueWordBlocklist defines words that should not be part of an user annotation value // (can be used to run arbitrary code or configs, for example) and that should be dropped. @@ -719,7 +719,7 @@ type Configuration struct { // DatadogSampleRate specifies sample rate for any traces created. // Default: use a dynamic rate instead - DatadogSampleRate *float32 `json:"datadog-sample-rate",omitempty` + DatadogSampleRate *float32 `json:"datadog-sample-rate,omitempty"` // MainSnippet adds custom configuration to the main section of the nginx configuration MainSnippet string `json:"main-snippet"` @@ -867,7 +867,7 @@ func NewDefault() Configuration { AllowCrossNamespaceResources: true, AllowBackendServerHeader: false, AnnotationValueWordBlocklist: "", - AnnotationsRisk: "Critical", + AnnotationsRiskLevel: "Critical", AccessLogPath: "/var/log/nginx/access.log", AccessLogParams: "", EnableAccessLogForDefaultBackend: false, diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go index 2e3b44406a..c353d1b5ea 100644 --- a/internal/ingress/controller/controller_test.go +++ b/internal/ingress/controller/controller_test.go @@ -75,7 +75,7 @@ func (fis fakeIngressStore) GetBackendConfiguration() ngx_config.Configuration { func (fis fakeIngressStore) GetSecurityConfiguration() defaults.SecurityConfiguration { return defaults.SecurityConfiguration{ - AnnotationsRisk: fis.configuration.AnnotationsRisk, + AnnotationsRiskLevel: fis.configuration.AnnotationsRiskLevel, AllowCrossNamespaceResources: fis.configuration.AllowCrossNamespaceResources, } } diff --git a/internal/ingress/controller/store/store.go b/internal/ingress/controller/store/store.go index 6f37414c27..c11e35d766 100644 --- a/internal/ingress/controller/store/store.go +++ b/internal/ingress/controller/store/store.go @@ -1145,7 +1145,7 @@ func (s *k8sStore) GetSecurityConfiguration() defaults.SecurityConfiguration { secConfig := defaults.SecurityConfiguration{ AllowCrossNamespaceResources: s.backendConfig.AllowCrossNamespaceResources, - AnnotationsRisk: s.backendConfig.AnnotationsRisk, + AnnotationsRiskLevel: s.backendConfig.AnnotationsRiskLevel, } return secConfig } diff --git a/internal/ingress/defaults/main.go b/internal/ingress/defaults/main.go index 078d50e63b..8cd0e8ba5b 100644 --- a/internal/ingress/defaults/main.go +++ b/internal/ingress/defaults/main.go @@ -178,7 +178,7 @@ type SecurityConfiguration struct { // This valid will default to `false` on future releases AllowCrossNamespaceResources bool `json:"allow-cross-namespace-resources"` - // AnnotationsRisk represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations + // AnnotationsRiskLevel represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations // with risk High and Critical will not be accepted - AnnotationsRisk string `json:"annotations-risk"` + AnnotationsRiskLevel string `json:"annotations-risk-level"` } diff --git a/internal/ingress/resolver/mock.go b/internal/ingress/resolver/mock.go index 2a1c265e7d..679c3b13ce 100644 --- a/internal/ingress/resolver/mock.go +++ b/internal/ingress/resolver/mock.go @@ -26,9 +26,9 @@ import ( // Mock implements the Resolver interface type Mock struct { - ConfigMaps map[string]*apiv1.ConfigMap - AnnotationRisk string - AllowCrossNamespace bool + ConfigMaps map[string]*apiv1.ConfigMap + AnnotationsRiskLevel string + AllowCrossNamespace bool } // GetDefaultBackend returns the backend that must be used as default @@ -37,12 +37,12 @@ func (m Mock) GetDefaultBackend() defaults.Backend { } func (m Mock) GetSecurityConfiguration() defaults.SecurityConfiguration { - defRisk := m.AnnotationRisk + defRisk := m.AnnotationsRiskLevel if defRisk == "" { defRisk = "Critical" } return defaults.SecurityConfiguration{ - AnnotationsRisk: defRisk, + AnnotationsRiskLevel: defRisk, AllowCrossNamespaceResources: m.AllowCrossNamespace, } } diff --git a/test/e2e/settings/validations/validations.go b/test/e2e/settings/validations/validations.go index 2163ff3e17..6f1715ada6 100644 --- a/test/e2e/settings/validations/validations.go +++ b/test/e2e/settings/validations/validations.go @@ -34,7 +34,7 @@ var _ = framework.IngressNginxDescribeSerial("annotation validations", func() { host := "annotation-validations" // Low and Medium Risk annotations should be allowed, the rest should be denied - f.UpdateNginxConfigMapData("annotations-risk", "Medium") + f.UpdateNginxConfigMapData("annotations-risk-level", "Medium") // Sleep a while just to guarantee that the configmap is applied framework.Sleep() @@ -61,7 +61,7 @@ var _ = framework.IngressNginxDescribeSerial("annotation validations", func() { host := "annotation-validations" // Low and Medium Risk annotations should be allowed, the rest should be denied - f.UpdateNginxConfigMapData("annotations-risk", "Medium") + f.UpdateNginxConfigMapData("annotations-risk-level", "Medium") // Sleep a while just to guarantee that the configmap is applied framework.Sleep()