I wanted to write this because I don’t hear enough real people discouraging the use of Web Application Firewalls (WAFs). Probably because the search results for “Web Application Firewall” are all written by WAF vendors. Anyone reading just that could conclude that WAFs are a good idea. I’m here to offer another perspective, after having suffered through using a WAF for two years.
Web Application Firewalls were created early in the Internet’s history, especially popularized by the ModSecurity project in 2002. WAFs essentially work by intercepting every single HTTP request (and sometimes responses too) and evaluating several hundred regular expressions over the URI, headers, and body, sometimes aided by machine learning. If the request kinda looks like SQL, shell code, etc., the server may block your request.
In the infancy of the cybersecurity field, WAFs seemed like a good idea. HTTP requests were tiny, infrequent, and mostly contained mundane form data. But today, WAFs have overstayed their welcome in the security toolbelt. There are better techniques you can use that make even the most advanced WAFs entirely obsolete.
WAFs have Horrible Performance
Since WAFs run hundreds of regular expressions on every request, you may ask, “isn’t that super inefficient?” Yes, very.
WAF | No WAF | |
---|---|---|
Average time taken to upload 9,462 text files | 7.36 | 4.55 |
Average requests per second | 1285 | 2079 |
Number of requests blocked erroneously | 5 | 0 |
Peak nginx CPU during trial | 73% | 8% |
Specifics about the benchmark
The easiest way I know to get modsecurity + CoreRuleSet installed is through ingress-nginx, which I’ve installed in a Kind cluster.
# https://kind.sigs.k8s.io/docs/user/quick-start/
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 32080
hostPort: 32080
protocol: TCP
- containerPort: 32443
hostPort: 32443
protocol: TCP
EOF
# https://kubernetes.github.io/ingress-nginx/user-guide/third-party-addons/modsecurity/
helm upgrade --install ingress-nginx ingress-nginx
--repo https://kubernetes.github.io/ingress-nginx
--namespace ingress-nginx --create-namespace
--set controller.service.type=NodePort
--set controller.service.nodePorts.https=32443
--set controller.service.nodePorts.http=32080
--set controller.ingressClassResource.default=true
--set controller.allowSnippetAnnotations=true
For the test, I’ll be uploading files to MinIO using these values:
replicas: 1
mode: standalone
resources:
requests:
memory: 512Mi
persistence:
enabled: false
rootUser: rootuser
rootPassword: rootpass123
buckets:
- name: bucket1
policy: none
purge: false
ingress:
enabled: true
hosts: [minio-waf.localhost]
annotations:
nginx.ingress.kubernetes.io/enable-modsecurity: "true"
nginx.ingress.kubernetes.io/enable-owasp-core-rules: "true"
nginx.ingress.kubernetes.io/modsecurity-snippet: |
Include /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf
SecRuleEngine On
# Even the core rules are ridiculous, blocking PUT requests, certain content-types, or any body with "options" in it
SecRuleRemoveById 911100 920420 921110
helm upgrade --install minio minio/minio -f values.yaml -n minio --create-namespace
helm upgrade --install minio-waf minio/minio -f values-waf.yaml -n minio-waf --create-namespace
# Verify the WAF is working (should get a 403)
curl 'http://minio-waf.localhost:32080/?q=../../etc/passwd'
We’ll be uploading just the “Documentation” folder of the v6.6 Linux Kernel, which contains 9462 files for a total of 65MB.
curl -LO https://github.com/torvalds/linux/archive/refs/tags/v6.6.zip
unzip v6.6.zip 'linux-6.6/Documentation/*'
Configure the minio client:
# You may need to add these hosts to /etc/hosts
export MC_HOST_nowaf='http://rootuser:rootpass123@minio.localhost:32080'
export MC_HOST_waf='http://rootuser:rootpass123@minio-waf.localhost:32080'
Run the benchmark (5 times each):
time mc cp -r linux-6.6/Documentation/ waf/bucket1/
time mc cp -r linux-6.6/Documentation/ nowaf/bucket1/
In addition to slowing down every request, you also need significant additional RAM for buffering requests. Since not a single byte in the buffer can be flushed to the bac