Docker Web Application Firewall (WAF) for Protecting Web Apps against AI Scrapers
1.9K
Source Code: git.tjdev.de/DrMaxNix/fuckai
Docker Web Application Firewall (WAF) for Protecting Web Apps against AI Scrapers

Caution
As the fuckai container and your application don't know the client's IP address (they only see the address of the container before them), the first link in the chain (eg. your reverse proxy) must forward the client's IP address to following containers using the `X-Forwarded-For` HTTP header. fuckai expects this header to be set; a missing or wrong `X-Forwarded-For` header value will lead to unexpected behavior!
Caution
If your server is reachable via public IPv6 and your reverse proxy is running inside a docker container, you must also enable IPv6 in your docker network: `enable_ipv6: true`. If you don't do this, IPv6 connections will not use docker's NAT, they will be proxied to IPv4 instead; thus, from the persepective of your reverse proxy, these connections will come from a local address (e.g. `172.18.0.1`). In this case, all clients will be treated as having the same ip address, which basically defeats the purpose of giving different keys to different clients.
Add the fuckai service to your docker-compose.yml, mount your host system's timezone files and point it to your application container:
services:
fuckai:
image: drmaxnix/fuckai:1
restart: unless-stopped
environment:
APPLICATION_HOST: "app"
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- 127.0.0.1:80:80
app:
# ...
Now point your reverse proxy at port 80 of the fuckai container.
Important
Make sure, you don't point your reverse proxy directly at the application container and check that your application container has no publicly accessible http/https ports (eg. remove `ports` entries from `docker-compose.yml`)
Following configuration options are available by setting environment variables:
| Option | Type | Default | Description |
|---|---|---|---|
| DEBUG | bool | false | Whether debug information should be visible (not safe for production usage!) |
| DEBUG_BREAK_CHALLENGE | bool | false | Whether the challenge should always fail; useful for keeping the challenge page on screen while tweaking the note and footer content |
| LOG_ALL | bool | false | Log following requests: All |
| LOG_API | bool | false | Log following requests: Going to the fuckai API |
| LOG_CHALLENGE | bool | false | Log following requests: Intercepted by a challenge page |
| LOG_VALIDATED | bool | false | Log following requests: Allowed to access the application only because they presented a valid key |
| BRANDING | bool | true | Whether branding text should be displayed on the challenge page |
| KEYTYPE | string | secret+ip+date | How to build the cookie keys (see below for possible values and explanations) |
| CHALLENGE | string | js | Which kind of challenge to use (see below for possible values and explanations) |
| APPLICATION_HOST🔸 | host/ip | Hostname or ip address of application to forward requests to | |
| APPLICATION_PORT | int | 80 | Port number of application to forward requests to |
| COMMENT | string | Comment text to add to the first and last line of the challenge page's raw html (here it is most likely to show up in an error message if the challenge page is breaking some kind of api; this text could contain a short explanation and ways to contact you for reporting problems) | |
| NOTE_HTML | string | Custom html code to be inserted between the explanation text and the status section (make sure to wrap any text in <span> elements!) | |
| FOOTER_HTML | string | Custom html code to be inserted at the end of the page (make sure to wrap any text in <span> elements!) |
Options marked with
🔸are required
The key that allows the client temporary access to the application is built from multiple components. Generally, it always contains a secret component, which will be regenerated at each container start. All components will then be hashed to basically create a signed form of all non-secret components. Selecting the right components to build the key is crucial in determining when clients will be presented with the challenge page again. Or in other words: they determine how long and for which client a single key is valid. Following component combinations are currently available:
| Keytype | Key derivation example (simplified) | Description |
|---|---|---|
secret | sha1("LBAdA7k9...") | All clients share the same key; key becomes invalid only on container restarts |
secret+date | sha1("LBAdA7k9..." + "2025-09-29") | All clients share the same key; key becomes invalid every day |
secret+week | sha1("LBAdA7k9..." + "2025-35") | All clients share the same key; key becomes invalid every week |
secret+ip | sha1("LBAdA7k9..." + "203.0.113.124") | Every client receives their own key; key becomes invalid only on container restarts |
secret+ip+date | sha1("LBAdA7k9..." + "203.0.113.124" + "2025-09-29") | Every client receives their own key; key becomes invalid every day |
secret+ip+week | sha1("LBAdA7k9..." + "203.0.113.124" + "2025-35") | Every client receives their own key; key becomes invalid every week |
To separate bots from living creatures, we ask visitors to do a challenge. Depending on how eagerly you want to do this separation, you can pick from multiple challenges with varying difficulty. Following challenges are currently available:
| Challenge | Description |
|---|---|
js | The client's browser must execute a small JavaScript snippet; does not require any user interaction |
button | The user must click on a big button on the challenge page; requires easy/short user interaction |
To prevent losing SEO or breaking APIs, certain paths can be added to a whitelist, which will cause these paths to always be allowed, regardless of whether a valid key has been provided or not. For this, there are three files that can be mounted into the container:
/allow-str.txt: List of allowed paths (exact match)/allow-beg.txt: List of allowed path prefixes/allow-reg.txt: List of allowed path regex patternsIf you have these files set up and saved in the same directory as your docker-compose.yml, you can simply mount them into the container as follows:
services:
fuckai:
# ...
volumes:
# ...
- ./allow-str.txt:/allow-str.txt:ro
- ./allow-beg.txt:/allow-beg.txt:ro
- ./allow-reg.txt:/allow-reg.txt:ro
allow-str.txt:
# allow robots and sitemap files
/robots.txt
/sitemap.xml
# allow home page for seo
/
# allow the about main page
/about
allow-beg.txt:
# allow all api calls
/api/
# allow all files in the .well-known directory
/.well-known/
# allow all files in the assets directory
/assets/
# allow the about sub-pages
/about/
allow-reg.txt:
# allow user profile pages
^/user/[a-z0-9_-]+$
Content type
Image
Digest
sha256:486831208…
Size
9.1 MB
Last updated
29 days ago
docker pull drmaxnix/fuckai