Merge pull request #3309 from diazjf/modsecurity-location
Customize ModSecurity to be used in Locations
This commit is contained in:
commit
d97999c07d
9 changed files with 307 additions and 7 deletions
|
@ -95,6 +95,10 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|
|||
|[nginx.ingress.kubernetes.io/influxdb-host](#influxdb)|string|
|
||||
|[nginx.ingress.kubernetes.io/influxdb-server-name](#influxdb)|string|
|
||||
|[nginx.ingress.kubernetes.io/use-regex](#use-regex)|bool|
|
||||
|[nginx.ingress.kubernetes.io/enable-modsecurity](#modsecurity)|bool|
|
||||
|[nginx.ingress.kubernetes.io/enable-owasp-core-rules](#modsecurity)|bool|
|
||||
|[nginx.ingress.kubernetes.io/modsecurity-transaction-id](#modsecurity)|string|
|
||||
|
||||
|
||||
### Canary
|
||||
|
||||
|
@ -634,6 +638,29 @@ For details on how to write WAF rules, please refer to [https://github.com/p0pr0
|
|||
|
||||
[configmap]: ./configmap.md
|
||||
|
||||
### ModSecurity
|
||||
|
||||
[ModSecurity](http://modsecurity.org/) is an OpenSource Web Application firewall. It can be enabled for a particular set
|
||||
of ingress locations. The ModSecurity module must first be enabled by by enabling ModSecurity in the
|
||||
[ConfigMap](configmap.md#enable-modsecurity). Note this will enable ModSecurity for all paths, and each path
|
||||
must be disabled manually.
|
||||
|
||||
It can be enabled using the following annotation:
|
||||
```yaml
|
||||
nginx.ingress.kubernetes.io/enable-modsecurity: "true"
|
||||
```
|
||||
|
||||
You can enable the [OWASP Core Rule Set](https://www.modsecurity.org/CRS/Documentation/) by
|
||||
setting the following annotation:
|
||||
```yaml
|
||||
nginx.ingress.kubernetes.io/enable-owasp-core-rules: "true"
|
||||
```
|
||||
|
||||
You can pass transactionIDs from nginx by setting up the following:
|
||||
```yaml
|
||||
nginx.ingress.kubernetes.io/modsecurity-transaction-id: "$request_id"
|
||||
```
|
||||
|
||||
### InfluxDB
|
||||
|
||||
Using `influxdb-*` annotations we can monitor requests passing through a Location by sending them to an InfluxDB backend exposing the UDP socket
|
||||
|
@ -688,6 +715,4 @@ When this annotation is set to `true`, the case insensitive regular expression [
|
|||
|
||||
Additionally, if the [`rewrite-target` annotation](#rewrite) is used on any Ingress for a given host, then the case insensitive regular expression [location modifier](https://nginx.org/en/docs/http/ngx_http_core_module.html#location) will be enforced on ALL paths for a given host regardless of what Ingress they are defined on.
|
||||
|
||||
Please read about [ingress path matching](../ingress-path-matching.md) before using this modifier.
|
||||
|
||||
|
||||
Please read about [ingress path matching](../ingress-path-matching.md) before using this modifier.
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/golang/glog"
|
||||
"github.com/imdario/mergo"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/canary"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/sslcipher"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
|
@ -98,6 +99,7 @@ type Ingress struct {
|
|||
Logs log.Config
|
||||
LuaRestyWAF luarestywaf.Config
|
||||
InfluxDB influxdb.Config
|
||||
ModSecurity modsecurity.Config
|
||||
}
|
||||
|
||||
// Extractor defines the annotation parsers to be used in the extraction of annotations
|
||||
|
@ -140,6 +142,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
"LuaRestyWAF": luarestywaf.NewParser(cfg),
|
||||
"InfluxDB": influxdb.NewParser(cfg),
|
||||
"BackendProtocol": backendprotocol.NewParser(cfg),
|
||||
"ModSecurity": modsecurity.NewParser(cfg),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
88
internal/ingress/annotations/modsecurity/main.go
Normal file
88
internal/ingress/annotations/modsecurity/main.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2018 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 modsecurity
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
// Config contains the AuthSSLCert used for mutual authentication
|
||||
// and the configured ValidationDepth
|
||||
type Config struct {
|
||||
Enable bool `json:"enable-modsecurity"`
|
||||
OWASPRules bool `json:"enable-owasp-core-rules"`
|
||||
TransactionID string `json:"modsecurity-transaction-id"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Config types
|
||||
func (modsec1 *Config) Equal(modsec2 *Config) bool {
|
||||
if modsec1 == modsec2 {
|
||||
return true
|
||||
}
|
||||
if modsec1 == nil || modsec2 == nil {
|
||||
return false
|
||||
}
|
||||
if modsec1.Enable != modsec2.Enable {
|
||||
return false
|
||||
}
|
||||
if modsec1.OWASPRules != modsec2.OWASPRules {
|
||||
return false
|
||||
}
|
||||
if modsec1.TransactionID != modsec2.TransactionID {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NewParser creates a new ModSecurity annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return modSecurity{resolver}
|
||||
}
|
||||
|
||||
type modSecurity struct {
|
||||
r resolver.Resolver
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to enable ModSecurity in a particular location
|
||||
func (a modSecurity) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
|
||||
enableModSecurity, err := parser.GetBoolAnnotation("enable-modsecurity", ing)
|
||||
if err != nil {
|
||||
enableModSecurity = false
|
||||
}
|
||||
|
||||
owaspRules, err := parser.GetBoolAnnotation("enable-owasp-core-rules", ing)
|
||||
if err != nil {
|
||||
owaspRules = false
|
||||
}
|
||||
|
||||
transactionID, err := parser.GetStringAnnotation("modsecurity-transaction-id", ing)
|
||||
if err != nil {
|
||||
transactionID = ""
|
||||
}
|
||||
|
||||
return Config{
|
||||
Enable: enableModSecurity,
|
||||
OWASPRules: owaspRules,
|
||||
TransactionID: transactionID,
|
||||
}, nil
|
||||
}
|
73
internal/ingress/annotations/modsecurity/main_test.go
Normal file
73
internal/ingress/annotations/modsecurity/main_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2018 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 modsecurity
|
||||
|
||||
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) {
|
||||
enable := parser.GetAnnotationWithPrefix("enable-modsecurity")
|
||||
owasp := parser.GetAnnotationWithPrefix("enable-owasp-core-rules")
|
||||
transID := parser.GetAnnotationWithPrefix("modsecurity-transaction-id")
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected Config
|
||||
}{
|
||||
{map[string]string{enable: "true"}, Config{true, false, ""}},
|
||||
{map[string]string{enable: "false"}, Config{false, false, ""}},
|
||||
{map[string]string{enable: ""}, Config{false, false, ""}},
|
||||
|
||||
{map[string]string{owasp: "true"}, Config{false, true, ""}},
|
||||
{map[string]string{owasp: "false"}, Config{false, false, ""}},
|
||||
{map[string]string{owasp: ""}, Config{false, false, ""}},
|
||||
|
||||
{map[string]string{transID: "ok"}, Config{false, false, "ok"}},
|
||||
{map[string]string{transID: ""}, Config{false, false, ""}},
|
||||
|
||||
{map[string]string{}, Config{false, false, ""}},
|
||||
{nil, Config{false, false, ""}},
|
||||
}
|
||||
|
||||
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)
|
||||
if result != testCase.expected {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -355,6 +355,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([]
|
|||
loc.DefaultBackend = anns.DefaultBackend
|
||||
loc.BackendProtocol = anns.BackendProtocol
|
||||
loc.CustomHTTPErrors = anns.CustomHTTPErrors
|
||||
loc.ModSecurity = anns.ModSecurity
|
||||
|
||||
if loc.Redirect.FromToWWW {
|
||||
server.RedirectFromToWWW = true
|
||||
|
@ -396,6 +397,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([]
|
|||
DefaultBackend: anns.DefaultBackend,
|
||||
BackendProtocol: anns.BackendProtocol,
|
||||
CustomHTTPErrors: anns.CustomHTTPErrors,
|
||||
ModSecurity: anns.ModSecurity,
|
||||
}
|
||||
|
||||
if loc.Redirect.FromToWWW {
|
||||
|
@ -848,6 +850,7 @@ func (n *NGINXController) createServers(data []*extensions.Ingress,
|
|||
defLoc.LuaRestyWAF = anns.LuaRestyWAF
|
||||
defLoc.InfluxDB = anns.InfluxDB
|
||||
defLoc.BackendProtocol = anns.BackendProtocol
|
||||
defLoc.ModSecurity = anns.ModSecurity
|
||||
} else {
|
||||
glog.V(3).Infof("Ingress %q defines both a backend and rules. Using its backend as default upstream for all its rules.",
|
||||
ingKey)
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/auth"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/authreq"
|
||||
|
@ -280,6 +281,9 @@ type Location struct {
|
|||
// CustomHTTPErrors specifies the error codes that should be intercepted.
|
||||
// +optional
|
||||
CustomHTTPErrors []int `json:"custom-http-errors"`
|
||||
// ModSecurity allows to enable and configure modsecurity
|
||||
// +optional
|
||||
ModSecurity modsecurity.Config `json:"modsecurity"`
|
||||
}
|
||||
|
||||
// SSLPassthroughBackend describes a SSL upstream server configured
|
||||
|
|
|
@ -380,6 +380,10 @@ func (l1 *Location) Equal(l2 *Location) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
if !(&l1.ModSecurity).Equal(&l2.ModSecurity) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -16,9 +16,7 @@ pid /tmp/nginx.pid;
|
|||
load_module /etc/nginx/modules/ngx_http_geoip2_module.so;
|
||||
{{ end }}
|
||||
|
||||
{{ if $cfg.EnableModsecurity }}
|
||||
load_module /etc/nginx/modules/ngx_http_modsecurity_module.so;
|
||||
{{ end }}
|
||||
|
||||
{{ if $cfg.EnableOpentracing }}
|
||||
load_module /etc/nginx/modules/ngx_http_opentracing_module.so;
|
||||
|
@ -1020,13 +1018,17 @@ stream {
|
|||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $all.Cfg.EnableModsecurity }}
|
||||
{{ if (or $location.ModSecurity.Enable $all.Cfg.EnableModsecurity) }}
|
||||
modsecurity on;
|
||||
|
||||
modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
|
||||
{{ if $all.Cfg.EnableOWASPCoreRules }}
|
||||
{{ if (or $location.ModSecurity.OWASPRules $all.Cfg.EnableOWASPCoreRules) }}
|
||||
modsecurity_rules_file /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf;
|
||||
{{ end }}
|
||||
|
||||
{{ if (not (empty $location.ModSecurity.TransactionID)) }}
|
||||
modsecurity_transaction_id "{{ $location.ModSecurity.TransactionID }}";
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if isLocationAllowed $location }}
|
||||
|
|
98
test/e2e/annotations/modsecurity.go
Normal file
98
test/e2e/annotations/modsecurity.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
Copyright 2018 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 (
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
"k8s.io/ingress-nginx/test/e2e/framework"
|
||||
)
|
||||
|
||||
var _ = framework.IngressNginxDescribe("Annotations - ModSecurityLocation", func() {
|
||||
f := framework.NewDefaultFramework("modsecuritylocation")
|
||||
|
||||
BeforeEach(func() {
|
||||
f.NewEchoDeployment()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
f.UpdateNginxConfigMapData("enable-modsecurity", "false")
|
||||
})
|
||||
|
||||
It("should enable modsecurity", func() {
|
||||
host := "modsecurity.foo.com"
|
||||
nameSpace := f.IngressController.Namespace
|
||||
|
||||
annotations := map[string]string{
|
||||
"nginx.ingress.kubernetes.io/enable-modsecurity": "true",
|
||||
}
|
||||
|
||||
f.UpdateNginxConfigMapData("enable-modsecurity", "true")
|
||||
|
||||
ing := framework.NewSingleIngress(host, "/", host, nameSpace, "http-svc", 80, &annotations)
|
||||
f.EnsureIngress(ing)
|
||||
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return strings.Contains(server, "modsecurity on;") &&
|
||||
strings.Contains(server, "modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;")
|
||||
})
|
||||
})
|
||||
|
||||
It("should enable modsecurity with transaction ID and OWASP rules", func() {
|
||||
host := "modsecurity.foo.com"
|
||||
nameSpace := f.IngressController.Namespace
|
||||
|
||||
annotations := map[string]string{
|
||||
"nginx.ingress.kubernetes.io/enable-modsecurity": "true",
|
||||
"nginx.ingress.kubernetes.io/enable-owasp-core-rules": "true",
|
||||
"nginx.ingress.kubernetes.io/modsecurity-transaction-id": "modsecurity-$request_id",
|
||||
}
|
||||
|
||||
f.UpdateNginxConfigMapData("enable-modsecurity", "true")
|
||||
|
||||
ing := framework.NewSingleIngress(host, "/", host, nameSpace, "http-svc", 80, &annotations)
|
||||
f.EnsureIngress(ing)
|
||||
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return strings.Contains(server, "modsecurity on;") &&
|
||||
strings.Contains(server, "modsecurity_rules_file /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf;") &&
|
||||
strings.Contains(server, "modsecurity_transaction_id \"modsecurity-$request_id\";")
|
||||
})
|
||||
})
|
||||
|
||||
It("should disable modsecurity", func() {
|
||||
host := "modsecurity.foo.com"
|
||||
nameSpace := f.IngressController.Namespace
|
||||
|
||||
annotations := map[string]string{
|
||||
"nginx.ingress.kubernetes.io/enable-modsecurity": "false",
|
||||
}
|
||||
|
||||
f.UpdateNginxConfigMapData("enable-modsecurity", "false")
|
||||
|
||||
ing := framework.NewSingleIngress(host, "/", host, nameSpace, "http-svc", 80, &annotations)
|
||||
f.EnsureIngress(ing)
|
||||
|
||||
f.WaitForNginxServer(host,
|
||||
func(server string) bool {
|
||||
return !strings.Contains(server, "modsecurity on;")
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue