0% прочитано

Як я захистила Laravel від сканерів чутливих шляхів за допомогою Nginx і Fail2ban

Після розслідування автоматизованого сканування я винесла захист чутливих шляхів перед Laravel. Nginx одразу повертає 403, окремий журнал фіксує небезпечну спробу, Fail2ban читає цей журнал, а nftables тимчасово блокує джерело на HTTP і HTTPS.

10 жовтня 2026 р. 12 хв читанняNginx

У попередній статті я описала розслідування підозрілого трафіку: різкі сплески переглядів, повторювані IP, послідовний обхід сторінок і звернення до /.env, /app/.env, /.git та wp-config.php.

Розслідування показало ще одну проблему: небезпечний запит не завжди зупинявся там, де я очікувала.

Особливо показовим був /app/.env. Через пріоритет правил Nginx цей шлях міг потрапити в окреме проксування і завершитися 500 замість ранньої відмови.

Після цього я змінила саму межу захисту.

Високодостовірний небезпечний запит не повинен доходити ні до Laravel, ні до іншого внутрішнього сервісу.

Мета цього захисту не в тому, щоб визначити всіх ботів. Я блокую лише вузький набір запитів, які мають дуже високу ймовірність бути скануванням чутливих файлів.

Небезпечний запит краще зупинити до Laravel

Відмова якомога раніше
Небажано: Internet   |   vNginx   |   vLaravel / інший сервіс   |   vобробка   |   vвідмова 

Для запиту до /.env немає сенсу завантажувати Laravel, виконувати проміжні обробники, звертатися до бази або передавати його іншому внутрішньому сервісу.

Якщо шлях однозначно належить до заборонених, вебсервер може завершити запит раніше.

Це не замінює захист самого застосунку. Це лише правильніша межа для запиту, якого всередині взагалі не повинно бути.

Чому одного regex location виявилося недостатньо

Спрощений приклад конфлікту правил
location ^~ /app/ {    proxy_pass http://realtime_upstream;} location ~ /\. {    deny all;}

Загальне правило для прихованих файлів виглядало достатнім, доки поруч не виявився пріоритетніший префіксний location.

location ^~ /app/ змінює порядок вибору правил. Після його вибору Nginx уже не перевіряє звичайні regex location.

Через це /app/.env міг оминути загальне правило для прихованих файлів.

Я вирішила не прив’язувати захист чутливих шляхів до того, який прикладний location переможе.

Як тепер працює захист

Два рівні захисту
HTTP-запит    |    vNginx    |    | чутливий шлях?    |    +---- ні ----> звичайна обробка    |    так    |    v403    |    vокремий журнал безпеки
Схема захисту: запит надходить до Nginx, небезпечна спроба записується в окремий журнал, Fail2ban читає журнал, а nftables тимчасово блокує IP.
Схема захисту від сканування чутливих шляхів.

Тут є дві різні відповідальності.

Nginx вирішує долю поточного запиту і негайно повертає 403.

Fail2ban читає вже створену подію в журналі й вирішує, чи потрібно тимчасово заблокувати джерело наступних запитів.

Тобто Fail2ban не замінює правило Nginx. Він додає другий рівень після правильної відмови.

Чутливі шляхи визначаю до проксування

Спрощений map для високодостовірних шляхів
map $uri $sensitive_path {    default 0;     ~*^/\.env(?:$|/)       1;    ~*^/app/\.env(?:$|/)   1;    ~*^/\.git(?:$|/)       1;    ~*^/wp-config\.php$    1;}

map $uri створює окрему ознаку небезпечного шляху, яка не залежить від прикладного location.

Я навмисно не додаю сюди кожен дивний URL, який можна зустріти в інтернеті.

Для автоматичного блокування краще мати короткий перелік дуже надійних ознак, ніж широке правило з великою кількістю хибних спрацювань.

Небезпечні запити записую в окремий журнал

Окремий журнал і рання відмова
log_format sensitive_path    '$remote_addr $time_iso8601 '    '"$request" $status'; server {    access_log        /var/log/nginx/security-sensitive.log        sensitive_path        if=$sensitive_path;     if ($sensitive_path) {        return 403;    }     # Normal locations continue below.}

Окремий журнал спрощує наступний рівень захисту.

Fail2ban не потрібно переглядати весь звичайний журнал Nginx і намагатися відрізняти нормальний 404 від спроби знайти .env.

До спеціального файла потрапляють лише запити, які вже пройшли вузьку перевірку чутливого шляху.

Перед застосуванням завжди перевіряю Nginx

Перевірка перед застосуванням
sudo nginx -t sudo systemctl reload nginx

Після зміни конфігурації я не перезавантажую Nginx навмання.

Спочатку nginx -t має підтвердити правильність конфігурації.

Тільки після цього виконую reload.

Спочатку перевіряю Nginx без автоматичного блокування

Локальна перевірка на сервері
DOMAIN=example.com curl -k -I \  --resolve "$DOMAIN:443:127.0.0.1" \  "https://$DOMAIN/app/.env"

За допомогою --resolve можна направити домен на 127.0.0.1 без зміни DNS.

Очікуваний результат для чутливого шляху - 403.

Таку перевірку я виконую до ввімкнення автоматичного блокування або лише тоді, коли loopback-адреси гарантовано виключені з правил Fail2ban.

Fail2ban читає лише журнал високодостовірних подій

Встановлення Fail2ban на Ubuntu
sudo apt update sudo apt install fail2ban

Я не блокую адресу лише через велику кількість переглядів, мобільний браузер або країну.

Для цього правила подією є вже розпізнана спроба звернення до чутливого шляху.

Фільтр Fail2ban залишається простим

/etc/fail2ban/filter.d/nginx-sensitive-paths.conf
[Definition] failregex = ^<HOST>\s+\S+\s+".*"\s+403$ ignoreregex =

Фільтр не намагається повторно визначити, чи небезпечний URL.

Це вже зробив Nginx.

Fail2ban лише читає спеціальний журнал і отримує IP із події, яка завершилася 403.

Для високодостовірної події достатньо одного спрацювання

Приклад спеціалізованого правила
[nginx-sensitive-paths] enabled = true filter = nginx-sensitive-paths logpath =    /var/log/nginx/security-sensitive.log maxretry = 1 findtime = 600 bantime = 86400 port = http,https

maxretry = 1 тут є свідомим вибором.

Я не використовую такий поріг для широких припущень. Але звичайному читачеві немає причини запитувати /.env або /.git.

Після однієї такої події адреса блокується на 86400 секунд, тобто на 24 години.

Блокування стосується лише HTTP і HTTPS

Це правило стосується вебтрафіку.

Тому блокування обмежене портами 80 і 443.

SSH я до цього правила не додаю. Захист адміністративного доступу є окремою задачею.

Fail2ban передає тимчасове блокування в nftables

Наступне з’єднання блокується ще до Nginx
Nginx   |   vжурнал безпеки   |   vFail2ban   |   vnftables   |   Xнаступне HTTP-з'єднання

Перший небезпечний запит доходить до Nginx, отримує 403 і створює запис у журналі.

Після цього Fail2ban додає тимчасове правило в nftables.

Наступні вебз’єднання із заблокованої адреси вже не доходять до Nginx.

UFW і nftables можуть виконувати різні ролі

UFW може залишатися відповідальним за постійну політику доступу сервера, а Fail2ban додавати тимчасові правила через nftables.

Для мене важливо перевірити фактичний результат після блокування, а не покладатися лише на назву одного інструмента.

Загальні правила міжмережевого екрана і SSH я через цю задачу не змінюю.

Конфігурацію Fail2ban теж перевіряю до використання

Перевірка конфігурації та стану
sudo fail2ban-client -t sudo fail2ban-client status sudo fail2ban-client status \  nginx-sensitive-paths

fail2ban-client -t для мене такий самий обов’язковий крок, як nginx -t.

Після запуску перевіряю як загальний стан Fail2ban, так і конкретне правило для чутливих шляхів.

Фактичне блокування перевіряю в nftables

Перевірка мережевих правил
sudo nft list table inet f2b-table

Статус Fail2ban показує його власний стан.

Я додатково перевіряю nftables, щоб переконатися, що мережеве правило справді створене.

Перевіряю не тільки небезпечні, а й звичайні шляхи

Чотири небезпечні і чотири контрольні приклади
Мають розпізнатися: /.env/app/.env/.git/config/wp-config.php  Не повинні розпізнатися: //posts/example/assets/app.css/app/socket

Я перевіряю не тільки те, що небезпечні URL знаходяться.

Не менш важливо довести, що звичайні маршрути не потрапляють під правило.

У перевірці чотири небезпечні приклади розпізналися, а чотири контрольні не дали хибного збігу.

Для тесту використовую зарезервовану адресу, а не реальну IP відвідувача

Зарезервована документаційна адреса
192.0.2.123

Для синтетичної перевірки я використовую адресу з діапазону, зарезервованого для документації.

Так можна перевірити блокування і розблокування, не копіюючи реальну адресу відвідувача в команди або публічну статтю.

Окремо перевіряю весь автоматичний шлях

Синтетична подія в журналі
TEST_IP=192.0.2.123 printf '%s %s "%s" %s\n' \  "$TEST_IP" \  "$(date --iso-8601=seconds)" \  'GET /.env HTTP/1.1' \  '403' \  | sudo tee -a \      /var/log/nginx/security-sensitive.log

Ручне додавання IP у nftables доводить лише те, що блокування технічно можливе.

Мені потрібно було перевірити повний шлях: журнал -> Fail2ban -> nftables.

У моїй перевірці синтетична подія викликала автоматичне блокування приблизно за дві секунди.

Після тесту прибираю тестове блокування

Очищення після тесту
sudo fail2ban-client set \  nginx-sensitive-paths \  unbanip 192.0.2.123 sudo fail2ban-client status \  nginx-sensitive-paths

Успішний тест не завершений, поки його стан не прибраний.

Після перевірки тестова адреса була розблокована, а активних тестових блокувань не залишилося.

У звичайній роботі Fail2ban видаляє тимчасове блокування автоматично після завершення bantime.

Зміни захисту застосовую через перевірку, застосування і контроль стану

Приклад контрольованого застосування
sudo ./server-protection.sh --check sudo ./server-protection.sh --apply sudo ./server-protection.sh --status

Зміни вебсервера та мережевого захисту я не хочу застосовувати набором випадкових ручних команд.

Спочатку створюю резервні копії потрібних конфігурацій, виконую перевірку, переглядаю результат і тільки після цього дозволяю застосування.

Окремий режим перевірки стану допомагає швидко переконатися, що після зміни система залишилася в очікуваному стані.

Так само важливо мати зрозумілий шлях відкату.

Що я навмисно не блокую цим правилом

Я не блокую:

  • усі адреси з певної країни;
  • мобільні пристрої;
  • певний браузер;
  • усіх клієнтів із великою кількістю переглядів;
  • звичайні 404;
  • SSH через це правило для вебтрафіку.

Кожна з цих ознак може мати нормальне пояснення.

Автоматичне блокування залишаю для подій, де ризик хибного спрацювання значно нижчий.

Чого я не роблю під час таких змін на сервері

Кілька окремих правил безпеки роботи:

  • не перевіряю небезпечні URL зі своєї реальної публічної IP після ввімкнення автоматичного блокування;
  • не змінюю SSH без окремої причини та окремої перевірки;
  • не перебудовую загальні правила міжмережевого екрана разом із маленькою зміною Nginx;
  • не запускаю apt autoremove лише через встановлення Fail2ban;
  • не перезавантажую весь сервер, якщо зміна цього не потребує;
  • не переписую історичну аналітику через нове правило захисту.

Що я перевірила перед тим, як залишити захист увімкненим

Перевірка всього ланцюга
Визначення шляхів 4 небезпечні-> розпізнано 4 контрольні-> не розпізнано  Nginx чутливий шлях-> 403  Fail2ban

Один успішний тест ще не доводить відсутність хибних блокувань

Автоматична перевірка довела, що технічний ланцюг працює.

Але після ввімкнення я все одно спостерігаю за окремим журналом, кількістю блокувань і тим, які URL їх викликали.

Це особливо важливо для правила з maxretry = 1.

Якщо легітимний сценарій коли-небудь потрапить під правило, краще звузити саме правило, а не називати хибне блокування нормальною ціною захисту.

Мій список перевірки перед автоматичним блокуванням

Я перевіряю:

  • Чи Nginx знаходить лише високодостовірні чутливі шляхи?
  • Чи звичайні URL не потрапляють під правило?
  • Чи заборонений шлях повертає 403 до проксування?
  • Чи подія з’являється в окремому журналі?
  • Чи проходить nginx -t?
  • Чи проходить fail2ban-client -t?
  • Чи активне лише очікуване спеціалізоване правило?
  • Чи блокуються тільки порти 80/443?
  • Чи SSH не зачеплений?
  • Чи блокування реально з’являється в nftables?
  • Чи автоматичний тест проходить від журналу до nftables?
  • Чи тестова адреса після перевірки розблокована?
  • Чи є резервна копія і зрозумілий шлях відкату?

Захист працює шарами, а не одним великим правилом

Після цього налаштування небезпечний запит більше не залежить від випадкового збігу з потрібним location.

Nginx розпізнає вузький набір чутливих шляхів і повертає 403 ще до Laravel або іншого внутрішнього сервісу.

Окремий журнал перетворює цю відмову на чітку подію безпеки.

Fail2ban читає подію, а nftables тимчасово блокує наступні вебз’єднання від того самого джерела.

При цьому я не намагаюся перетворити систему на універсальний засіб визначення ботів і не блокую широкі групи відвідувачів за слабкими ознаками.

Небезпечний запит краще зупинити на рівні вебсервера, а автоматичне мережеве блокування додавати лише там, де сигнал достатньо точний.

Nginx зупиняє небезпечний запит зараз. Fail2ban і nftables зменшують кількість наступних запитів від того самого джерела.