Veřejný web dříve nebo později najdou automatické skenery. Na mém Raspberry Pi se to projevilo desítkami podobných záznamů v Apache error logu:
[Tue Oct 13 08:22:51.047444 2026] [php:error] [client 192.0.2.123:64739] script '/var/www/example/public/mail.php' not found or unable to stat
[Tue Oct 13 08:22:52.352933 2026] [php:error] [client 192.0.2.123:51802] script '/var/www/example/public/wp-config.php' not found or unable to stat
Nešlo o úspěšné napadení. Text not found or unable to stat naopak říká, že hledaný soubor na serveru nebyl. Skenery postupně zkoušejí názvy známých webshellů, konfiguračních souborů a děravých PHP aplikací. Přesto vytvářejí zbytečnou zátěž a rychle plní log.
Na takové opakované požadavky se hodí Fail2Ban. Sleduje logy, pomocí filtrů v nich rozpoznává podezřelé chování a po překročení nastaveného limitu přidá IP adresu do firewallového banu.
Použité prostředí
Tento postup jsem ověřil na následující kombinaci:
- Raspberry Pi OS založený na Debianu,
- Apache jako webový server,
- PHP obsluhované přímo z Apache,
- systemd a systemd journal,
- Fail2Ban 1.0.2.
Na jiné distribuci se mohou lišit názvy balíčků, umístění logů i výchozí firewallová akce. Není dobré bez ověření kopírovat cestu k logu z cizího návodu.
Instalace Fail2Ban
Na Raspberry Pi OS stačí distribuční balíček:
sudo apt update
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl is-active fail2ban
fail2ban-client --version
V mém případě byla výsledkem aktivní služba a verze:
active
Fail2Ban v1.0.2
Nejdřív najít správný Apache error log
Apache může používat společný /var/log/apache2/error.log, ale jednotlivý virtual host může mít vlastní soubor. Aktivní direktivy ErrorLog najdeme takto:
sudo grep -Rns '^[[:space:]]*ErrorLog' /etc/apache2/apache2.conf /etc/apache2/sites-enabled
Na mém serveru výstup ukázal obecný log i samostatný log webu:
/etc/apache2/apache2.conf:134:ErrorLog ${APACHE_LOG_DIR}/error.log
/etc/apache2/sites-enabled/000-default-ssl.conf:11:ErrorLog ${APACHE_LOG_DIR}/my-page.com-error.log
První test nad obecným error.log nenašel žádnou shodu. Nebyla to chyba filtru — podezřelé požadavky byly v logu konkrétního virtual hostu.
Ověření filtru apache-noscript
Balíček Fail2Ban už obsahuje filtr apache-noscript, který umí rozpoznat požadavky na neexistující skripty. Před zapnutím jailu je důležité filtr otestovat nad skutečným logem:
sudo fail2ban-regex /var/log/apache2/my-page.com-error.log /etc/fail2ban/filter.d/apache-noscript.conf
V mém logu filtr našel 257 relevantních řádků z 258:
Failregex: 257 total
Lines: 258 lines, 0 ignored, 257 matched, 1 missed
Jediný nezachycený řádek byl nesouvisející zákaz výpisu adresáře .well-known. To je správně: filtr nemá banovat libovolnou chybu Apache, ale pouze odpovídající pokusy o spuštění neexistujících skriptů.
0 matched, jail zatím nezapínejte. Nejprve zkontrolujte cestu k logu a formát jeho řádků. Jinak vznikne falešný pocit ochrany.
Jail pro neexistující PHP skripty
Distribuční jail.conf se přímo neupravuje, protože jej může přepsat aktualizace balíčku. Vlastní nastavení patří do souboru s příponou .local:
sudoedit /etc/fail2ban/jail.d/my-page-apache.local
Obsah:
[apache-noscript]
enabled = true
port = http,https
filter = apache-noscript
logpath = /var/log/apache2/my-page.com-error.log
backend = auto
maxretry = 5
findtime = 2m
bantime = 24h
Význam tří nejdůležitějších hodnot:
maxretry = 5— pět zachycených pokusů spustí ban,findtime = 2m— pokusy se musí vejít do dvou minut,bantime = 24h— IP zůstane blokovaná 24 hodin.
Pro přísnější ochranu lze později snížit maxretry například na 3. Hodnota 1 už může způsobit ban po jediném starém odkazu nebo překlepu, takže ji bez dobrého důvodu nedoporučuji.
Když chybí /var/log/auth.log
Po vytvoření Apache jailu jsem otestoval celou konfiguraci:
sudo fail2ban-client -t
Test nejprve skončil chybou:
ERROR Failed during configuration: Have not found any log file for sshd jail
Výchozí SSH jail hledal /var/log/auth.log, který na tomto systému neexistoval. SSH události byly pouze v systemd journalu. Řešením bylo nainstalovat Python rozhraní k journalu a nastavit SSH jail na backend systemd:
sudo apt install -y python3-systemd
sudoedit /etc/fail2ban/jail.d/sshd-systemd.local
Obsah souboru:
[sshd]
enabled = true
backend = systemd
Backend systemd nečte klasický logpath. Používá journalmatch dodaný SSH filtrem a události načítá přímo ze systemd journalu.
Kontrola konfigurace a spuštění
Po opravě SSH backendu už test prošel:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo systemctl is-active fail2ban
sudo fail2ban-client status
sudo fail2ban-client status apache-noscript
sudo fail2ban-client status sshd
Očekávaný stav je aktivní služba a seznam obsahující jaily apache-noscript a sshd. U SSH se místo souboru zobrazí journal filtr podobný tomuto:
Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
Varování 'allowipv6' not defined ... Using default one: 'auto' bylo v této verzi pouze informativní. Samotný test skončil hláškou OK: configuration test is successful.
Bezprostředně po restartu se také může stát, že první fail2ban-client status ještě nenajde socket. Pokud je služba aktivní a následující dotaz už funguje, šlo jen o krátký souběh při startu služby.
Jak poznat, že ban opravdu funguje
Stav Apache jailu zobrazíme příkazem:
sudo fail2ban-client status apache-noscript
Po prvním skutečném banu vypadal výsledek přibližně takto:
Currently failed: 0
Total failed: 8
Currently banned: 1
Total banned: 1
Banned IP list: 192.0.2.123
Currently banned znamená počet IP blokovaných právě teď. Total banned je počet banů od posledního spuštění daného jailu. Po uplynutí 24 hodin první hodnota klesne, zatímco celkový počet zůstane do restartu jailu zachovaný.
Události služby lze sledovat živě:
sudo journalctl -u fail2ban -f
Proč při maxretry 5 vzniklo osm řádků?
Fail2Ban neblokuje požadavek před jeho prvním zapsáním. Pracuje v tomto pořadí:
- Apache přijme požadavek a zapíše chybu.
- Fail2Ban načte nový řádek a vyhodnotí filtr.
- Po dosažení
maxretrynainstaluje firewallové pravidlo. - Teprve další síťový provoz z dané IP narazí na ban.
Útočný skener navíc posílal více požadavků paralelně z několika spojení. Než Fail2Ban zpracoval pátý řádek, měl Apache další požadavky už přijaté. Proto se do logu dostalo sedm nebo osm chyb. Je to očekávané chování, nikoli selhání banu.
Snížení limitu na 3 zkrátí reakci, ale ani potom nelze slíbit přesně tři řádky. Chceme-li zabránit samotnému vzniku PHP chyb, je potřeba navíc zachytit neexistující *.php už v konfiguraci Apache. Fail2Ban řeší opakujícího se klienta, nikoli první zalogování.
Stejně tak nelze věřit hodnotě Referer. Skener si může bez problémů poslat například https://www.google.com/, aniž by z Googlu skutečně přišel.
Změna limitu bez restartu celé služby
Trvalou hodnotu upravíme v lokálním jailu:
sudoedit /etc/fail2ban/jail.d/my-page-apache.local
Například:
maxretry = 3
Potom konfiguraci ověříme a znovu načteme pouze příslušný jail:
sudo fail2ban-client -t
sudo fail2ban-client reload apache-noscript
sudo fail2ban-client get apache-noscript maxretry
Poslední příkaz ukáže hodnotu, kterou běžící jail skutečně používá.
Co dělat při vlastním omylem způsobeném banu
Testovat pět neexistujících PHP adres z vlastní veřejné IP není dobrý nápad. Pokud se přesto zablokujete a máte k serveru jinou cestu, konkrétní IP lze odblokovat:
sudo fail2ban-client set apache-noscript unbanip VASE_IP_ADRESA
Nevkládejte ale široké rozsahy do výjimek jen kvůli pohodlí. Každá výjimka oslabuje ochranu a u dynamické veřejné adresy může časem patřit někomu jinému.
Shrnutí
Nejdůležitější část celého nasazení nebyla samotná instalace balíčku, ale ověření skutečného toku logů. Obecný Apache log byl prázdný, web používal vlastní ErrorLog a SSH události byly pouze v systemd journalu. Teprve test přes fail2ban-regex a kontrola obou aktivních jailů potvrdily, že ochrana opravdu funguje.
Výsledkem je jednoduchá vícevrstvá ochrana:
- Apache dál vrací 404 na neexistující skripty,
- Fail2Ban po několika pokusech odřízne opakujícího se skenera,
- SSH jail čte přihlašovací události ze systemd journalu.
Zdroje
