Merge pull request #3309 from diazjf/modsecurity-location

Customize ModSecurity to be used in Locations
This commit is contained in:
k8s-ci-robot 2018-11-07 09:29:01 -08:00 committed by GitHub
commit d97999c07d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 307 additions and 7 deletions

View file

@ -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-host](#influxdb)|string|
|[nginx.ingress.kubernetes.io/influxdb-server-name](#influxdb)|string| |[nginx.ingress.kubernetes.io/influxdb-server-name](#influxdb)|string|
|[nginx.ingress.kubernetes.io/use-regex](#use-regex)|bool| |[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 ### Canary
@ -634,6 +638,29 @@ For details on how to write WAF rules, please refer to [https://github.com/p0pr0
[configmap]: ./configmap.md [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 ### InfluxDB
Using `influxdb-*` annotations we can monitor requests passing through a Location by sending them to an InfluxDB backend exposing the UDP socket 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. 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.

View file

@ -20,6 +20,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
"github.com/imdario/mergo" "github.com/imdario/mergo"
"k8s.io/ingress-nginx/internal/ingress/annotations/canary" "k8s.io/ingress-nginx/internal/ingress/annotations/canary"
"k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity"
"k8s.io/ingress-nginx/internal/ingress/annotations/sslcipher" "k8s.io/ingress-nginx/internal/ingress/annotations/sslcipher"
apiv1 "k8s.io/api/core/v1" apiv1 "k8s.io/api/core/v1"
@ -98,6 +99,7 @@ type Ingress struct {
Logs log.Config Logs log.Config
LuaRestyWAF luarestywaf.Config LuaRestyWAF luarestywaf.Config
InfluxDB influxdb.Config InfluxDB influxdb.Config
ModSecurity modsecurity.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
@ -140,6 +142,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
"LuaRestyWAF": luarestywaf.NewParser(cfg), "LuaRestyWAF": luarestywaf.NewParser(cfg),
"InfluxDB": influxdb.NewParser(cfg), "InfluxDB": influxdb.NewParser(cfg),
"BackendProtocol": backendprotocol.NewParser(cfg), "BackendProtocol": backendprotocol.NewParser(cfg),
"ModSecurity": modsecurity.NewParser(cfg),
}, },
} }
} }

View 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
}

View 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)
}
}
}

View file

@ -355,6 +355,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([]
loc.DefaultBackend = anns.DefaultBackend loc.DefaultBackend = anns.DefaultBackend
loc.BackendProtocol = anns.BackendProtocol loc.BackendProtocol = anns.BackendProtocol
loc.CustomHTTPErrors = anns.CustomHTTPErrors loc.CustomHTTPErrors = anns.CustomHTTPErrors
loc.ModSecurity = anns.ModSecurity
if loc.Redirect.FromToWWW { if loc.Redirect.FromToWWW {
server.RedirectFromToWWW = true server.RedirectFromToWWW = true
@ -396,6 +397,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([]
DefaultBackend: anns.DefaultBackend, DefaultBackend: anns.DefaultBackend,
BackendProtocol: anns.BackendProtocol, BackendProtocol: anns.BackendProtocol,
CustomHTTPErrors: anns.CustomHTTPErrors, CustomHTTPErrors: anns.CustomHTTPErrors,
ModSecurity: anns.ModSecurity,
} }
if loc.Redirect.FromToWWW { if loc.Redirect.FromToWWW {
@ -848,6 +850,7 @@ func (n *NGINXController) createServers(data []*extensions.Ingress,
defLoc.LuaRestyWAF = anns.LuaRestyWAF defLoc.LuaRestyWAF = anns.LuaRestyWAF
defLoc.InfluxDB = anns.InfluxDB defLoc.InfluxDB = anns.InfluxDB
defLoc.BackendProtocol = anns.BackendProtocol defLoc.BackendProtocol = anns.BackendProtocol
defLoc.ModSecurity = anns.ModSecurity
} else { } else {
glog.V(3).Infof("Ingress %q defines both a backend and rules. Using its backend as default upstream for all its rules.", glog.V(3).Infof("Ingress %q defines both a backend and rules. Using its backend as default upstream for all its rules.",
ingKey) ingKey)

View file

@ -20,6 +20,7 @@ import (
apiv1 "k8s.io/api/core/v1" apiv1 "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1" extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/util/intstr" "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/auth"
"k8s.io/ingress-nginx/internal/ingress/annotations/authreq" "k8s.io/ingress-nginx/internal/ingress/annotations/authreq"
@ -280,6 +281,9 @@ type Location struct {
// CustomHTTPErrors specifies the error codes that should be intercepted. // CustomHTTPErrors specifies the error codes that should be intercepted.
// +optional // +optional
CustomHTTPErrors []int `json:"custom-http-errors"` 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 // SSLPassthroughBackend describes a SSL upstream server configured

View file

@ -380,6 +380,10 @@ func (l1 *Location) Equal(l2 *Location) bool {
return false return false
} }
if !(&l1.ModSecurity).Equal(&l2.ModSecurity) {
return false
}
return true return true
} }

View file

@ -16,9 +16,7 @@ pid /tmp/nginx.pid;
load_module /etc/nginx/modules/ngx_http_geoip2_module.so; load_module /etc/nginx/modules/ngx_http_geoip2_module.so;
{{ end }} {{ end }}
{{ if $cfg.EnableModsecurity }}
load_module /etc/nginx/modules/ngx_http_modsecurity_module.so; load_module /etc/nginx/modules/ngx_http_modsecurity_module.so;
{{ end }}
{{ if $cfg.EnableOpentracing }} {{ if $cfg.EnableOpentracing }}
load_module /etc/nginx/modules/ngx_http_opentracing_module.so; load_module /etc/nginx/modules/ngx_http_opentracing_module.so;
@ -1020,13 +1018,17 @@ stream {
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ if $all.Cfg.EnableModsecurity }} {{ if (or $location.ModSecurity.Enable $all.Cfg.EnableModsecurity) }}
modsecurity on; modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf; 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; modsecurity_rules_file /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf;
{{ end }} {{ end }}
{{ if (not (empty $location.ModSecurity.TransactionID)) }}
modsecurity_transaction_id "{{ $location.ModSecurity.TransactionID }}";
{{ end }}
{{ end }} {{ end }}
{{ if isLocationAllowed $location }} {{ if isLocationAllowed $location }}

View 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;")
})
})
})