From 4cc3494fcb13100647702890bee23dc8d11cca6f Mon Sep 17 00:00:00 2001 From: Mac Chaffee Date: Wed, 23 Nov 2022 12:29:22 -0500 Subject: [PATCH] Add support for Coraza via 'modsecurity-use-coraza' --- docs/.gitignore | 1 + docs/content/en/docs/configuration/keys.md | 7 +- docs/content/en/docs/examples/modsecurity.md | 29 +++++++- docs/static/resources/coraza-deployment.yaml | 73 +++++++++++++++++++ pkg/converters/ingress/annotations/global.go | 1 + pkg/converters/ingress/types/global.go | 1 + pkg/haproxy/instance_test.go | 50 ++++++++++++- pkg/haproxy/types/types.go | 1 + rootfs/etc/templates/haproxy/haproxy.tmpl | 9 +++ .../templates/modsecurity/modsecurity.tmpl | 12 +++ 10 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 docs/static/resources/coraza-deployment.yaml diff --git a/docs/.gitignore b/docs/.gitignore index 3633da8d5..db89b677e 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,4 +1,5 @@ package-lock.json public/ resources/ +!static/resources/ node_modules/ diff --git a/docs/content/en/docs/configuration/keys.md b/docs/content/en/docs/configuration/keys.md index dd541854f..098cfb653 100644 --- a/docs/content/en/docs/configuration/keys.md +++ b/docs/content/en/docs/configuration/keys.md @@ -415,6 +415,7 @@ The table below describes all supported configuration keys. | [`modsecurity-timeout-hello`](#modsecurity) | time with suffix | Global | `100ms` | | [`modsecurity-timeout-idle`](#modsecurity) | time with suffix | Global | `30s` | | [`modsecurity-timeout-processing`](#modsecurity) | time with suffix | Global | `1s` | +| [`modsecurity-use-coraza`](#modsecurity) | [true\|false] | Global | `false` | | [`nbproc-ssl`](#nbproc) | number of process | Global | `0` | | [`nbthread`](#nbthread) | number of threads | Global | | | [`no-tls-redirect-locations`](#ssl-redirect) | comma-separated list of URIs | Global | `/.well-known/acme-challenge` | @@ -1997,6 +1998,7 @@ See also: | `modsecurity-timeout-idle` | `Global` | `30s` | | | `modsecurity-timeout-processing` | `Global` | `1s` | | | `modsecurity-timeout-server` | `Global` | `5s` | v0.10 | +| `modsecurity-use-coraza` | `Global` | `false` | v0.14 | Configure modsecurity agent. These options only have effect if `modsecurity-endpoints` @@ -2019,10 +2021,11 @@ The following keys are supported: * `modsecurity-timeout-idle`: Defines the maximum time to wait before close an idle connection. Default value is `30s`. * `modsecurity-timeout-processing`: Defines the maximum time to wait for the whole ModSecurity processing. Default value is `1s`. * `modsecurity-timeout-server`: Defines the maximum time to wait for an agent response. Configures the haproxy's timeout server. Defaults to `5s` if not configured. +* `modsecurity-use-coraza`: Defines whether the generated SPOE config should include Coraza-specific values. In order to use Coraza instead of Modsecurity, you must set this to "true" and also set `modsecurity-args` based on the instructions in the [coraza-spoa repository](https://github.com/corazawaf/coraza-spoa). A full example can be found [here]({{% relref "../examples/modsecurity#using-coraza-instead-of-modsecurity" %}}). See also: -* [example]({{% relref "../examples/modsecurity" %}}) page. +* [modsecurity example]({{% relref "../examples/modsecurity" %}}) page. * [`waf`](#waf) configuration key. * https://www.haproxy.org/download/2.0/doc/SPOE.txt * https://docs.haproxy.org/2.4/configuration.html#9.3 @@ -2858,7 +2861,7 @@ See also: | `waf-mode` | `Path` | `deny` | v0.9 | Defines which web application firewall (WAF) implementation should be used -to validate requests. Currently the only supported value is `modsecurity`. +to validate requests. Currently the only supported value is `modsecurity`, which also supports Coraza endpoints when `modsecurity-use-coraza` is set to "true". This configuration has no effect if the ModSecurity endpoints are not configured. diff --git a/docs/content/en/docs/examples/modsecurity.md b/docs/content/en/docs/examples/modsecurity.md index e95f3dc74..e1ad622e8 100644 --- a/docs/content/en/docs/examples/modsecurity.md +++ b/docs/content/en/docs/examples/modsecurity.md @@ -43,8 +43,8 @@ Check if the agent is up and running: ``` $ kubectl -n ingress-controller get deployment modsecurity-spoa -NAME READY UP-TO-DATE AVAILABLE AGE -modsecurity-spoa 3/3 3 3 7s +NAME READY UP-TO-DATE AVAILABLE AGE +modsecurity-spoa 3/3 3 3 7s ``` @@ -186,3 +186,28 @@ modsecurity-spoa-6596c6b444-cht27 2/2 Running 0 14m modsecurity-spoa-6596c6b444-kw2tr 2/2 Running 0 14m modsecurity-spoa-6596c6b444-mkndw 2/2 Running 0 14m ``` + +## Using Coraza instead of ModSecurity + +Since the maintainers of ModSecurity are dropping support in 2024, [OWASP has created a replacement called Coraza](https://coreruleset.org/20211222/talking-about-modsecurity-and-the-new-coraza-waf/) which is a drop-in replacement for ModSecurity. + +In order to use Coraza, the process is essentially the same as described in the above sections, with a few exceptions. First, in the haproxy-ingress ConfigMap, add the following two additional keys: + +```yaml + modsecurity-use-coraza: true + modsecurity-args: "app=hdr(host) id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body" +``` + +You may require different `modsecurity-args` depending on your Coraza config and your version of coraza-spoa. Check the [coraza-spoa README](https://github.com/corazawaf/coraza-spoa) for full details. + +Second, you'll need to change the spoa-modsecurity container image to a coraza-spoa image and create a configmap to hold the Coraza config.yaml. A complete example with all the changes can be found [here](/resources/coraza-deployment.yaml). + +{{% alert title="Warning" color="warning" %}} +The coraza-spoa image that we provide in the above example is based on [an experimental branch of coraza-spoa](https://github.com/corazawaf/coraza-spoa/pull/36). For production environments, it would be best to wait until the experimental changes are merged and [an official image is released](https://github.com/corazawaf/coraza-spoa/issues/37). +{{% /alert %}} + +### Troubleshooting Coraza + +* Ensure that the `bind` port in Coraza's config.yaml matches the port in `modsecurity-endpoints`. +* Pay close attention to the `modsecurity-args` (specifically the `app` arg) and make sure they are set according to the coraza-spoa docs since the exact args may vary depending on your version of coraza-spoa. +* For more complex issues, it may help to set `log_level: debug` in Coraza. diff --git a/docs/static/resources/coraza-deployment.yaml b/docs/static/resources/coraza-deployment.yaml new file mode 100644 index 000000000..a2de34aa3 --- /dev/null +++ b/docs/static/resources/coraza-deployment.yaml @@ -0,0 +1,73 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: coraza-spoa + name: coraza-spoa + namespace: ingress-controller +spec: + replicas: 3 + selector: + matchLabels: + run: coraza-spoa + template: + metadata: + labels: + run: coraza-spoa + spec: + containers: + - name: coraza-spoa + # NOTE: Built based on this PR: https://github.com/corazawaf/coraza-spoa/pull/36 + # An official coraza-spoa image will be released by the upstream project soon. + image: quay.io/jcmoraisjr/coraza-spoa:experimental + ports: + - containerPort: 12345 + name: spop + protocol: TCP + resources: + limits: + cpu: 200m + memory: 150Mi + requests: + cpu: 200m + memory: 150Mi + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 12345 + timeoutSeconds: 4 + volumeMounts: + - name: coraza-config + mountPath: /config.yaml + subPath: config.yaml + readOnly: true + volumes: + - name: coraza-config + configMap: + name: coraza-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: coraza-config + namespace: ingress-controller +data: + # Check the official documentation: https://github.com/corazawaf/coraza-spoa + config.yaml: | + bind: :12345 + default_application: default_app + applications: + default_app: + include: + - /etc/coraza-spoa/coraza.conf + - /etc/coraza-spoa/crs-setup.conf + - /etc/coraza-spoa/rules/*.conf + + transaction_ttl: 60000 + transaction_active_limit: 100000 + log_level: info + log_file: /dev/stdout diff --git a/pkg/converters/ingress/annotations/global.go b/pkg/converters/ingress/annotations/global.go index 5686502b5..7113e7a79 100644 --- a/pkg/converters/ingress/annotations/global.go +++ b/pkg/converters/ingress/annotations/global.go @@ -345,6 +345,7 @@ func (c *updater) buildGlobalModSecurity(d *globalData) { d.global.ModSecurity.Timeout.Processing = c.validateTime(d.mapper.Get(ingtypes.GlobalModsecurityTimeoutProcessing)) d.global.ModSecurity.Timeout.Server = c.validateTime(d.mapper.Get(ingtypes.GlobalModsecurityTimeoutServer)) d.global.ModSecurity.Args = utils.Split(d.mapper.Get(ingtypes.GlobalModsecurityArgs).Value, " ") + d.global.ModSecurity.UseCoraza = d.mapper.Get(ingtypes.GlobalModsecurityUseCoraza).Bool() } func (c *updater) buildGlobalDNS(d *globalData) { diff --git a/pkg/converters/ingress/types/global.go b/pkg/converters/ingress/types/global.go index 0f91472d7..1731849c7 100644 --- a/pkg/converters/ingress/types/global.go +++ b/pkg/converters/ingress/types/global.go @@ -99,6 +99,7 @@ const ( GlobalModsecurityTimeoutIdle = "modsecurity-timeout-idle" GlobalModsecurityTimeoutProcessing = "modsecurity-timeout-processing" GlobalModsecurityTimeoutServer = "modsecurity-timeout-server" + GlobalModsecurityUseCoraza = "modsecurity-use-coraza" GlobalNbprocBalance = "nbproc-balance" GlobalNbprocSSL = "nbproc-ssl" GlobalNbthread = "nbthread" diff --git a/pkg/haproxy/instance_test.go b/pkg/haproxy/instance_test.go index 73da4212c..07fe641ac 100644 --- a/pkg/haproxy/instance_test.go +++ b/pkg/haproxy/instance_test.go @@ -4801,6 +4801,8 @@ func TestModSecurity(t *testing.T) { modsecExp string modsecAgentArgs []string modsecAgentExp string + modsecUseCoraza bool + modsecOtherExp string }{ { waf: "modsecurity", @@ -4882,12 +4884,39 @@ func TestModSecurity(t *testing.T) { timeout connect 1s timeout server 2s server modsec-spoa0 10.0.0.101:12345`, + modsecOtherExp: ` + messages check-request + option var-prefix modsec`, modsecAgentArgs: []string{"unique-id", "method", "path", "query", "req.ver", "req.hdrs_bin"}, modsecAgentExp: ` +spoe-message check-request args unique-id method path query req.ver req.hdrs_bin event on-backend-http-request`, }, + // Test modsecurity-use-coraza + { + waf: "modsecurity", + wafmode: "deny", + endpoints: []string{"10.0.0.101:12345"}, + backendExp: ` + filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf + http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 } + http-request deny if !{ var(txn.coraza.fail) -m int eq 0 }`, + modsecExp: ` + timeout connect 1s + timeout server 2s + server modsec-spoa0 10.0.0.101:12345`, + modsecAgentArgs: []string{"app=hdr(host)", "id=unique-id", "src-ip=src", "src-port=src_port", "dst-ip=dst", "dst-port=dst_port", "method=method", "path=path", "query=query", "version=req.ver", "headers=req.hdrs", "body=req.body"}, + modsecAgentExp: ` +spoe-message coraza-req + args app=hdr(host) id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body + event on-backend-http-request`, + modsecOtherExp: ` + messages coraza-req + option var-prefix coraza`, + modsecUseCoraza: true, + }, } for _, test := range testCases { c := setup(t) @@ -4913,7 +4942,7 @@ func TestModSecurity(t *testing.T) { globalModsec.Timeout.Connect = "1s" globalModsec.Timeout.Server = "2s" globalModsec.Args = test.modsecAgentArgs - + globalModsec.UseCoraza = test.modsecUseCoraza c.Update() var modsec string @@ -4922,18 +4951,35 @@ func TestModSecurity(t *testing.T) { backend spoe-modsecurity mode tcp` + test.modsecExp } - c.checkConfig(` + // unique-id-format must be set when using Coraza + if test.modsecUseCoraza { + c.checkConfig(` <> <> + unique-id-format %[uuid()] backend d1_app_8080 mode http` + test.backendExp + ` server s1 172.17.0.11:8080 weight 100 <> <> <>` + modsec) + } else { + c.checkConfig(` +<> +<> +backend d1_app_8080 + mode http` + test.backendExp + ` + server s1 172.17.0.11:8080 weight 100 +<> +<> +<>` + modsec) + } if test.modsecAgentExp != "" { c.containsText("spoe-modsecurity.conf", c.readConfig(c.tempdir+"/spoe-modsecurity.conf"), test.modsecAgentExp) } + if test.modsecOtherExp != "" { + c.containsText("spoe-modsecurity.conf", c.readConfig(c.tempdir+"/spoe-modsecurity.conf"), test.modsecOtherExp) + } c.logger.CompareLogging(defaultLogging) c.teardown() diff --git a/pkg/haproxy/types/types.go b/pkg/haproxy/types/types.go index 7caae4f08..65f67ab84 100644 --- a/pkg/haproxy/types/types.go +++ b/pkg/haproxy/types/types.go @@ -183,6 +183,7 @@ type ModSecurityConfig struct { Endpoints []string Timeout ModSecurityTimeoutConfig Args []string + UseCoraza bool } // CookieConfig ... diff --git a/rootfs/etc/templates/haproxy/haproxy.tmpl b/rootfs/etc/templates/haproxy/haproxy.tmpl index 9a28d7e00..ea61b4154 100644 --- a/rootfs/etc/templates/haproxy/haproxy.tmpl +++ b/rootfs/etc/templates/haproxy/haproxy.tmpl @@ -184,6 +184,10 @@ defaults {{- if $global.Timeout.Tunnel }} timeout tunnel {{ $global.Timeout.Tunnel }} {{- end }} +{{- /* Coraza will crash if the unique-id isn't set, which requires us to set the format here */}} +{{- if $global.ModSecurity.UseCoraza }} + unique-id-format %[uuid()] +{{- end }} {{- range $snippet := $global.CustomDefaults }} {{ $snippet }} {{- end }} @@ -582,7 +586,12 @@ backend {{ $backend.ID }} {{- range $i, $waf := $wafCfg.Items }} {{- if eq $waf.Mode "deny" }} {{- range $pathIDs := $wafCfg.PathIDs $i }} +{{- if $global.ModSecurity.UseCoraza }} + http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 } + http-request deny if !{ var(txn.coraza.fail) -m int eq 0 } +{{- else }} http-request deny if { var(txn.modsec.code) -m int gt 0 } +{{- end }} {{- if $pathIDs }} { var(txn.pathID) -m str {{ $pathIDs }} }{{ end }} {{- end }} {{- end }} diff --git a/rootfs/etc/templates/modsecurity/modsecurity.tmpl b/rootfs/etc/templates/modsecurity/modsecurity.tmpl index 53ac2dd85..3f2af397c 100644 --- a/rootfs/etc/templates/modsecurity/modsecurity.tmpl +++ b/rootfs/etc/templates/modsecurity/modsecurity.tmpl @@ -9,12 +9,24 @@ {{- $modsec := .Global.ModSecurity }} [modsecurity] spoe-agent modsecurity-agent +{{- if .Global.ModSecurity.UseCoraza }} + messages coraza-req + option var-prefix coraza +{{- else }} messages check-request option var-prefix modsec +{{- end }} timeout hello {{ $modsec.Timeout.Hello }} timeout idle {{ $modsec.Timeout.Idle }} timeout processing {{ $modsec.Timeout.Processing }} use-backend spoe-modsecurity + log global + option dontlog-normal + +{{- if .Global.ModSecurity.UseCoraza }} +spoe-message coraza-req +{{- else }} spoe-message check-request +{{- end }} args {{ $modsec.Args | join " " }} event on-backend-http-request