From 519560084112f699d66e44f1818ac3bc5a82a875 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Date: Sat, 3 Nov 2018 23:14:27 -0500 Subject: [PATCH] Allows ModSecurity to be configured per location The following annotations will be added: - enable-modsecurity - enable-owasp-core-rules - modsecurity-transaction-id Fixes #3167 --- .../nginx-configuration/annotations.md | 31 +++++- internal/ingress/annotations/annotations.go | 3 + .../ingress/annotations/modsecurity/main.go | 88 +++++++++++++++++ .../annotations/modsecurity/main_test.go | 73 ++++++++++++++ internal/ingress/controller/controller.go | 3 + internal/ingress/types.go | 4 + internal/ingress/types_equals.go | 4 + rootfs/etc/nginx/template/nginx.tmpl | 10 +- test/e2e/annotations/modsecurity.go | 98 +++++++++++++++++++ 9 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 internal/ingress/annotations/modsecurity/main.go create mode 100644 internal/ingress/annotations/modsecurity/main_test.go create mode 100644 test/e2e/annotations/modsecurity.go diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md index 76ff807b9..6a499b065 100644 --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -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. \ No newline at end of file diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go index 9cd1f12d3..f0ab709cd 100644 --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -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), }, } } diff --git a/internal/ingress/annotations/modsecurity/main.go b/internal/ingress/annotations/modsecurity/main.go new file mode 100644 index 000000000..cdb93b265 --- /dev/null +++ b/internal/ingress/annotations/modsecurity/main.go @@ -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 +} diff --git a/internal/ingress/annotations/modsecurity/main_test.go b/internal/ingress/annotations/modsecurity/main_test.go new file mode 100644 index 000000000..6dc404499 --- /dev/null +++ b/internal/ingress/annotations/modsecurity/main_test.go @@ -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) + } + } +} diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index 45145e5ad..62143ac55 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -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) diff --git a/internal/ingress/types.go b/internal/ingress/types.go index 64c5d0eae..77a18cc94 100644 --- a/internal/ingress/types.go +++ b/internal/ingress/types.go @@ -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 diff --git a/internal/ingress/types_equals.go b/internal/ingress/types_equals.go index 659d85d0f..f5db890ee 100644 --- a/internal/ingress/types_equals.go +++ b/internal/ingress/types_equals.go @@ -380,6 +380,10 @@ func (l1 *Location) Equal(l2 *Location) bool { return false } + if !(&l1.ModSecurity).Equal(&l2.ModSecurity) { + return false + } + return true } diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 7a0bc3d85..fa1a04ecd 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -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 }} diff --git a/test/e2e/annotations/modsecurity.go b/test/e2e/annotations/modsecurity.go new file mode 100644 index 000000000..262a48b1a --- /dev/null +++ b/test/e2e/annotations/modsecurity.go @@ -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;") + }) + }) +})