Traefik itself does not include WAF capabilities. If you want to add this capability, you can opt to replace Traefik with Apache httpd or nginx coupled with ModSecurity, however you loose the autoconfiguration of Traefik.
Fortunately, Alexis Couvreur has developed a ModSecurity plugin for Traefik to forward requests received by Traefik to another webserver (running ModSecurity) before actually forwarding the requests to the application server. If the ModSecurity webserver returns a code > 400, then Traefik will reject the request, otherwise it will forward it to the application server.
The suggested setup uses owasp/modsecurity-crs image for ModSecurity and since this can act as a reverse proxy, it uses the well known containous/whoami image as backend, since it is lightweight and always return a 200 status code.
The setup I decided to use is identical with the addition of SSL between the components, and multiple WAF containers depending on their intended use (paranoia level, detection only, different rules, etc.).
SSL certificate
Let’s first create the SSL certificate. Since this is a test environment, a self-signed certificate is fine. For production use, I recommend signing the certificate with your existing CA. The common name matches the value of environment variable SERVER_NAME
. v3_req
extensions are included to generate a server certificate instead of a CA certificate.
openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -keyout server.key -out server.crt -days 3650 -subj "/C=US/ST=California/L=Log Angeles/O=Foobar/CN=waf" -addext "subjectAltName=DNS:*" -extensions v3_req
Traefik needs to trust this certificate, so we need to create a custom image. Create a Dockerfile
in traefik
directory with the following content:
FROM traefik:2.5.7
ADD server.crt /usr/local/share/ca-certificates/server.crt
RUN update-ca-certificates
In your docker-compose.yml
file, replace the image: traefik:2.5.7
with build: traefik
and build the container image with docker compose build traefik
.
ModSecurity plugin
Since it uses a Traefik plugin, you will need a Traefik Pilot token. Once you have a token, pass it to your Traefik container using environment variable PILOT_TOKEN
. I use a .env
file with the following content:
PILOT_TOKEN=token
Then, add the following items to the command of your Traefik container:
- "--pilot.token=${PILOT_TOKEN}"
- "--experimental.plugins.traefik-modsecurity-plugin.modulename=github.com/acouvreur/traefik-modsecurity-plugin"
- "--experimental.plugins.traefik-modsecurity-plugin.version=v1.0.3"
Then add the following label to your Traefik container:
- "traefik.http.middlewares.waf.plugin.traefik-modsecurity-plugin.modSecurityUrl=https://waf:443"
WAF service
Let’s add the WAF service:
waf:
image: owasp/modsecurity-crs:apache
environment:
- PARANOIA=1
- ANOMALY_INBOUND=10
- ANOMALY_OUTBOUND=5
- BACKEND=https://dummy
- SERVER_NAME=waf
volumes:
- ./server.key:/usr/local/apache2/conf/server.key:ro
- ./server.crt:/usr/local/apache2/conf/server.crt:ro
Notice the BACKEND
variable matches the dummy
container name.
Dummy service
The suggested configuration uses containous/whoami
but I have decided to use nginx. The main reason is stability: I have had some issues with containous/whoami
, sometimes it crashed with no apparent reason. We are going to replace nginx default configuration file with our own and pass it the SSL certificate:
dummy:
image: nginx:latest
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./server.key:/certs/server.key:ro
- ./server.crt:/certs/server.crt:ro
Paste the following in a file named nginx.conf
:
user nginx;
worker_processes auto;
error_log stderr notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
return 200 'OK\n';
access_log off;
ssl_certificate /certs/server.crt;
ssl_certificate_key /certs/server.key;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
}
}
This configuration enables SSL and makes nginx reply with a status code 200 whatever the request is.
Restart the Traefik stack using docker compose up -d
.
Add WAF to an app
Edit docker-compose.yml
for an app you want to protect and add the following label:
- "traefik.http.routers.myapp.middlewares=waf@docker"
Restart the app stack.
Validate ModSecurity rules
Call your app normally first, you should not experiment errors.
Then add ?test=../
to the URI, and you should receive a status code 403 Forbidden
.
Adding more WAF services
We now have seen how to have one WAF container. To add another WAF container, you need to:
- in
docker-compose.yml
, copy/paste thewaf
service, rename it to your liking (eg:wafparanoia4
) and adapt the environment variables (eg:PARANOIA=4
) - in
docker-compose.yml
, add a new HTTP middleware matching your new WAF service (eg:wafparanoia4
) to Traefik pointing to this new WAF container - in
docker-compose.yml
of the app to be protected by this new WAF service, add a label to use the middleware you just created - restart your Traefik stack
- restart your app stack
- profit!
Some thoughts
Since the request is forwarded to a dummy container, only the request is actually analyzed. If the requests passes WAF checks, it will go to your app server. Then, if the response of your app contains something that would be blocked by WAF, here it will not.
If you need to analyze the response as well, then I think you should not use this plugin but add a container owasp/modsecurity-crs in the app stack and use this as backend of Traefik.
Another solution could be to use a single owasp/modsecurity-crs container and overwrite the config file conf/extra/httpd-vhosts.conf
to specify your own backends.
That’s it folks.