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:

  1. in docker-compose.yml, copy/paste the waf service, rename it to your liking (eg: wafparanoia4) and adapt the environment variables (eg: PARANOIA=4)
  2. in docker-compose.yml, add a new HTTP middleware matching your new WAF service (eg: wafparanoia4) to Traefik pointing to this new WAF container
  3. 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
  4. restart your Traefik stack
  5. restart your app stack
  6. 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.