diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md index f1eb69e1b..a9e4f9f4e 100644 --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -80,6 +80,9 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz |[nginx.ingress.kubernetes.io/lua-resty-waf-debug](#lua-resty-waf)|"true" or "false"| |[nginx.ingress.kubernetes.io/lua-resty-waf-ignore-rulesets](#lua-resty-waf)|string| |[nginx.ingress.kubernetes.io/lua-resty-waf-extra-rules](#lua-resty-waf)|string| +|[nginx.ingress.kubernetes.io/lua-resty-waf-allow-unknown-content-types](#lua-resty-waf)|"true" or "false"| +|[nginx.ingress.kubernetes.io/lua-resty-waf-score-threshold](#lua-resty-waf)|number| +|[nginx.ingress.kubernetes.io/lua-resty-waf-process-multipart-body](#lua-resty-waf)|"true" or "false"| |[nginx.ingress.kubernetes.io/enable-influxdb](#influxdb)|"true" or "false"| |[nginx.ingress.kubernetes.io/influxdb-measurement](#influxdb)|string| |[nginx.ingress.kubernetes.io/influxdb-port](#influxdb)|string| @@ -558,6 +561,32 @@ It is also possible to configure custom WAF rules per ingress using the `nginx.i nginx.ingress.kubernetes.io/lua-resty-waf-extra-rules: '[=[ { "access": [ { "actions": { "disrupt" : "DENY" }, "id": 10001, "msg": "my custom rule", "operator": "STR_CONTAINS", "pattern": "foo", "vars": [ { "parse": [ "values", 1 ], "type": "REQUEST_ARGS" } ] } ], "body_filter": [], "header_filter":[] } ]=]' ``` +Since the default allowed contents were `"text/html", "text/json", "application/json"` +We can enable the following annotation for allow all contents type: + + +```yaml +nginx.ingress.kubernetes.io/lua-resty-waf-allow-unknown-content-types: "true" +``` + +The default score of lua-resty-waf is 5, which usually triggered if hitting 2 default rules, you can modify the score threshold with following annotation: + + +```yaml +nginx.ingress.kubernetes.io/lua-resty-waf-score-threshold: "10" +``` + +When you enabled HTTPS in the endpoint and since resty-lua will return 500 error when processing "multipart" contents +Reference for this [issue](https://github.com/p0pr0ck5/lua-resty-waf/issues/166) + +By default, it will be "true" + +You may enable the following annotation for work around: + +```yaml +nginx.ingress.kubernetes.io/lua-resty-waf-process-multipart-body: "false" +``` + For details on how to write WAF rules, please refer to [https://github.com/p0pr0ck5/lua-resty-waf](https://github.com/p0pr0ck5/lua-resty-waf). [configmap]: ./configmap.md diff --git a/internal/ingress/annotations/luarestywaf/main.go b/internal/ingress/annotations/luarestywaf/main.go index ca7f4a8be..c29a97e9c 100644 --- a/internal/ingress/annotations/luarestywaf/main.go +++ b/internal/ingress/annotations/luarestywaf/main.go @@ -31,10 +31,13 @@ var luaRestyWAFModes = map[string]bool{"ACTIVE": true, "INACTIVE": true, "SIMULA // Config returns lua-resty-waf configuration for an Ingress rule type Config struct { - Mode string `json:"mode"` - Debug bool `json:"debug"` - IgnoredRuleSets []string `json:"ignored-rulesets"` - ExtraRulesetString string `json:"extra-ruleset-string"` + Mode string `json:"mode"` + Debug bool `json:"debug"` + IgnoredRuleSets []string `json:"ignored-rulesets"` + ExtraRulesetString string `json:"extra-ruleset-string"` + ScoreThreshold int `json:"score-threshold"` + AllowUnknownContentTypes bool `json:"allow-unknown-content-types"` + ProcessMultipartBody bool `json:"process-multipart-body"` } // Equal tests for equality between two Config types @@ -57,6 +60,15 @@ func (e1 *Config) Equal(e2 *Config) bool { if e1.ExtraRulesetString != e2.ExtraRulesetString { return false } + if e1.ScoreThreshold != e2.ScoreThreshold { + return false + } + if e1.AllowUnknownContentTypes != e2.AllowUnknownContentTypes { + return false + } + if e1.ProcessMultipartBody != e2.ProcessMultipartBody { + return false + } return true } @@ -95,10 +107,22 @@ func (a luarestywaf) Parse(ing *extensions.Ingress) (interface{}, error) { // TODO(elvinefendi) maybe validate the ruleset string here extraRulesetString, _ := parser.GetStringAnnotation("lua-resty-waf-extra-rules", ing) + scoreThreshold, _ := parser.GetIntAnnotation("lua-resty-waf-score-threshold", ing) + + allowUnknownContentTypes, _ := parser.GetBoolAnnotation("lua-resty-waf-allow-unknown-content-types", ing) + + processMultipartBody, err := parser.GetBoolAnnotation("lua-resty-waf-process-multipart-body", ing) + if err != nil { + processMultipartBody = true + } + return &Config{ - Mode: mode, - Debug: debug, - IgnoredRuleSets: ignoredRuleSets, - ExtraRulesetString: extraRulesetString, + Mode: mode, + Debug: debug, + IgnoredRuleSets: ignoredRuleSets, + ExtraRulesetString: extraRulesetString, + ScoreThreshold: scoreThreshold, + AllowUnknownContentTypes: allowUnknownContentTypes, + ProcessMultipartBody: processMultipartBody, }, nil } diff --git a/internal/ingress/annotations/luarestywaf/main_test.go b/internal/ingress/annotations/luarestywaf/main_test.go index d71191f12..60b80e22f 100644 --- a/internal/ingress/annotations/luarestywaf/main_test.go +++ b/internal/ingress/annotations/luarestywaf/main_test.go @@ -30,6 +30,9 @@ func TestParse(t *testing.T) { luaRestyWAFAnnotation := parser.GetAnnotationWithPrefix("lua-resty-waf") luaRestyWAFDebugAnnotation := parser.GetAnnotationWithPrefix("lua-resty-waf-debug") luaRestyWAFIgnoredRuleSetsAnnotation := parser.GetAnnotationWithPrefix("lua-resty-waf-ignore-rulesets") + luaRestyWAFScoreThresholdAnnotation := parser.GetAnnotationWithPrefix("lua-resty-waf-score-threshold") + luaRestyWAFAllowUnknownContentTypesAnnotation := parser.GetAnnotationWithPrefix("lua-resty-waf-allow-unknown-content-types") + luaRestyWAFProcessMultipartBody := parser.GetAnnotationWithPrefix("lua-resty-waf-process-multipart-body") ap := NewParser(&resolver.Mock{}) if ap == nil { @@ -43,21 +46,25 @@ func TestParse(t *testing.T) { {nil, &Config{}}, {map[string]string{}, &Config{}}, - {map[string]string{luaRestyWAFAnnotation: "active"}, &Config{Mode: "ACTIVE", Debug: false, IgnoredRuleSets: []string{}}}, + {map[string]string{luaRestyWAFAnnotation: "active"}, &Config{Mode: "ACTIVE", Debug: false, IgnoredRuleSets: []string{}, ProcessMultipartBody: true}}, {map[string]string{luaRestyWAFDebugAnnotation: "true"}, &Config{Debug: false}}, - {map[string]string{luaRestyWAFAnnotation: "active", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "ACTIVE", Debug: true, IgnoredRuleSets: []string{}}}, - {map[string]string{luaRestyWAFAnnotation: "active", luaRestyWAFDebugAnnotation: "false"}, &Config{Mode: "ACTIVE", Debug: false, IgnoredRuleSets: []string{}}}, - {map[string]string{luaRestyWAFAnnotation: "inactive", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "INACTIVE", Debug: true, IgnoredRuleSets: []string{}}}, + {map[string]string{luaRestyWAFAnnotation: "active", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "ACTIVE", Debug: true, IgnoredRuleSets: []string{}, ProcessMultipartBody: true}}, + {map[string]string{luaRestyWAFAnnotation: "active", luaRestyWAFDebugAnnotation: "false"}, &Config{Mode: "ACTIVE", Debug: false, IgnoredRuleSets: []string{}, ProcessMultipartBody: true}}, + {map[string]string{luaRestyWAFAnnotation: "inactive", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "INACTIVE", Debug: true, IgnoredRuleSets: []string{}, ProcessMultipartBody: true}}, {map[string]string{ - luaRestyWAFAnnotation: "active", - luaRestyWAFDebugAnnotation: "true", - luaRestyWAFIgnoredRuleSetsAnnotation: "ruleset1, ruleset2 ruleset3, another.ruleset"}, - &Config{Mode: "ACTIVE", Debug: true, IgnoredRuleSets: []string{"ruleset1", "ruleset2", "ruleset3", "another.ruleset"}}}, + luaRestyWAFAnnotation: "active", + luaRestyWAFDebugAnnotation: "true", + luaRestyWAFIgnoredRuleSetsAnnotation: "ruleset1, ruleset2 ruleset3, another.ruleset", + luaRestyWAFScoreThresholdAnnotation: "10", + luaRestyWAFAllowUnknownContentTypesAnnotation: "true"}, + &Config{Mode: "ACTIVE", Debug: true, IgnoredRuleSets: []string{"ruleset1", "ruleset2", "ruleset3", "another.ruleset"}, ScoreThreshold: 10, AllowUnknownContentTypes: true, ProcessMultipartBody: true}}, - {map[string]string{luaRestyWAFAnnotation: "siMulate", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "SIMULATE", Debug: true, IgnoredRuleSets: []string{}}}, + {map[string]string{luaRestyWAFAnnotation: "siMulate", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "SIMULATE", Debug: true, IgnoredRuleSets: []string{}, ProcessMultipartBody: true}}, {map[string]string{luaRestyWAFAnnotation: "siMulateX", luaRestyWAFDebugAnnotation: "true"}, &Config{Debug: false}}, + + {map[string]string{luaRestyWAFAnnotation: "active", luaRestyWAFProcessMultipartBody: "false"}, &Config{Mode: "ACTIVE", ProcessMultipartBody: false, IgnoredRuleSets: []string{}}}, } ing := &extensions.Ingress{ diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index a7c2c7287..ca62f1672 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -891,9 +891,23 @@ stream { waf:set_option("mode", "{{ $location.LuaRestyWAF.Mode }}") waf:set_option("storage_zone", "waf_storage") + + {{ if $location.LuaRestyWAF.AllowUnknownContentTypes }} + waf:set_option("allow_unknown_content_types", true) + {{ else }} waf:set_option("allowed_content_types", { "text/html", "text/json", "application/json" }) + {{ end }} + waf:set_option("event_log_level", ngx.WARN) + {{ if gt $location.LuaRestyWAF.ScoreThreshold 0 }} + waf:set_option("score_threshold", {{ $location.LuaRestyWAF.ScoreThreshold }}) + {{ end }} + + {{ if not $location.LuaRestyWAF.ProcessMultipartBody }} + waf:set_option("process_multipart_body", false) + {{ end }} + {{ if $location.LuaRestyWAF.Debug }} waf:set_option("debug", true) waf:set_option("event_log_request_arguments", true) diff --git a/test/e2e/annotations/luarestywaf.go b/test/e2e/annotations/luarestywaf.go index ed332db04..a20fbdc49 100644 --- a/test/e2e/annotations/luarestywaf.go +++ b/test/e2e/annotations/luarestywaf.go @@ -65,6 +65,71 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() { Expect(len(errs)).Should(Equal(0)) Expect(resp.StatusCode).Should(Equal(http.StatusOK)) }) + It("should apply the score threshold", func() { + host := "foo" + createIngress(f, host, "http-svc", 80, map[string]string{ + "nginx.ingress.kubernetes.io/lua-resty-waf": "active", + "nginx.ingress.kubernetes.io/lua-resty-waf-score-threshold": "20"}) + + url := fmt.Sprintf("%s?msg=XSS", f.IngressController.HTTPURL) + resp, _, errs := gorequest.New(). + Get(url). + Set("Host", host). + End() + + Expect(len(errs)).Should(Equal(0)) + Expect(resp.StatusCode).Should(Equal(http.StatusOK)) + }) + It("should not reject request with an unknown content type", func() { + host := "foo" + contenttype := "application/octet-stream" + createIngress(f, host, "http-svc", 80, map[string]string{ + "nginx.ingress.kubernetes.io/lua-resty-waf-allow-unknown-content-types": "true", + "nginx.ingress.kubernetes.io/lua-resty-waf": "active"}) + + url := fmt.Sprintf("%s?msg=my-message", f.IngressController.HTTPURL) + resp, _, errs := gorequest.New(). + Get(url). + Set("Host", host). + Set("Content-Type", contenttype). + End() + + Expect(len(errs)).Should(Equal(0)) + Expect(resp.StatusCode).Should(Equal(http.StatusOK)) + }) + It("should not fail a request with multipart content type when multipart body processing disabled", func() { + contenttype := "multipart/form-data; boundary=alamofire.boundary.3fc2e849279e18fc" + host := "foo" + createIngress(f, host, "http-svc", 80, map[string]string{ + "nginx.ingress.kubernetes.io/lua-resty-waf-process-multipart-body": "false", + "nginx.ingress.kubernetes.io/lua-resty-waf": "active"}) + + url := fmt.Sprintf("%s?msg=my-message", f.IngressController.HTTPURL) + resp, _, errs := gorequest.New(). + Get(url). + Set("Host", host). + Set("Content-Type", contenttype). + End() + + Expect(len(errs)).Should(Equal(0)) + Expect(resp.StatusCode).Should(Equal(http.StatusOK)) + }) + It("should fail a request with multipart content type when multipart body processing enabled by default", func() { + contenttype := "multipart/form-data; boundary=alamofire.boundary.3fc2e849279e18fc" + host := "foo" + createIngress(f, host, "http-svc", 80, map[string]string{ + "nginx.ingress.kubernetes.io/lua-resty-waf": "active"}) + + url := fmt.Sprintf("%s?msg=my-message", f.IngressController.HTTPURL) + resp, _, errs := gorequest.New(). + Get(url). + Set("Host", host). + Set("Content-Type", contenttype). + End() + + Expect(len(errs)).Should(Equal(0)) + Expect(resp.StatusCode).Should(Equal(http.StatusBadRequest)) + }) It("should apply configured extra rules", func() { host := "foo" createIngress(f, host, "http-svc", 80, map[string]string{