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

run lua-resty-waf in different modes #2317

Merged
merged 2 commits into from
Apr 9, 2018
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
6 changes: 4 additions & 2 deletions docs/user-guide/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The following annotations are supported:
|[nginx.ingress.kubernetes.io/ssl-ciphers](#ssl-ciphers)|string|
|[nginx.ingress.kubernetes.io/connection-proxy-header](#connection-proxy-header)|string|
|[nginx.ingress.kubernetes.io/enable-access-log](#enable-access-log)|"true" or "false"|
|[nginx.ingress.kubernetes.io/lua-resty-waf](#lua-resty-waf)|"true" or "false"|
|[nginx.ingress.kubernetes.io/lua-resty-waf](#lua-resty-waf)|string|
|[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|
Expand Down Expand Up @@ -474,10 +474,12 @@ Using `lua-resty-waf-*` annotations we can enable and control [lua-resty-waf](ht
Following configuration will enable WAF for the paths defined in the corresponding ingress:

```yaml
nginx.ingress.kubernetes.io/lua-resty-waf: "true"
nginx.ingress.kubernetes.io/lua-resty-waf: "active"
```

In order to run it in debugging mode you can set `nginx.ingress.kubernetes.io/lua-resty-waf-debug` to `"true"` in addition to the above configuration.
The other possible values for `nginx.ingress.kubernetes.io/lua-resty-waf` are `inactive` and `simulate`. In `inactive` mode WAF won't do anything, whereas
in `simulate` mode it will log a warning message if there's a matching WAF rule for given request. This is useful to debug a rule and eliminate possible false positives before fully deploying it.

`lua-resty-waf` comes with predefined set of rules(https://github.com/p0pr0ck5/lua-resty-waf/tree/84b4f40362500dd0cb98b9e71b5875cb1a40f1ad/rules) that covers ModSecurity CRS.
You can use `nginx.ingress.kubernetes.io/lua-resty-waf-ignore-rulesets` to ignore subset of those rulesets. For an example:
Expand Down
2 changes: 1 addition & 1 deletion internal/file/bindata.go

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions internal/ingress/annotations/luarestywaf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ import (
extensions "k8s.io/api/extensions/v1beta1"

"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)

var luaRestyWAFModes = map[string]bool{"ACTIVE": true, "INACTIVE": true, "SIMULATE": true}

// Config returns lua-resty-waf configuration for an Ingress rule
type Config struct {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
Debug bool `json:"debug"`
IgnoredRuleSets []string `json: "ignored-rulesets"`
ExtraRulesetString string `json: "extra-ruleset-string"`
Expand All @@ -42,7 +45,7 @@ func (e1 *Config) Equal(e2 *Config) bool {
if e1 == nil || e2 == nil {
return false
}
if e1.Enabled != e2.Enabled {
if e1.Mode != e2.Mode {
return false
}
if e1.Debug != e2.Debug {
Expand Down Expand Up @@ -71,11 +74,16 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
// used to indicate if the location/s contains a fragment of
// configuration to be included inside the paths of the rules
func (a luarestywaf) Parse(ing *extensions.Ingress) (interface{}, error) {
enabled, err := parser.GetBoolAnnotation("lua-resty-waf", ing)
mode, err := parser.GetStringAnnotation("lua-resty-waf", ing)
if err != nil {
return &Config{}, err
}

mode = strings.ToUpper(mode)
if _, ok := luaRestyWAFModes[mode]; !ok {
return &Config{}, errors.NewInvalidAnnotationContent("lua-resty-waf", mode)
}

debug, _ := parser.GetBoolAnnotation("lua-resty-waf-debug", ing)

ignoredRuleSetsStr, _ := parser.GetStringAnnotation("lua-resty-waf-ignore-rulesets", ing)
Expand All @@ -88,7 +96,7 @@ func (a luarestywaf) Parse(ing *extensions.Ingress) (interface{}, error) {
extraRulesetString, _ := parser.GetStringAnnotation("lua-resty-waf-extra-rules", ing)

return &Config{
Enabled: enabled,
Mode: mode,
Debug: debug,
IgnoredRuleSets: ignoredRuleSets,
ExtraRulesetString: extraRulesetString,
Expand Down
17 changes: 10 additions & 7 deletions internal/ingress/annotations/luarestywaf/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,21 @@ func TestParse(t *testing.T) {
{nil, &Config{}},
{map[string]string{}, &Config{}},

{map[string]string{luaRestyWAFAnnotation: "true"}, &Config{Enabled: true, Debug: false, IgnoredRuleSets: []string{}}},
{map[string]string{luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: false, Debug: false}},
{map[string]string{luaRestyWAFAnnotation: "active"}, &Config{Mode: "ACTIVE", Debug: false, IgnoredRuleSets: []string{}}},
{map[string]string{luaRestyWAFDebugAnnotation: "true"}, &Config{Debug: false}},

{map[string]string{luaRestyWAFAnnotation: "true", luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: true, Debug: true, IgnoredRuleSets: []string{}}},
{map[string]string{luaRestyWAFAnnotation: "true", luaRestyWAFDebugAnnotation: "false"}, &Config{Enabled: true, Debug: false, IgnoredRuleSets: []string{}}},
{map[string]string{luaRestyWAFAnnotation: "false", luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: false, Debug: true, IgnoredRuleSets: []string{}}},
{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: "true",
luaRestyWAFAnnotation: "active",
luaRestyWAFDebugAnnotation: "true",
luaRestyWAFIgnoredRuleSetsAnnotation: "ruleset1, ruleset2 ruleset3, another.ruleset"},
&Config{Enabled: true, Debug: true, IgnoredRuleSets: []string{"ruleset1", "ruleset2", "ruleset3", "another.ruleset"}}},
&Config{Mode: "ACTIVE", Debug: true, IgnoredRuleSets: []string{"ruleset1", "ruleset2", "ruleset3", "another.ruleset"}}},

{map[string]string{luaRestyWAFAnnotation: "siMulate", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "SIMULATE", Debug: true, IgnoredRuleSets: []string{}}},
{map[string]string{luaRestyWAFAnnotation: "siMulateX", luaRestyWAFDebugAnnotation: "true"}, &Config{Debug: false}},
}

ing := &extensions.Ingress{
Expand Down
11 changes: 10 additions & 1 deletion internal/ingress/controller/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ var (
}
return true
},
"shouldConfigureLuaRestyWAF": shouldConfigureLuaRestyWAF,
"buildLuaSharedDictionaries": buildLuaSharedDictionaries,
"buildLocation": buildLocation,
"buildAuthLocation": buildAuthLocation,
Expand Down Expand Up @@ -168,6 +169,14 @@ func formatIP(input string) string {
return fmt.Sprintf("[%s]", input)
}

func shouldConfigureLuaRestyWAF(disableLuaRestyWAF bool, mode string) bool {
if !disableLuaRestyWAF && len(mode) > 0 {
return true
}

return false
}

func buildLuaSharedDictionaries(s interface{}, dynamicConfigurationEnabled bool, disableLuaRestyWAF bool) string {
servers, ok := s.([]*ingress.Server)
if !ok {
Expand All @@ -191,7 +200,7 @@ func buildLuaSharedDictionaries(s interface{}, dynamicConfigurationEnabled bool,
luaRestyWAFEnabled := func() bool {
for _, server := range servers {
for _, location := range server.Locations {
if location.LuaRestyWAF.Enabled {
if len(location.LuaRestyWAF.Mode) > 0 {
return true
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/ingress/controller/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ func TestBuildLuaSharedDictionaries(t *testing.T) {
t.Errorf("expected to not include 'waf_storage' but got %s", config)
}

servers[1].Locations[0].LuaRestyWAF = luarestywaf.Config{Enabled: true}
servers[1].Locations[0].LuaRestyWAF = luarestywaf.Config{Mode: "ACTIVE"}
config = buildLuaSharedDictionaries(servers, false, false)
if !strings.Contains(config, "lua_shared_dict waf_storage") {
t.Errorf("expected to configure 'waf_storage', but got %s", config)
Expand Down
6 changes: 3 additions & 3 deletions rootfs/etc/nginx/template/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -816,12 +816,12 @@ stream {
{{ end }}

location {{ $path }} {
{{ if (and (not $all.Cfg.DisableLuaRestyWAF) $location.LuaRestyWAF.Enabled) }}
{{ if shouldConfigureLuaRestyWAF $all.Cfg.DisableLuaRestyWAF $location.LuaRestyWAF.Mode }}
access_by_lua_block {
local lua_resty_waf = require("resty.waf")
local waf = lua_resty_waf:new()

waf:set_option("mode", "ACTIVE")
waf:set_option("mode", "{{ $location.LuaRestyWAF.Mode }}")
waf:set_option("storage_zone", "waf_storage")
waf:set_option("allowed_content_types", { "text/html", "text/json", "application/json" })
waf:set_option("event_log_level", ngx.WARN)
Expand Down Expand Up @@ -857,7 +857,7 @@ stream {
}
{{ end }}
log_by_lua_block {
{{ if (and (not $all.Cfg.DisableLuaRestyWAF) $location.LuaRestyWAF.Enabled) }}
{{ if shouldConfigureLuaRestyWAF $all.Cfg.DisableLuaRestyWAF $location.LuaRestyWAF.Mode }}
local lua_resty_waf = require "resty.waf"
local waf = lua_resty_waf:new()
waf:exec()
Expand Down
49 changes: 34 additions & 15 deletions test/e2e/annotations/luarestywaf.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package annotations
import (
"fmt"
"net/http"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
Expand All @@ -42,7 +43,7 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
Context("when lua-resty-waf is enabled", func() {
It("should return 403 for a malicious request that matches a default WAF rule and 200 for other requests", func() {
host := "foo"
createIngress(f, host, map[string]string{"nginx.ingress.kubernetes.io/lua-resty-waf": "true"})
createIngress(f, host, map[string]string{"nginx.ingress.kubernetes.io/lua-resty-waf": "active"})

url := fmt.Sprintf("%s?msg=<A href=\"http://mysite.com/\">XSS</A>", f.NginxHTTPURL)
resp, _, errs := gorequest.New().
Expand All @@ -56,7 +57,7 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
It("should not apply ignored rulesets", func() {
host := "foo"
createIngress(f, host, map[string]string{
"nginx.ingress.kubernetes.io/lua-resty-waf": "true",
"nginx.ingress.kubernetes.io/lua-resty-waf": "active",
"nginx.ingress.kubernetes.io/lua-resty-waf-ignore-rulesets": "41000_sqli, 42000_xss"})

url := fmt.Sprintf("%s?msg=<A href=\"http://mysite.com/\">XSS</A>", f.NginxHTTPURL)
Expand All @@ -71,20 +72,20 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
It("should apply configured extra rules", func() {
host := "foo"
createIngress(f, host, map[string]string{
"nginx.ingress.kubernetes.io/lua-resty-waf": "true",
"nginx.ingress.kubernetes.io/lua-resty-waf": "active",
"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":[]
}
]=]`,
{ "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":[]
}
]=]`,
})

url := fmt.Sprintf("%s?msg=my-message", f.NginxHTTPURL)
Expand Down Expand Up @@ -120,6 +121,24 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
Expect(len(errs)).Should(Equal(0))
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
})
It("should run in simulate mode", func() {
host := "foo"
createIngress(f, host, map[string]string{"nginx.ingress.kubernetes.io/lua-resty-waf": "simulate"})

url := fmt.Sprintf("%s?msg=<A href=\"http://mysite.com/\">XSS</A>", f.NginxHTTPURL)
resp, _, errs := gorequest.New().
Get(url).
Set("Host", host).
End()

Expect(len(errs)).Should(Equal(0))
Expect(resp.StatusCode).Should(Equal(http.StatusOK))

time.Sleep(5 * time.Second)
log, err := f.NginxLogs()
Expect(err).ToNot(HaveOccurred())
Expect(log).To(ContainSubstring("Request score greater than score threshold"))
})
})
})

Expand Down