У попередній статті я описала розслідування підозрілого трафіку: різкі сплески переглядів, повторювані 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 вирішує долю поточного запиту і негайно повертає 403.
Fail2ban читає вже створену подію в журналі й вирішує, чи потрібно тимчасово заблокувати джерело наступних запитів.
Тобто Fail2ban не замінює правило Nginx. Він додає другий рівень після правильної відмови.
Чутливі шляхи визначаю до проксування
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 читає лише журнал високодостовірних подій
sudo apt update sudo apt install fail2banЯ не блокую адресу лише через велику кількість переглядів, мобільний браузер або країну.
Для цього правила подією є вже розпізнана спроба звернення до чутливого шляху.
Фільтр Fail2ban залишається простим
[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,httpsmaxretry = 1 тут є свідомим вибором.
Я не використовую такий поріг для широких припущень. Але звичайному читачеві немає причини запитувати /.env або /.git.
Після однієї такої події адреса блокується на 86400 секунд, тобто на 24 години.
Блокування стосується лише HTTP і HTTPS
Це правило стосується вебтрафіку.
Тому блокування обмежене портами 80 і 443.
SSH я до цього правила не додаю. Захист адміністративного доступу є окремою задачею.
Fail2ban передає тимчасове блокування в nftables
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-pathsfail2ban-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 зменшують кількість наступних запитів від того самого джерела.



