Fail2ban log-analyse: wat onze filters misten en hoe we het oplosten
Fail2ban staat erom bekend dat het brute-force en bekende scan-patronen blokkeert. Maar “bekende patronen” is precies de adder onder het gras: als jouw filter een patroon niet herkent, bestaat de aanval voor fail2ban simpelweg niet. Onlangs heb ik met fail2ban-regex de échte logs van onze server doorgelicht, en dat leverde een verrassing op: meerdere actieve aanvalscampagnes die de hele week door scanden, zonder dat één regel uit onze filters ze zag.
Dit is het verhaal van wat we misten, hoe we het vonden, en wat we ertegen deden.
De methode: missed-line analyse
De standaard aanpak is filters uitbreiden zodra je een ban mist. Maar dat is reactief — je mist een ban pas nadat een aanval al (deels) is gelukt, en je ziet alleen wat je toevallig opvalt.
De betere methode is proactief: draai elk filter tegen een week aan echte logregels en kijk welke verdachte regels geen enkele regel matchen. Fail2ban heeft daar een ingebouwde tool voor:
fail2ban-regex /var/log/nginx/access.log \
/etc/fail2ban/filter.d/nginx-scan.conf --print-all-missed
Alle regels die hieruit rollen zijn verkeer dat door onze hele detectie gleed. Over een week aan access.log (ruim een miljoen regels) sorteerde ik de duizenden “missed” regels op patroon, en er kwamen vier klassen boven die we volledig misten.
Wat we misten
1. AI-assistant credential-scans (de grootste blinde vlek)
De meest opvallende vondst was een gerichte en actieve campagne: scanners die kwamen zoeken naar credentials van AI-ontwikkeltools.
GET /.claude/credentials.json
GET /.claude/settings.json
GET /.claude.json
GET /.codex/auth.json
GET /.config/codex/auth.json
GET /.config/gcloud/application_default_credentials.json
Deze tools — Claude Code, OpenAI Codex, Google Cloud — slaan authenticatietokens op in bestanden in de home-directory. Aanvallers scannen daar massaal op, want een blootgesteld .codex/auth.json is meteen een bruikbaar API-token. Alleen: geen enkele regel in ons filter herkende deze paden. Vier willekeurig gekozen IP’s die dit deden genereerden samen 173 requests — nul fail2ban-matches. 88 unieke IP’s deden dit soort scans in één week.
2. Secret- en credential-bestandsnamen
De tweede klasse zijn generieke geheime bestanden die aanvallers op willekeurige servers proberen:
GET /credentials.json
GET /secrets.yml
GET /client_secrets.json
GET /terraform.tfstate
GET /id_rsa
GET /server.key
GET /docker-compose.yml
GET /firebase-admin.json
GET /keys/service-account.json
Ons filter dekte .env, .git, .aws, WordPress en database-tools prima, maar deze bredere reeks van secret-bestandsnamen ontbrak compleet. Met name de moderne varianten — serviceAccountKey.json, firebase-admin.json, application_default_credentials.json — waren onzichtbaar.
3. Symfony profiler-scanning
Daarnaast vonden we tests op de Symfony framework debug-profiler:
GET /_profiler/phpinfo
GET /app_dev.php/_profiler/open?file=app/config/parameters.yml
Dit is een bekende begin-stap van aanvallen op Symfony-applicaties: de profiler lekt configuratie en omgevingsvariabelen als hij onbedoeld openstaat. 57 unieke IP’s probeerden dit in een week.
4. WordPress REST API varianten
Onze wp-json-regel was te strikt — hij verwachtte exact /wp-json/. Aanvallers omzeilen dat op eenvoudige manieren:
GET //wp-json/batch/v1 (dubbele slash)
GET /blog/wp-json/batch/v1 (pad-prefix)
GET /wordpress/wp-json/batch/v1
GET /wp-json (zonder trailing slash)
Deze varianten misten onze filter volledig. Vooral de dubbele-slash- en pad-prefix-trucs zijn makkelijk te missen, en sommige IP’s die uitsluitend díe varianten gebruikten kregen geen enkele ban.
Wat we ertegen deden
De fix zat in één filterbestand: /etc/fail2ban/filter.d/nginx-scan.conf. Ik voegde vier nieuwe regelpatronen toe:
1. AI-assistant tools — expliciete losse regels voor .claude, /claude/, .codex, .config/codex en codex/auth.json.
2. Secret-bestanden — een gecombineerde regel voor credentials, client_secrets, application_default_credentials, secrets, serviceAccountKey, service-account, firebase-admin, .docker/config.json, .aws/credentials met de gebruikelijke extensies (json, yml, yaml, ini, txt, xml, toml, enc), plus id_rsa, id_ed25519, server.key, terraform.tfstate, docker-compose, password.txt.
3. Symfony profiler — een regel voor _profiler/.
4. WordPress varianten — de wp-json-regel uitgebreid met //, /blog/, /wordpress/-prefixen en een woordgrens zodat ook de slashloze vorm matcht.
Twee lessen over fail2ban-regex
Tijdens het testen liep ik tegen twee praktische valkuilen aan die het vermelden waard zijn:
Vermijd diepe alternatieftuples na een greedy
.*". Constructies als.*(\.claude(\.json|/)|/claude/(config\.json|...))faalden in de praktijk door backtracking — een simpele letterlijke match lukte wel, maar zodra ik de alternatieven in één tuple na een greedy.*"zette niet meer. De oplossing was losse regels: één regel per patroon, elke eindigend met\b. Dat werkte direct en is beter leesbaar.Statuscodes zijn je vriend. Alle nieuwe regels beperken zich tot
(403|404). Daardoor matchen we géén legitieme requests — bijvoorbeeld de.claude/-map die écht in een van onze Forgejo-repositories zit en gewoon met200wordt geserveerd. Die is nu veilig uitgesloten van detectie.
Het resultaat
Na de wijziging draaide ik dezelfde logs opnieuw door de nieuwe filter:
- 173/173 AI-assistant scanregels gevangen (was 0)
- 44/44 van de vandaag-voorkomende secret/profiler/wp varianten gevangen
- Over de volledige weeklog: matches gingen van ~1.100 naar 15.541 — een twaalfvoudige toename in gedetecteerde aanvallen
- 0 false positives op legitieme paden (geverifieerd: geen enkele 403/404 op onze eigen Forgejo-repositories of Nextcloud API-paden)
Het plan ging live met fail2ban-client reload nginx-scan, en de fail2ban-jail draait nu met de uitgebreide detectie.
Conclusie
Ons fail2ban-systeem was al vrij goed — 10 jails, progressieve banning, AbuseIPDB-rapportage. Maar “goed” betekent niet “compleet”. De missed-line analyse met fail2ban-regex liet zien dat aanvallers zich snel aanpassen: toen wij nog dachten aan .env en WordPress, scanden ze al AI-tool-credentials en moderne cloud-secrets.
De belangrijkste les: vertrouw niet op een filter, maar toets hem periodiek tegen je echte logs. Een uur missed-line analyse vond meer dan we in maanden aan incremental filterverbeteringen hadden opgemerkt.
Het is onderdeel van een bredere beveiligingscyclus — AI-tools en hun credential-structuren veranderen snel. Deze analyse hoort daarom geen eenmalige actie te zijn, maar een terugkerende controle.