- Coraza Web Application Firewall implemented as Envoy Go Filter.
Pull and start the image:
docker pull ghcr.io/united-security-providers/envoy-coraza:v2.0.0
docker run -p 8080:10000 ghcr.io/united-security-providers/envoy-coraza:v2.0.0First visit http://localhost:8080 and then http://localhost:8080/alert('xss').
Note
The second request should be blocked and you should see WAF rule triggered: Javascript method detected in the container logs.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
directives |
YAML map | Yes | - | Defines the WAF configurations available to the filter, e.g. one with CRS fully enforced and one with the engine off. It is a map where each key is a WAF name and each value is an object with a simple_directives list of SecLang directive strings. |
default_directive |
string | Yes | - | The fallback WAF to use when no host mapping matches. Must be a key defined in directives. |
host_directive_map |
YAML map | No | {} |
Defines how requests are mapped to WAFs. See matching behaviour below. |
log_format |
string | No | text |
Filter log format. Valid values: text, json. |
use_re2 |
boolean | No | true |
Use the RE2 regex engine. Only has effect in the performance build. |
use_libinjection |
boolean | No | true |
Use libinjection for SQL injection and XSS detection. Only has effect in the performance build. |
Example:
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
directives:
waf1:
simple_directives:
- "Include @coraza-setup"
- "Include @crs-setup"
- "SecDefaultAction \"phase:3,log,auditlog,pass\""
- "SecDefaultAction \"phase:4,log,auditlog,pass\""
- "SecDefaultAction \"phase:5,log,auditlog,pass\""
- "SecDebugLogLevel 3"
- "Include @owasp_crs/*.conf"
off:
simple_directives:
- "SecRuleEngine Off"
default_directive: "waf1"
host_directive_map:
"foo.example.com": "waf1"
"bar.example.com": "off"
log_format: "text"For a complete example Envoy configuration please refer to envoy.yaml.
For each request the filter resolves the directive set as follows:
- Exact match: the Host header as received (e.g.
foo.example.com:8443) is looked up directly. - Hostname-only match: if the Host header contains a port and no exact match was found, the port is stripped and the lookup is retried (e.g.
foo.example.com). - Default: if neither lookup matched,
default_directiveis used.
To match traffic arriving on a specific port only, include the port in the host map key (e.g. "foo.example.com:80": "waf1"). A key without a port (e.g. "foo.example.com") matches any port not covered by a more specific entry.
The Core Rule Set comes embedded in the extension.
Tip
You can also load a different CRS version or your own rules from filesystem.
Additionally to the rules, configuration files for setting up the rule engine and coraza are embedded as well. To include embedded rules and config files, the @ sign is used when referencing a path.
- @owasp_crs/*.conf: the CRS rules
- @coraza-setup: configures the rule engine for coraza
- @crs-setup: setup coreruleset
Example loading entire coreruleset:
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
directives:
waf1:
simple_directives:
- "Include @coraza-setup"
- "SecDebugLogLevel 9"
- "SecRuleEngine On"
- "Include @crs-setup"
- "Include @owasp_crs/*.conf"
default_directive: "waf1"Loading some pieces of the ruleset:
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
directives:
waf1:
simple_directives:
- "Include @coraza-setup"
- "SecDebugLogLevel 9"
- "SecRuleEngine On"
- "Include @crs-setup"
- "Include @owasp_crs/REQUEST-901-INITIALIZATION.conf"
default_directive: "waf1"- In order to mitigate as much as possible malicious requests (or connections open) sent upstream, it is recommended to keep the CRS Early Blocking feature enabled (SecAction
900120).
If you want to run the ftw test suite (for example in your ci environment), the configuration files are included in the shared object as well:
- @crs-ftw: configures rule engine for ftw tests
- @coraza-ftw: configures coraza for ftw tests
Additionally to the compiled in CRS, filter supports loading rules from filesystem. This can be useful to load custom rules, blocklists or another CRS version.
For example to load a file myrule.conf, we can first mount it into the container
docker run -v ./envoy.yaml:/etc/envoy/envoy.yaml -v ./myrule.conf:/etc/envoy/myrule.conf ghcr.io/united-security-providers/envoy-coraza:v2.0.0If you run envoy directly this is of course not needed, simply put it somewhere on the filesystem
And in the envoy config we can include the file:
[...]
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
directives:
waf1:
simple_directives:
- "Include @coraza-setup"
- "SecDebugLogLevel 9"
- "SecRuleEngine On"
- "Include @crs-setup"
- "Include @owasp_crs/*.conf"
- "Include /etc/envoy/myrule.conf"
default_directive: "waf1"
[...]Note the missing @, it means "try to load from filesystem"
The following example shows how to mount and use blocklist.txt:
docker run -v ./envoy.yaml:/etc/envoy/envoy.yaml -v ./blocklist.txt:/etc/envoy/blocklist.txt ghcr.io/united-security-providers/envoy-coraza:v2.0.0And in the envoy config add the rule:
[...]
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
directives:
waf1:
simple_directives:
- "Include @coraza-setup"
- "SecDebugLogLevel 9"
- "SecRuleEngine On"
- "Include @crs-setup"
- "Include @owasp_crs/*.conf"
- "SecRule REMOTE_ADDR \"@ipMatchFromFile /etc/envoy/blocklist.txt\" \"id:200003,phase:1,deny,status:403,msg:'IP Blocked by Blocklist'\""
default_directive: "waf1"
[...]Example loading CRS 4.22 (assuming you have the rules locally):
docker run -v ./envoy.yaml:/etc/envoy/envoy.yaml -v ./coreruleset-4.22:/etc/envoy/crs-4.22 ghcr.io/united-security-providers/envoy-coraza:v2.0.0And in the envoy config load the ruleset:
[...]
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
directives:
waf1:
simple_directives:
- "Include @coraza-setup"
- "SecDebugLogLevel 9"
- "SecRuleEngine On"
- "Include /etc/envoy/crs-4.22/crs-setup.conf.example"
- "Include /etc/envoy/crs-4.22/*.conf"
default_directive: "waf1"
[...]By default the filter writes plain text logs.
The log format can be changed to json using the log_format configuration option:
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
log_format: "json"
directives:
waf1:
simple_directives:
[ ... .... ]
default_directive: "waf1"Note that this setting does not automatically set the AuditLog Engine to JSON
If an audit log in json is desired, it must be configured with SecLang. For example:
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
log_format: "json"
directives:
waf1:
simple_directives:
# ... other directives ...
- "SecAuditLog /etc/envoy/logs/audit.log"
- "SecAuditLogParts ABCFHKZ"
- "SecAuditEngine RelevantOnly"
- "SecAuditLogRelevantStatus ^(?:5|4)"
- "SecAuditLogFormat JSON"
default_directive: "waf1"- Enable EnvoyPatchPolicy
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-gateway-config
namespace: envoy-gateway-system
data:
envoy-gateway.yaml: |
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyGateway
provider:
type: Kubernetes
gateway:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
extensionApis:
enableEnvoyPatchPolicy: true- Update the EnvoyProxy to use the united-security-providers/envoy-coraza image:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: eg
namespace: envoy-gateway-system
spec:
provider:
type: Kubernetes
kubernetes:
envoyDeployment:
container:
image: ghcr.io/united-security-providers/envoy-coraza:v2.0.0- Enable the plugin with an EnvoyPatchPolicy:
Note: The correct
pathin the JSONPatch depends on the listener protocol. Envoy Gateway usesdefault_filter_chainfor HTTP listeners andfilter_chains/0for HTTPS listeners (which use SNI-based filter chain matching). Using the wrong path will cause the patch to fail to apply.
For an HTTPS listener:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyPatchPolicy
metadata:
name: coraza-patch-policy
namespace: envoy-gateway-system
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: eg-internal
type: JSONPatch
jsonPatches:
- type: "type.googleapis.com/envoy.config.listener.v3.Listener"
## The name is in the format <namespace>/<gateway>/<listener> as per the XDS Name Scheme V2 - https://gateway.envoyproxy.io/docs/tasks/extensibility/envoy-patch-policy/#xds-name-scheme-v2
name: envoy-gateway-system/eg/https
operation:
op: add
## Needs to be added as the first item in the 'http_filters' array
## HTTPS listeners use filter_chains/0; HTTP listeners use default_filter_chain
path: "/filter_chains/0/filters/0/typed_config/http_filters/0"
value:
name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: coraza-waf
library_path: /etc/envoy/coraza-waf.so
plugin_name: coraza-waf
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
## setting the logs to json format, so they are the same as the default EnvoyGateway Access Logs
log_format: "json"
## configure coraza/CRS
directives:
default:
simple_directives:
- "Include @coraza-setup"
- "SecDebugLogLevel 9"
- "SecRuleEngine On"
- "Include @crs-setup"
- "Include @owasp_crs/*.conf"
off:
simple_directives:
- "SecRuleEngine Off"
default_directive: "default"
host_directive_map:
"foo.example.com": "off"
"bar.example.com": "default"For an HTTP listener, replace the name and path fields accordingly:
name: envoy-gateway-system/eg/http
operation:
op: add
path: "/default_filter_chain/filters/0/typed_config/http_filters/0"See Makefile for all targets.
make build
# or
make performanceBuildYou will find the go waf plugin under ./build/coraza-waf.so.
There is a known performance issue with larger request bodies in Coraza.
To help mitigate this, a new build target named performanceBuild has been introduced.
This target compiles the filter with support for both re2 and
libinjection to improve throughput.
The only downside is that this build introduces runtime dependencies on re2 and libinjection.
You can enable this behavior through the configuration. For example:
...
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: coraza-waf
library_path: /etc/envoy/coraza-waf.so
plugin_name: coraza-waf
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
use_re2: true
use_libinjection: trueNote
Setting these configuration options in the normal build will have no effect on coraza.
The following command runs the go-ftw test suite against the filter with the CRS fully loaded.
make ftwTake a look at the config files ftw.yml and overrides.yml for details about tests currently excluded and overridden.
One can also run a single test by executing:
FTW_INCLUDE=920410 make ftwRun the tests and abort on the first test that fails:
FTW_FAILFAST=1 make ftwThe following command runs a small set of end to end tests against the filter with the CRS fully loaded.
make e2e