lua-resty-waf controller (#2304)
This commit is contained in:
parent
b17ed7b6fd
commit
a6fe800a47
15 changed files with 455 additions and 37 deletions
2
Makefile
2
Makefile
|
@ -50,7 +50,7 @@ IMAGE = $(REGISTRY)/$(IMGNAME)
|
||||||
MULTI_ARCH_IMG = $(IMAGE)-$(ARCH)
|
MULTI_ARCH_IMG = $(IMAGE)-$(ARCH)
|
||||||
|
|
||||||
# Set default base image dynamically for each arch
|
# Set default base image dynamically for each arch
|
||||||
BASEIMAGE?=quay.io/kubernetes-ingress-controller/nginx-$(ARCH):0.38
|
BASEIMAGE?=quay.io/kubernetes-ingress-controller/nginx-$(ARCH):0.40
|
||||||
|
|
||||||
ifeq ($(ARCH),arm)
|
ifeq ($(ARCH),arm)
|
||||||
QEMUARCH=arm
|
QEMUARCH=arm
|
||||||
|
|
|
@ -66,6 +66,8 @@ 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-debug](#lua-resty-waf)|"true" or "false"|
|
||||||
|
|
||||||
**Note:** all the values must be a string. In case of booleans or number it must be quoted.
|
**Note:** all the values must be a string. In case of booleans or number it must be quoted.
|
||||||
|
|
||||||
|
@ -463,3 +465,14 @@ In some scenarios could be required to disable NGINX access logs. To enable this
|
||||||
```yaml
|
```yaml
|
||||||
nginx.ingress.kubernetes.io/enable-access-log: "false"
|
nginx.ingress.kubernetes.io/enable-access-log: "false"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Lua Resty WAF
|
||||||
|
|
||||||
|
Using `lua-resty-waf-*` annotations we can enable and control [lua-resty-waf](https://github.com/p0pr0ck5/lua-resty-waf) per location.
|
||||||
|
Following configuration will enable WAF for the paths defined in the corresponding ingress:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
nginx.ingress.kubernetes.io/lua-resty-waf: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -37,6 +37,7 @@ import (
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/loadbalancing"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/loadbalancing"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/annotations/luarestywaf"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/portinredirect"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/portinredirect"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
|
||||||
|
@ -93,6 +94,7 @@ type Ingress struct {
|
||||||
SSLCiphers string
|
SSLCiphers string
|
||||||
Logs log.Config
|
Logs log.Config
|
||||||
GRPC bool
|
GRPC bool
|
||||||
|
LuaRestyWAF luarestywaf.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extractor defines the annotation parsers to be used in the extraction of annotations
|
// Extractor defines the annotation parsers to be used in the extraction of annotations
|
||||||
|
@ -133,6 +135,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
||||||
"SSLCiphers": sslcipher.NewParser(cfg),
|
"SSLCiphers": sslcipher.NewParser(cfg),
|
||||||
"Logs": log.NewParser(cfg),
|
"Logs": log.NewParser(cfg),
|
||||||
"GRPC": grpc.NewParser(cfg),
|
"GRPC": grpc.NewParser(cfg),
|
||||||
|
"LuaRestyWAF": luarestywaf.NewParser(cfg),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
74
internal/ingress/annotations/luarestywaf/main.go
Normal file
74
internal/ingress/annotations/luarestywaf/main.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 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 luarestywaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
extensions "k8s.io/api/extensions/v1beta1"
|
||||||
|
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config returns lua-resty-waf configuration for an Ingress rule
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Debug bool `json:"debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal tests for equality between two Config types
|
||||||
|
func (e1 *Config) Equal(e2 *Config) bool {
|
||||||
|
if e1 == e2 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if e1 == nil || e2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if e1.Enabled != e2.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if e1.Debug != e2.Debug {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type luarestywaf struct {
|
||||||
|
r resolver.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser creates a new CORS annotation parser
|
||||||
|
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||||
|
return luarestywaf{r}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 luarestywaf) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||||
|
enabled, err := parser.GetBoolAnnotation("lua-resty-waf", ing)
|
||||||
|
if err != nil {
|
||||||
|
return &Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
debug, _ := parser.GetBoolAnnotation("lua-resty-waf-debug", ing)
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
Enabled: enabled,
|
||||||
|
Debug: debug,
|
||||||
|
}, nil
|
||||||
|
}
|
69
internal/ingress/annotations/luarestywaf/main_test.go
Normal file
69
internal/ingress/annotations/luarestywaf/main_test.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 luarestywaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
api "k8s.io/api/core/v1"
|
||||||
|
extensions "k8s.io/api/extensions/v1beta1"
|
||||||
|
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
luaRestyWAFAnnotation := parser.GetAnnotationWithPrefix("lua-resty-waf")
|
||||||
|
luaRestyWAFDebugAnnotation := parser.GetAnnotationWithPrefix("lua-resty-waf-debug")
|
||||||
|
|
||||||
|
ap := NewParser(&resolver.Mock{})
|
||||||
|
if ap == nil {
|
||||||
|
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
annotations map[string]string
|
||||||
|
expected *Config
|
||||||
|
}{
|
||||||
|
{nil, &Config{}},
|
||||||
|
{map[string]string{}, &Config{}},
|
||||||
|
|
||||||
|
{map[string]string{luaRestyWAFAnnotation: "true"}, &Config{Enabled: true, Debug: false}},
|
||||||
|
{map[string]string{luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: false, Debug: false}},
|
||||||
|
|
||||||
|
{map[string]string{luaRestyWAFAnnotation: "true", luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: true, Debug: true}},
|
||||||
|
{map[string]string{luaRestyWAFAnnotation: "true", luaRestyWAFDebugAnnotation: "false"}, &Config{Enabled: true, Debug: false}},
|
||||||
|
{map[string]string{luaRestyWAFAnnotation: "false", luaRestyWAFDebugAnnotation: "true"}, &Config{Enabled: false, Debug: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
ing := &extensions.Ingress{
|
||||||
|
ObjectMeta: meta_v1.ObjectMeta{
|
||||||
|
Name: "foo",
|
||||||
|
Namespace: api.NamespaceDefault,
|
||||||
|
},
|
||||||
|
Spec: extensions.IngressSpec{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
ing.SetAnnotations(testCase.annotations)
|
||||||
|
result, _ := ap.Parse(ing)
|
||||||
|
config := result.(*Config)
|
||||||
|
if !config.Equal(testCase.expected) {
|
||||||
|
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -503,6 +503,10 @@ type Configuration struct {
|
||||||
// NoAuthLocations is a comma-separated list of locations that
|
// NoAuthLocations is a comma-separated list of locations that
|
||||||
// should not get authenticated
|
// should not get authenticated
|
||||||
NoAuthLocations string `json:"no-auth-locations"`
|
NoAuthLocations string `json:"no-auth-locations"`
|
||||||
|
|
||||||
|
// DisableLuaRestyWAF disables lua-resty-waf globally regardless
|
||||||
|
// of whether there's an ingress that has enabled the WAF using annotation
|
||||||
|
DisableLuaRestyWAF bool `json:"disable-lua-resty-waf"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefault returns the default nginx configuration
|
// NewDefault returns the default nginx configuration
|
||||||
|
|
|
@ -457,6 +457,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([]
|
||||||
loc.Connection = anns.Connection
|
loc.Connection = anns.Connection
|
||||||
loc.Logs = anns.Logs
|
loc.Logs = anns.Logs
|
||||||
loc.GRPC = anns.GRPC
|
loc.GRPC = anns.GRPC
|
||||||
|
loc.LuaRestyWAF = anns.LuaRestyWAF
|
||||||
|
|
||||||
if loc.Redirect.FromToWWW {
|
if loc.Redirect.FromToWWW {
|
||||||
server.RedirectFromToWWW = true
|
server.RedirectFromToWWW = true
|
||||||
|
@ -492,6 +493,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([]
|
||||||
Connection: anns.Connection,
|
Connection: anns.Connection,
|
||||||
Logs: anns.Logs,
|
Logs: anns.Logs,
|
||||||
GRPC: anns.GRPC,
|
GRPC: anns.GRPC,
|
||||||
|
LuaRestyWAF: anns.LuaRestyWAF,
|
||||||
}
|
}
|
||||||
|
|
||||||
if loc.Redirect.FromToWWW {
|
if loc.Redirect.FromToWWW {
|
||||||
|
@ -928,6 +930,7 @@ func (n *NGINXController) createServers(data []*extensions.Ingress,
|
||||||
defLoc.Whitelist = anns.Whitelist
|
defLoc.Whitelist = anns.Whitelist
|
||||||
defLoc.Denied = anns.Denied
|
defLoc.Denied = anns.Denied
|
||||||
defLoc.GRPC = anns.GRPC
|
defLoc.GRPC = anns.GRPC
|
||||||
|
defLoc.LuaRestyWAF = anns.LuaRestyWAF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,29 +119,30 @@ var (
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
"buildLocation": buildLocation,
|
"buildLuaSharedDictionaries": buildLuaSharedDictionaries,
|
||||||
"buildAuthLocation": buildAuthLocation,
|
"buildLocation": buildLocation,
|
||||||
"buildAuthResponseHeaders": buildAuthResponseHeaders,
|
"buildAuthLocation": buildAuthLocation,
|
||||||
"buildLoadBalancingConfig": buildLoadBalancingConfig,
|
"buildAuthResponseHeaders": buildAuthResponseHeaders,
|
||||||
"buildProxyPass": buildProxyPass,
|
"buildLoadBalancingConfig": buildLoadBalancingConfig,
|
||||||
"filterRateLimits": filterRateLimits,
|
"buildProxyPass": buildProxyPass,
|
||||||
"buildRateLimitZones": buildRateLimitZones,
|
"filterRateLimits": filterRateLimits,
|
||||||
"buildRateLimit": buildRateLimit,
|
"buildRateLimitZones": buildRateLimitZones,
|
||||||
"buildResolvers": buildResolvers,
|
"buildRateLimit": buildRateLimit,
|
||||||
"buildUpstreamName": buildUpstreamName,
|
"buildResolvers": buildResolvers,
|
||||||
"isLocationInLocationList": isLocationInLocationList,
|
"buildUpstreamName": buildUpstreamName,
|
||||||
"isLocationAllowed": isLocationAllowed,
|
"isLocationInLocationList": isLocationInLocationList,
|
||||||
"buildLogFormatUpstream": buildLogFormatUpstream,
|
"isLocationAllowed": isLocationAllowed,
|
||||||
"buildDenyVariable": buildDenyVariable,
|
"buildLogFormatUpstream": buildLogFormatUpstream,
|
||||||
"getenv": os.Getenv,
|
"buildDenyVariable": buildDenyVariable,
|
||||||
"contains": strings.Contains,
|
"getenv": os.Getenv,
|
||||||
"hasPrefix": strings.HasPrefix,
|
"contains": strings.Contains,
|
||||||
"hasSuffix": strings.HasSuffix,
|
"hasPrefix": strings.HasPrefix,
|
||||||
"toUpper": strings.ToUpper,
|
"hasSuffix": strings.HasSuffix,
|
||||||
"toLower": strings.ToLower,
|
"toUpper": strings.ToUpper,
|
||||||
"formatIP": formatIP,
|
"toLower": strings.ToLower,
|
||||||
"buildNextUpstream": buildNextUpstream,
|
"formatIP": formatIP,
|
||||||
"getIngressInformation": getIngressInformation,
|
"buildNextUpstream": buildNextUpstream,
|
||||||
|
"getIngressInformation": getIngressInformation,
|
||||||
"serverConfig": func(all config.TemplateConfig, server *ingress.Server) interface{} {
|
"serverConfig": func(all config.TemplateConfig, server *ingress.Server) interface{} {
|
||||||
return struct{ First, Second interface{} }{all, server}
|
return struct{ First, Second interface{} }{all, server}
|
||||||
},
|
},
|
||||||
|
@ -167,6 +168,47 @@ func formatIP(input string) string {
|
||||||
return fmt.Sprintf("[%s]", input)
|
return fmt.Sprintf("[%s]", input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildLuaSharedDictionaries(s interface{}, dynamicConfigurationEnabled bool, disableLuaRestyWAF bool) string {
|
||||||
|
servers, ok := s.([]*ingress.Server)
|
||||||
|
if !ok {
|
||||||
|
glog.Errorf("expected an '[]*ingress.Server' type but %T was returned", s)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
out := []string{}
|
||||||
|
|
||||||
|
if dynamicConfigurationEnabled {
|
||||||
|
out = append(out,
|
||||||
|
"lua_shared_dict configuration_data 5M",
|
||||||
|
"lua_shared_dict round_robin_state 1M",
|
||||||
|
"lua_shared_dict locks 512k",
|
||||||
|
"lua_shared_dict balancer_ewma 1M",
|
||||||
|
"lua_shared_dict balancer_ewma_last_touched_at 1M",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !disableLuaRestyWAF {
|
||||||
|
luaRestyWAFEnabled := func() bool {
|
||||||
|
for _, server := range servers {
|
||||||
|
for _, location := range server.Locations {
|
||||||
|
if location.LuaRestyWAF.Enabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()
|
||||||
|
if luaRestyWAFEnabled {
|
||||||
|
out = append(out, "lua_shared_dict waf_storage 64M")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(out, ";\n\r") + ";"
|
||||||
|
}
|
||||||
|
|
||||||
// buildResolvers returns the resolvers reading the /etc/resolv.conf file
|
// buildResolvers returns the resolvers reading the /etc/resolv.conf file
|
||||||
func buildResolvers(res interface{}, disableIpv6 interface{}) string {
|
func buildResolvers(res interface{}, disableIpv6 interface{}) string {
|
||||||
// NGINX need IPV6 addresses to be surrounded by brackets
|
// NGINX need IPV6 addresses to be surrounded by brackets
|
||||||
|
|
|
@ -32,6 +32,7 @@ import (
|
||||||
"k8s.io/ingress-nginx/internal/file"
|
"k8s.io/ingress-nginx/internal/file"
|
||||||
"k8s.io/ingress-nginx/internal/ingress"
|
"k8s.io/ingress-nginx/internal/ingress"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/authreq"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/authreq"
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/annotations/luarestywaf"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/rewrite"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/rewrite"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/controller/config"
|
"k8s.io/ingress-nginx/internal/ingress/controller/config"
|
||||||
)
|
)
|
||||||
|
@ -296,6 +297,46 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestBuildLuaSharedDictionaries(t *testing.T) {
|
||||||
|
servers := []*ingress.Server{
|
||||||
|
{
|
||||||
|
Hostname: "foo.bar",
|
||||||
|
Locations: []*ingress.Location{{Path: "/", LuaRestyWAF: luarestywaf.Config{}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Hostname: "another.host",
|
||||||
|
Locations: []*ingress.Location{{Path: "/", LuaRestyWAF: luarestywaf.Config{}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
config := buildLuaSharedDictionaries(servers, false, false)
|
||||||
|
if config != "" {
|
||||||
|
t.Errorf("expected to not configure any lua shared dictionary, but generated %s", config)
|
||||||
|
}
|
||||||
|
config = buildLuaSharedDictionaries(servers, true, false)
|
||||||
|
if !strings.Contains(config, "lua_shared_dict configuration_data") {
|
||||||
|
t.Errorf("expected to include 'configuration_data' but got %s", config)
|
||||||
|
}
|
||||||
|
if strings.Contains(config, "waf_storage") {
|
||||||
|
t.Errorf("expected to not include 'waf_storage' but got %s", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
servers[1].Locations[0].LuaRestyWAF = luarestywaf.Config{Enabled: true}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
config = buildLuaSharedDictionaries(servers, true, false)
|
||||||
|
if !strings.Contains(config, "lua_shared_dict waf_storage") {
|
||||||
|
t.Errorf("expected to configure 'waf_storage', but got %s", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
config = buildLuaSharedDictionaries(servers, false, true)
|
||||||
|
if config != "" {
|
||||||
|
t.Errorf("expected to not configure any lua shared dictionary, but generated %s", config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormatIP(t *testing.T) {
|
func TestFormatIP(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
Input, Output string
|
Input, Output string
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/cors"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/cors"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||||
|
"k8s.io/ingress-nginx/internal/ingress/annotations/luarestywaf"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ratelimit"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/ratelimit"
|
||||||
"k8s.io/ingress-nginx/internal/ingress/annotations/redirect"
|
"k8s.io/ingress-nginx/internal/ingress/annotations/redirect"
|
||||||
|
@ -268,6 +269,8 @@ type Location struct {
|
||||||
// GRPC indicates if the kubernetes service exposes a gRPC interface
|
// GRPC indicates if the kubernetes service exposes a gRPC interface
|
||||||
// By default this is false
|
// By default this is false
|
||||||
GRPC bool `json:"grpc"`
|
GRPC bool `json:"grpc"`
|
||||||
|
// LuaRestyWAF contains parameters to configure lua-resty-waf
|
||||||
|
LuaRestyWAF luarestywaf.Config `json:"luaRestyWAF"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSLPassthroughBackend describes a SSL upstream server configured
|
// SSLPassthroughBackend describes a SSL upstream server configured
|
||||||
|
|
|
@ -385,6 +385,9 @@ func (l1 *Location) Equal(l2 *Location) bool {
|
||||||
if l1.GRPC != l2.GRPC {
|
if l1.GRPC != l2.GRPC {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if !(&l1.LuaRestyWAF).Equal(&l2.LuaRestyWAF) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,10 @@ end
|
||||||
|
|
||||||
local function get_current_lb_alg()
|
local function get_current_lb_alg()
|
||||||
local backend = get_current_backend()
|
local backend = get_current_backend()
|
||||||
|
if not backend then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
return backend["load-balance"] or DEFAULT_LB_ALG
|
return backend["load-balance"] or DEFAULT_LB_ALG
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -36,20 +36,19 @@ events {
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
{{ if $all.DynamicConfigurationEnabled }}
|
|
||||||
lua_package_cpath "/usr/local/lib/lua/?.so;/usr/lib/x86_64-linux-gnu/lua/5.1/?.so;;";
|
lua_package_cpath "/usr/local/lib/lua/?.so;/usr/lib/x86_64-linux-gnu/lua/5.1/?.so;;";
|
||||||
lua_package_path "/etc/nginx/lua/?.lua;/etc/nginx/lua/vendor/?.lua;/usr/local/lib/lua/?.lua;;";
|
lua_package_path "/etc/nginx/lua/?.lua;/etc/nginx/lua/vendor/?.lua;/usr/local/lib/lua/?.lua;;";
|
||||||
|
|
||||||
lua_shared_dict configuration_data 5M;
|
{{ buildLuaSharedDictionaries $servers $all.DynamicConfigurationEnabled $all.Cfg.DisableLuaRestyWAF }}
|
||||||
lua_shared_dict round_robin_state 1M;
|
|
||||||
lua_shared_dict locks 512k;
|
|
||||||
lua_shared_dict balancer_ewma 1M;
|
|
||||||
lua_shared_dict balancer_ewma_last_touched_at 1M;
|
|
||||||
|
|
||||||
init_by_lua_block {
|
init_by_lua_block {
|
||||||
require("resty.core")
|
require("resty.core")
|
||||||
collectgarbage("collect")
|
collectgarbage("collect")
|
||||||
|
|
||||||
|
local lua_resty_waf = require("resty.waf")
|
||||||
|
lua_resty_waf.init()
|
||||||
|
|
||||||
|
{{ if $all.DynamicConfigurationEnabled }}
|
||||||
-- init modules
|
-- init modules
|
||||||
local ok, res
|
local ok, res
|
||||||
|
|
||||||
|
@ -66,8 +65,10 @@ http {
|
||||||
else
|
else
|
||||||
balancer = res
|
balancer = res
|
||||||
end
|
end
|
||||||
|
{{ end }}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{ if $all.DynamicConfigurationEnabled }}
|
||||||
init_worker_by_lua_block {
|
init_worker_by_lua_block {
|
||||||
balancer.init_worker()
|
balancer.init_worker()
|
||||||
}
|
}
|
||||||
|
@ -815,6 +816,48 @@ stream {
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
location {{ $path }} {
|
location {{ $path }} {
|
||||||
|
{{ if (and (not $all.Cfg.DisableLuaRestyWAF) $location.LuaRestyWAF.Enabled) }}
|
||||||
|
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("storage_zone", "waf_storage")
|
||||||
|
waf:set_option("allowed_content_types", { "text/html", "text/json", "application/json" })
|
||||||
|
waf:set_option("event_log_level", ngx.WARN)
|
||||||
|
|
||||||
|
{{ if $location.LuaRestyWAF.Debug }}
|
||||||
|
waf:set_option("debug", true)
|
||||||
|
waf:set_option("event_log_request_arguments", true)
|
||||||
|
waf:set_option("event_log_request_body", true)
|
||||||
|
waf:set_option("event_log_request_headers", true)
|
||||||
|
waf:set_option("req_tid_header", true)
|
||||||
|
waf:set_option("res_tid_header", true)
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
waf:exec()
|
||||||
|
}
|
||||||
|
header_filter_by_lua_block {
|
||||||
|
local lua_resty_waf = require "resty.waf"
|
||||||
|
local waf = lua_resty_waf:new()
|
||||||
|
waf:exec()
|
||||||
|
}
|
||||||
|
body_filter_by_lua_block {
|
||||||
|
local lua_resty_waf = require "resty.waf"
|
||||||
|
local waf = lua_resty_waf:new()
|
||||||
|
waf:exec()
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
log_by_lua_block {
|
||||||
|
{{ if (and (not $all.Cfg.DisableLuaRestyWAF) $location.LuaRestyWAF.Enabled) }}
|
||||||
|
local lua_resty_waf = require "resty.waf"
|
||||||
|
local waf = lua_resty_waf:new()
|
||||||
|
waf:exec()
|
||||||
|
{{ end }}
|
||||||
|
{{ if $all.DynamicConfigurationEnabled}}
|
||||||
|
balancer.call()
|
||||||
|
{{ end }}
|
||||||
|
}
|
||||||
{{ if (and (not (empty $server.SSLCertificate)) $all.Cfg.HSTS) }}
|
{{ if (and (not (empty $server.SSLCertificate)) $all.Cfg.HSTS) }}
|
||||||
if ($scheme = https) {
|
if ($scheme = https) {
|
||||||
more_set_headers "Strict-Transport-Security: max-age={{ $all.Cfg.HSTSMaxAge }}{{ if $all.Cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }}{{ if $all.Cfg.HSTSPreload }}; preload{{ end }}";
|
more_set_headers "Strict-Transport-Security: max-age={{ $all.Cfg.HSTSMaxAge }}{{ if $all.Cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }}{{ if $all.Cfg.HSTSPreload }}; preload{{ end }}";
|
||||||
|
@ -1009,11 +1052,6 @@ stream {
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if not (empty $location.Backend) }}
|
{{ if not (empty $location.Backend) }}
|
||||||
{{ if $all.DynamicConfigurationEnabled}}
|
|
||||||
log_by_lua_block {
|
|
||||||
balancer.call()
|
|
||||||
}
|
|
||||||
{{ end }}
|
|
||||||
{{ buildProxyPass $server.Hostname $all.Backends $location $all.DynamicConfigurationEnabled }}
|
{{ buildProxyPass $server.Hostname $all.Backends $location $all.DynamicConfigurationEnabled }}
|
||||||
{{ if (or (eq $location.Proxy.ProxyRedirectFrom "default") (eq $location.Proxy.ProxyRedirectFrom "off")) }}
|
{{ if (or (eq $location.Proxy.ProxyRedirectFrom "default") (eq $location.Proxy.ProxyRedirectFrom "off")) }}
|
||||||
proxy_redirect {{ $location.Proxy.ProxyRedirectFrom }};
|
proxy_redirect {{ $location.Proxy.ProxyRedirectFrom }};
|
||||||
|
|
121
test/e2e/annotations/luarestywaf.go
Normal file
121
test/e2e/annotations/luarestywaf.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/parnurzeal/gorequest"
|
||||||
|
|
||||||
|
v1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
|
||||||
|
"k8s.io/ingress-nginx/test/e2e/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = framework.IngressNginxDescribe("Annotations - lua-resty-waf", func() {
|
||||||
|
f := framework.NewDefaultFramework("luarestywaf")
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
err := f.NewEchoDeployment()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
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"})
|
||||||
|
|
||||||
|
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.StatusForbidden))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when lua-resty-waf is not enabled", func() {
|
||||||
|
It("should return 200 even for a malicious request", func() {
|
||||||
|
host := "foo"
|
||||||
|
createIngress(f, host, map[string]string{})
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
func createIngress(f *framework.Framework, host string, annotations map[string]string) {
|
||||||
|
ing, err := f.EnsureIngress(&v1beta1.Ingress{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: host,
|
||||||
|
Namespace: f.Namespace.Name,
|
||||||
|
Annotations: annotations,
|
||||||
|
},
|
||||||
|
Spec: v1beta1.IngressSpec{
|
||||||
|
Rules: []v1beta1.IngressRule{
|
||||||
|
{
|
||||||
|
Host: host,
|
||||||
|
IngressRuleValue: v1beta1.IngressRuleValue{
|
||||||
|
HTTP: &v1beta1.HTTPIngressRuleValue{
|
||||||
|
Paths: []v1beta1.HTTPIngressPath{
|
||||||
|
{
|
||||||
|
Path: "/",
|
||||||
|
Backend: v1beta1.IngressBackend{
|
||||||
|
ServiceName: "http-svc",
|
||||||
|
ServicePort: intstr.FromInt(80),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(ing).NotTo(BeNil())
|
||||||
|
|
||||||
|
err = f.WaitForNginxServer(host,
|
||||||
|
func(server string) bool {
|
||||||
|
return Expect(server).Should(ContainSubstring("server_name foo")) &&
|
||||||
|
Expect(server).ShouldNot(ContainSubstring("return 503"))
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
resp, body, errs := gorequest.New().
|
||||||
|
Get(f.NginxHTTPURL).
|
||||||
|
Set("Host", host).
|
||||||
|
End()
|
||||||
|
|
||||||
|
Expect(len(errs)).Should(Equal(0))
|
||||||
|
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
|
||||||
|
Expect(body).Should(ContainSubstring(fmt.Sprintf("host=%v", host)))
|
||||||
|
}
|
Loading…
Reference in a new issue