Skip to content

Hardening your WebApp using CDN/FW/WAF

Published: February 15, 2026

Author: Khalil Imitik

waf-whitelist.svg

Exposing your web application directly to the internet is asking for trouble. Cross-site scripting, path traversal, bots, denial of service, you name it. Before worrying about features or uptime, lock the front door first.

We’re going to place three layers of defense in front of the server, each working together to block the most common attacks. Let’s get into it!

OSI model layers

A CDN is a reverse proxy that sits between your server and the internet. It doesn’t just absorb attacks like DDoS, it also caches your content globally (faster load times everywhere), terminates TLS on your behalf, and most importantly, hides your origin IP from the outside world

Without one, every request hits your server directly and your IP is fully exposed (which is not good)

There are plenty of CDN providers, CloudFront, Akamai, Fastly, Cloudflare. Pick whatever suits your stack. For me i’m going with Cloudflare here (for some reason)

First, Sign up or log in to your CDN account and add your domain via Domains > Onboard a domain

Domain addition

Then, head to your domain registrar and replace the existing nameservers with the ones Cloudflare gives you

Now point your domain to the server by creating A and/or AAAA (IPv4-6) records. Make sure the proxy toggle is enabled (this is what routes traffic through cloudflare’s edge)

A DNS record

Here’s the thing, even with CDN proxying your traffic, your origin IP can still leak (through DNS history, email headers, or forgotten/unproxied subdomains). Once an attacker finds it, they bypass the CDN entirely and hit your server raw. Game over
And to fix that, actually you only need to allow CDN’s IP ranges to reach your web ports. Everything else gets dropped at the firewall

For this, you want a dedicated firewall, not something baked into your application server. Such as Cisco ASA, FortiGate, or a software firewall like pfSense all work. We’ll use OPNsense in this walkthrough
Now the question is, which IPs do we allow? Actually most of major CDN publishes their IP ranges. They get updated from time to time, so you can fetch them dynamically (from their endpoints) rather than do it manually

In your firewall, you need to create an URL table alias that automatically fetches cloudflare’s IP list at a regular interval (12h works fine to me)

Cloudflare Allowed IPs

An alias by itself does nothing, you need to attach it to a rule. There are two way to do that

A. WAN firewall rules, add the rule directly under Firewall > Rules > WAN, using the alias as the source and restricting to ports TCP 80/443

Wan interface rule

B. NAT port forward, if you’re using port forwarding to route the external traffic to your server or reverse proxy, navigate to Firewall > NAT > Port Forward and set the alias as the source there. OPNsense/pfSense will automatically generate the corresponding firewall rule

NAT Port Forward for HTTPs

Actually, you could configure WAF rules directly from the CDN, that works. But if you’re already running a reverse proxy like traefik, you can go deeper by running your own WAF engine at the application layer using ModSecurity (apache/nginx) with CRS It inspects HTTP/HTTPs requests after they’ve passed through your CDN, and firewall, maybe IPS, right before they hit your app

Traefik doesn’t really have a built-in WAF. Instead, you could uses a middleware plugin that forwards every request to a separate ModSecurity container for inspection. The flow looks like this

Traefik waf flow

The WAF container runs apache (recommended) or nginx with ModSecurity and OWASP CRS baked in. It needs a dummy backend to forward requests to (just a lightweight traefik/whoami that always returns 200). The plugin checks the WAF’s response code, anything under 400 means the request is clean, but anything 400 or above means it gets blocked

services:
traefik:
image: traefik:v3.6
container_name: traefik
labels:
- traefik.enable=true
- traefik.http.services.traefik.loadbalancer.server.port=8080
# WAF middleware definition
- traefik.http.middlewares.waf.plugin.traefik-modsecurity-plugin.modSecurityUrl=http://waf:8080
- traefik.http.middlewares.waf.plugin.traefik-modsecurity-plugin.maxBodySize=10485760
depends_on:
- waf
networks:
- proxy
ports:
- "80:80"
- "443:443"
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock
- $PWD/traefik.yml:/etc/traefik/traefik.yml
- $PWD/config:/etc/traefik/config:ro
- $PWD/acme.json:/acme.json
- $PWD/logs:/var/log/traefik
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 10s
timeout: 10s
retries: 3
start_period: 10s
# OWASP ModSecurity
waf:
image: owasp/modsecurity-crs:apache-alpine
networks:
- proxy
environment:
- PARANOIA=1
- ANOMALY_INBOUND=10
- ANOMALY_OUTBOUND=5
- BACKEND=http://dummy
depends_on:
- dummy
# Dummy backend required by ModSecurity
dummy:
image: traefik/whoami
networks:
- proxy
networks:
proxy:
external: true

So, three containers, one job which is inspect every HTTP request before it touches your application

And the Traefik static config traefik.yml, this is where the plugin gets loaded

experimental:
plugins:
traefik-modsecurity-plugin:
moduleName: github.com/acouvreur/traefik-modsecurity-plugin
version: v1.3.0

Last thing, attaching the middleware to your services. In your app’s dynamic config or Docker labels, reference the WAF middleware

Dynamic Config
http:
routers:
webapp:
rule: "Host(`berber-bytes.com`)"
entryPoints:
- WebSecure
service: webapp
tls: {}
middlewares:
- waf@docker
services:
webapp:
loadBalancer:
servers:
- url: "http://webapp:8443"
Docker Labels
#OR
labels:
- traefik.enable=true
- traefik.http.routers.webapp.rule=Host(`berber-bytes.com`)
- traefik.http.routers.webapp.entrypoints=WebSecure
- traefik.http.routers.webapp.middlewares=waf@docker
- traefik.http.services.webapp.loadbalancer.server.port=8443

And of course, before you call it a day, make sure the WAF is actually doing its job

Open the Traefik dashboard and navigate to your router. You should see something like waf@docker listed under the middlewares column

Traefik waf enabled

Send a request that should trigger CRS rules. A classic RCE or maybe XSS in the query string works

RCE Payload
curl -I "http://berber-bytes.com/?cmd=;cat%20/etc/shadow"
XSS Payload
curl -I "http://berber-bytes.com/?q=<script>alert(1)</script>"

All of these should return Forbidden 403

Behind CDN, your origin sees Cloudflare’s edge IPs, not the actual visitor’s. This means your logs, rate or bruteforce limiters, and WAF rules all operate on the wrong IP. To fix that, you can use a Traefik plugin like cloudflarewarp that reads the CF-Connecting-IP header and overwrites the request’s source IP

Traefik.yml
experimental:
plugins:
cloudflarewarp:
moduleName: github.com/BetterCorp/cloudflarewarp
version: v1.3.3
Middleware Config
...
middlewares:
cloudflare-ip:
plugin:
cloudflarewarp:
disableDefault: false

Now! the front door is locked ✌︎㋡

CC BY-NC-ND 4.0
Attribution required • Non-commercial • No derivatives