run lua-resty-waf in different modes (#2317)
* run lua-resty-waf in different modes * update docs
This commit is contained in:
parent
bad8295a42
commit
d6eb44376d
8 changed files with 75 additions and 34 deletions
|
@ -66,7 +66,7 @@ The following annotations are supported:
|
||||||
|[nginx.ingress.kubernetes.io/ssl-ciphers](#ssl-ciphers)|string|
|
|[nginx.ingress.kubernetes.io/ssl-ciphers](#ssl-ciphers)|string|
|
||||||
|[nginx.ingress.kubernetes.io/connection-proxy-header](#connection-proxy-header)|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/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-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-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-extra-rules](#lua-resty-waf)|string|
|
||||||
|
@ -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:
|
Following configuration will enable WAF for the paths defined in the corresponding ingress:
|
||||||
|
|
||||||
```yaml
|
```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.
|
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.
|
`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:
|
You can use `nginx.ingress.kubernetes.io/lua-resty-waf-ignore-rulesets` to ignore subset of those rulesets. For an example:
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -23,12 +23,15 @@ import (
|
||||||
extensions "k8s.io/api/extensions/v1beta1"
|
extensions "k8s.io/api/extensions/v1beta1"
|
||||||
|
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
"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/ingress/resolver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var luaRestyWAFModes = map[string]bool{"ACTIVE": true, "INACTIVE": true, "SIMULATE": true}
|
||||||
|
|
||||||
// Config returns lua-resty-waf configuration for an Ingress rule
|
// Config returns lua-resty-waf configuration for an Ingress rule
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `json:"enabled"`
|
Mode string `json:"mode"`
|
||||||
Debug bool `json:"debug"`
|
Debug bool `json:"debug"`
|
||||||
IgnoredRuleSets []string `json: "ignored-rulesets"`
|
IgnoredRuleSets []string `json: "ignored-rulesets"`
|
||||||
ExtraRulesetString string `json: "extra-ruleset-string"`
|
ExtraRulesetString string `json: "extra-ruleset-string"`
|
||||||
|
@ -42,7 +45,7 @@ func (e1 *Config) Equal(e2 *Config) bool {
|
||||||
if e1 == nil || e2 == nil {
|
if e1 == nil || e2 == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if e1.Enabled != e2.Enabled {
|
if e1.Mode != e2.Mode {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if e1.Debug != e2.Debug {
|
if e1.Debug != e2.Debug {
|
||||||
|
@ -71,11 +74,16 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||||
// used to indicate if the location/s contains a fragment of
|
// used to indicate if the location/s contains a fragment of
|
||||||
// configuration to be included inside the paths of the rules
|
// configuration to be included inside the paths of the rules
|
||||||
func (a luarestywaf) Parse(ing *extensions.Ingress) (interface{}, error) {
|
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 {
|
if err != nil {
|
||||||
return &Config{}, err
|
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)
|
debug, _ := parser.GetBoolAnnotation("lua-resty-waf-debug", ing)
|
||||||
|
|
||||||
ignoredRuleSetsStr, _ := parser.GetStringAnnotation("lua-resty-waf-ignore-rulesets", ing)
|
ignoredRuleSetsStr, _ := parser.GetStringAnnotation("lua-resty-waf-ignore-rulesets", ing)
|
||||||
|
@ -88,7 +96,7 @@ func (a luarestywaf) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||||
extraRulesetString, _ := parser.GetStringAnnotation("lua-resty-waf-extra-rules", ing)
|
extraRulesetString, _ := parser.GetStringAnnotation("lua-resty-waf-extra-rules", ing)
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: enabled,
|
Mode: mode,
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
IgnoredRuleSets: ignoredRuleSets,
|
IgnoredRuleSets: ignoredRuleSets,
|
||||||
ExtraRulesetString: extraRulesetString,
|
ExtraRulesetString: extraRulesetString,
|
||||||
|
|
|
@ -43,18 +43,21 @@ func TestParse(t *testing.T) {
|
||||||
{nil, &Config{}},
|
{nil, &Config{}},
|
||||||
{map[string]string{}, &Config{}},
|
{map[string]string{}, &Config{}},
|
||||||
|
|
||||||
{map[string]string{luaRestyWAFAnnotation: "true"}, &Config{Enabled: true, Debug: false, IgnoredRuleSets: []string{}}},
|
{map[string]string{luaRestyWAFAnnotation: "active"}, &Config{Mode: "ACTIVE", Debug: false, IgnoredRuleSets: []string{}}},
|
||||||
{map[string]string{luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: false, Debug: false}},
|
{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: "active", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "ACTIVE", Debug: true, IgnoredRuleSets: []string{}}},
|
||||||
{map[string]string{luaRestyWAFAnnotation: "true", luaRestyWAFDebugAnnotation: "false"}, &Config{Enabled: true, Debug: false, IgnoredRuleSets: []string{}}},
|
{map[string]string{luaRestyWAFAnnotation: "active", luaRestyWAFDebugAnnotation: "false"}, &Config{Mode: "ACTIVE", Debug: false, IgnoredRuleSets: []string{}}},
|
||||||
{map[string]string{luaRestyWAFAnnotation: "false", luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: false, Debug: true, IgnoredRuleSets: []string{}}},
|
{map[string]string{luaRestyWAFAnnotation: "inactive", luaRestyWAFDebugAnnotation: "true"}, &Config{Mode: "INACTIVE", Debug: true, IgnoredRuleSets: []string{}}},
|
||||||
|
|
||||||
{map[string]string{
|
{map[string]string{
|
||||||
luaRestyWAFAnnotation: "true",
|
luaRestyWAFAnnotation: "active",
|
||||||
luaRestyWAFDebugAnnotation: "true",
|
luaRestyWAFDebugAnnotation: "true",
|
||||||
luaRestyWAFIgnoredRuleSetsAnnotation: "ruleset1, ruleset2 ruleset3, another.ruleset"},
|
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{
|
ing := &extensions.Ingress{
|
||||||
|
|
|
@ -119,6 +119,7 @@ var (
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
"shouldConfigureLuaRestyWAF": shouldConfigureLuaRestyWAF,
|
||||||
"buildLuaSharedDictionaries": buildLuaSharedDictionaries,
|
"buildLuaSharedDictionaries": buildLuaSharedDictionaries,
|
||||||
"buildLocation": buildLocation,
|
"buildLocation": buildLocation,
|
||||||
"buildAuthLocation": buildAuthLocation,
|
"buildAuthLocation": buildAuthLocation,
|
||||||
|
@ -168,6 +169,14 @@ func formatIP(input string) string {
|
||||||
return fmt.Sprintf("[%s]", input)
|
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 {
|
func buildLuaSharedDictionaries(s interface{}, dynamicConfigurationEnabled bool, disableLuaRestyWAF bool) string {
|
||||||
servers, ok := s.([]*ingress.Server)
|
servers, ok := s.([]*ingress.Server)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -191,7 +200,7 @@ func buildLuaSharedDictionaries(s interface{}, dynamicConfigurationEnabled bool,
|
||||||
luaRestyWAFEnabled := func() bool {
|
luaRestyWAFEnabled := func() bool {
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
for _, location := range server.Locations {
|
for _, location := range server.Locations {
|
||||||
if location.LuaRestyWAF.Enabled {
|
if len(location.LuaRestyWAF.Mode) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -321,7 +321,7 @@ func TestBuildLuaSharedDictionaries(t *testing.T) {
|
||||||
t.Errorf("expected to not include 'waf_storage' but got %s", config)
|
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)
|
config = buildLuaSharedDictionaries(servers, false, false)
|
||||||
if !strings.Contains(config, "lua_shared_dict waf_storage") {
|
if !strings.Contains(config, "lua_shared_dict waf_storage") {
|
||||||
t.Errorf("expected to configure 'waf_storage', but got %s", config)
|
t.Errorf("expected to configure 'waf_storage', but got %s", config)
|
||||||
|
|
|
@ -816,12 +816,12 @@ stream {
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
location {{ $path }} {
|
location {{ $path }} {
|
||||||
{{ if (and (not $all.Cfg.DisableLuaRestyWAF) $location.LuaRestyWAF.Enabled) }}
|
{{ if shouldConfigureLuaRestyWAF $all.Cfg.DisableLuaRestyWAF $location.LuaRestyWAF.Mode }}
|
||||||
access_by_lua_block {
|
access_by_lua_block {
|
||||||
local lua_resty_waf = require("resty.waf")
|
local lua_resty_waf = require("resty.waf")
|
||||||
local waf = lua_resty_waf:new()
|
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("storage_zone", "waf_storage")
|
||||||
waf:set_option("allowed_content_types", { "text/html", "text/json", "application/json" })
|
waf:set_option("allowed_content_types", { "text/html", "text/json", "application/json" })
|
||||||
waf:set_option("event_log_level", ngx.WARN)
|
waf:set_option("event_log_level", ngx.WARN)
|
||||||
|
@ -857,7 +857,7 @@ stream {
|
||||||
}
|
}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
log_by_lua_block {
|
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 lua_resty_waf = require "resty.waf"
|
||||||
local waf = lua_resty_waf:new()
|
local waf = lua_resty_waf:new()
|
||||||
waf:exec()
|
waf:exec()
|
||||||
|
|
|
@ -19,6 +19,7 @@ package annotations
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
@ -42,7 +43,7 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
|
||||||
Context("when lua-resty-waf is enabled", 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() {
|
It("should return 403 for a malicious request that matches a default WAF rule and 200 for other requests", func() {
|
||||||
host := "foo"
|
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)
|
url := fmt.Sprintf("%s?msg=<A href=\"http://mysite.com/\">XSS</A>", f.NginxHTTPURL)
|
||||||
resp, _, errs := gorequest.New().
|
resp, _, errs := gorequest.New().
|
||||||
|
@ -56,7 +57,7 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
|
||||||
It("should not apply ignored rulesets", func() {
|
It("should not apply ignored rulesets", func() {
|
||||||
host := "foo"
|
host := "foo"
|
||||||
createIngress(f, host, map[string]string{
|
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"})
|
"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)
|
url := fmt.Sprintf("%s?msg=<A href=\"http://mysite.com/\">XSS</A>", f.NginxHTTPURL)
|
||||||
|
@ -71,7 +72,7 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
|
||||||
It("should apply configured extra rules", func() {
|
It("should apply configured extra rules", func() {
|
||||||
host := "foo"
|
host := "foo"
|
||||||
createIngress(f, host, map[string]string{
|
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": `[=[
|
"nginx.ingress.kubernetes.io/lua-resty-waf-extra-rules": `[=[
|
||||||
{ "access": [
|
{ "access": [
|
||||||
{ "actions": { "disrupt" : "DENY" },
|
{ "actions": { "disrupt" : "DENY" },
|
||||||
|
@ -120,6 +121,24 @@ var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
|
||||||
Expect(len(errs)).Should(Equal(0))
|
Expect(len(errs)).Should(Equal(0))
|
||||||
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
|
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"))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue