Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UPT: annotation enhancement for resty-lua-waf #3187

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/user-guide/nginx-configuration/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -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"
```
DesmondH0 marked this conversation as resolved.
Show resolved Hide resolved

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
Expand Down
40 changes: 32 additions & 8 deletions internal/ingress/annotations/luarestywaf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
25 changes: 16 additions & 9 deletions internal/ingress/annotations/luarestywaf/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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{
Expand Down
14 changes: 14 additions & 0 deletions rootfs/etc/nginx/template/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions test/e2e/annotations/luarestywaf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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=<A href=\"http://mysite.com/\">XSS</A>", 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))
})
DesmondH0 marked this conversation as resolved.
Show resolved Hide resolved
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",
DesmondH0 marked this conversation as resolved.
Show resolved Hide resolved
"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{
Expand Down