Одного дня статистика блогу почала виглядати дуже приємно: переглядів стало помітно більше.
Частина трафіку приходила вночі, але сама по собі нічна активність ще нічого не доводить. Сайт доступний у різних часових поясах, а реальні люди теж читають у будь-який час.
Більше насторожило інше: аналітика позначала значну частину цих переглядів як звичайний людський трафік, але форма активності виглядала неприродно.
Тому замість того, щоб радіти новим цифрам або одразу оголошувати весь трафік ботами, я почала перевіряти його знизу вгору.
Першим сигналом були не великі цифри, а їхня форма
Звичайне зростання може виглядати так: час | | /\ | /\/ \__ |__/ \__ +----------------> Підозрілий сценарій: час | | |||||| | ||||||Один великий показник сам по собі слабкий сигнал.
Набагато корисніше дивитися, як він сформувався: за який час, скільки було унікальних відвідувачів, які сторінки відкривалися і чи повторювався один і той самий шаблон.
Коли багато переглядів з’являються короткими серіями, я хочу побачити первинні HTTP-запити, а не лише агреговану статистику.
Загальні та дедупліковані перегляди відповідають на різні питання
1000 загальних переглядів не обов'язково означає 1000 окремих читачів Наприклад: 1 джерело-> 50 сторінок-> повторні проходи-> сотні HTTP-запитівПорівняння загальних та дедуплікованих переглядів стало одним із перших корисних кроків.
Велика різниця між ними не доводить автоматизацію, але показує, що значну частину числа формують повторні звернення.
Це вже причина перейти від панелі аналітики до серверних журналів.
Android Chrome у User-Agent не доводить, що за запитом стоїть людина
Mozilla/5.0 (...)AppleWebKit/537.36 (...)Chrome/... Mobile Safari/...User-Agent корисний як один із сигналів, але клієнт надсилає його сам.
Автоматизований скрипт може представитися Android Chrome так само легко, як звичайний браузер.
Тому я не будую висновок лише на назві браузера або пристрою.
Це добре узгоджується з ширшим принципом: класифікація облікового запису і визначення автоматизованого запиту є різними речами.
Журнал Nginx показує окремі HTTP-запити
sudo awk '{print $1}' /var/log/nginx/access.log \ | sort \ | uniq -c \ | sort -nr \ | head -20Ця команда не визначає ботів. Вона лише показує, які адреси створили найбільше записів у поточному журналі.
Далі я дивлюся не тільки на кількість, а й на послідовність URL, інтервали між запитами та повторюваність поведінки.
Для іншого формату журналу позиції полів можуть відрізнятися, тому команди на основі awk потрібно адаптувати до реального log_format.
Послідовний обхід багатьох сторінок сильніший за один дивний запит
/posts/article-1/posts/article-2/posts/article-3/posts/article-4/posts/article-5... короткі інтервалиодне джерелодовгі послідовностіОдин відвідувач цілком може швидко перейти між кількома статтями.
Але довгий послідовний обхід великої кількості сторінок з короткими інтервалами вже дає інший контекст.
Особливо якщо той самий клієнт паралельно перевіряє URL, які звичайному читачеві взагалі не потрібні.
Звернення до .env, .git і wp-config.php змінило картину
sudo grep -E '/(\.env|app/\.env|\.git|wp-config\.php)' \ /var/log/nginx/access.log \ | tail -50Запити до /.env, /app/.env, /.git або wp-config.php вже мають зовсім іншу семантику, ніж читання статей.
Такі шляхи регулярно перевіряють автоматизовані сканери в пошуках помилково опублікованих секретів, репозиторіїв або конфігурацій інших платформ.
Особливо показовим для мене було поєднання: клієнт виглядає як звичайний мобільний браузер, читає сторінки сайту, а потім перевіряє типові чутливі файли.
HTTP-статус допомагає зрозуміти, що сталося із запитом
403-> сервер навмисно відмовив у доступі 404-> ресурс не знайдений 499-> клієнт закрив з'єднання до завершення відповіді 500-> під час обробки сталася серверна помилкаЯ не трактую всі невдалі запити однаково.
403 може означати, що захист спрацював саме так, як задумано.
404 для випадкового сканованого шляху теж не обов’язково є проблемою застосунку.
А 500 вже змушує перевірити, чому небезпечний URL взагалі дійшов до обробника, здатного впасти.
HTTP 499 не означає, що Laravel обов’язково зарахував перегляд
sudo awk '$9 == 499 || $9 == 500' \ /var/log/nginx/access.log \ | tail -50499 є статусом Nginx для ситуації, коли клієнт закрив з’єднання раніше, ніж сервер завершив відповідь.
Але з одного запису 499 я не можу зробити висновок, що Laravel Analytics точно записав або точно не записав перегляд.
Це залежить від того, наскільки далеко запит устиг пройти до моменту розриву і в якій точці застосунок записує аналітичну подію.
Тому кількість рядків у Nginx access log і кількість записів у прикладній аналітиці не повинні механічно збігатися.
Найцікавішим був 500 для /app/.env
Для чутливого файла я очікувала відмову або відсутність ресурсу, але один із варіантів шляху поводився інакше.
/app/.env потрапляв у маршрут, який повертав 500.
Спочатку це виглядало як проблема Laravel. Насправді причина була раніше: у правилах вибору location в Nginx.
Захист regex location можна обійти іншим пріоритетнішим location
location ^~ /app/ { proxy_pass http://realtime_upstream;} location ~ /\. { deny all;}У спрощеному вигляді проблема полягала в тому, що для /app/ існувало окреме правило з ^~, яке проксувало трафік до іншого сервісу.
Після вибору такого префіксного location Nginx уже не застосовує звичайні regex location, які могли б заблокувати прихований файл.
У результаті /app/.env не потрапляв під очікуване правило захисту і йшов до зовсім іншого upstream.
Саме тому дивний 500 виявився корисним сигналом конфігураційної прогалини.
Я перевіряю фактичну конфігурацію Nginx, а не лише файл, який пам’ятаю
sudo nginx -T 2>&1 \ | grep -nE 'location|proxy_pass|deny all|server_name'nginx -T показує вже зібрану конфігурацію з усіма підключеними файлами.
Це важливо, коли правила рознесені між кількома конфігураціями або коли один location непомітно має вищий пріоритет за інший.
Я хочу бачити саме те, що реально виконує Nginx.
Nginx Logs і Laravel Analytics показують різні рівні системи
Internet | vNginx access log | | request may stop here vLaravel | | application rules vAnalytics event | vAggregated statisticsNginx бачить запит раніше за Laravel.
Частина запитів може завершитися на рівні вебсервера, частина дійде до застосунку, а аналітика ще додатково застосує власні правила включення, дедуплікації та класифікації.
Тому я використовую ці джерела разом, але не очікую від них однакових цифр.
Країна сама по собі не є ознакою бота
Під час такого розслідування дуже легко перейти від "я бачу багато підозрілих запитів із цього регіону" до "увесь трафік із цієї країни є ботами".
Я так не роблю.
Країна є лише одним виміром аналітики. Реальні читачі, VPN, мобільні оператори, хмарні мережі та автоматизовані клієнти можуть змішуватися в одному географічному зрізі.
Я класифікую поведінку, а не громадянство або географію користувача.
Я шукаю комбінацію сигналів, а не одну магічну ознаку
Слабкий сигнал окремо: - нічний час- Android Chrome- багато переглядів- одна країна Сильніший контекст разом: - повторюваний IP- короткі інтервали- послідовний обхід сторінок- багато однотипних запитів- probes до .env / .git- характерні HTTP-статусиЯ не переписую історичну аналітику після одного розслідування
Після того як частина старого трафіку стала виглядати підозріло, можна було б спробувати заднім числом виключити її з усіх графіків.
Я цього не роблю без окремого надійного правила міграції.
Нове розуміння допомагає покращити майбутню класифікацію і захист, але воно не робить кожен історичний запис однозначно неправильним.
Мій порядок перевірки підозрілого трафіку
Analytics spike | vtotal vs deduplicated views | vNginx access log | vtop IPs | vrequest sequence | vsensitive-path probesЩо я перевіряю, коли статистика раптом різко зростає
Мій короткий список:
- Як співвідносяться загальні та дедупліковані перегляди?
- Чи є короткі різкі серії запитів?
- Які IP створюють найбільше записів у журналі?
- Чи обходить одне джерело сторінки послідовно?
- Чи є звернення до
.env,.git,wp-config.phpабо подібних шляхів? - Які HTTP-статуси повертаються?
- Чи дивний 500 не вказує на неправильний Nginx location?
- Чи не покладається класифікація лише на User-Agent?
- Чи не роблю я висновок про ботів лише за країною?
- Чи порівнюю я Nginx Logs і Laravel Analytics як різні джерела, а не як однакові лічильники?
Не кожен перегляд, позначений як людський, зробила людина
Найважливіший результат цього розслідування для мене був не в тому, щоб поставити ярлик "бот" на певну групу відвідувачів.
Важливіше було побачити межі самої аналітики.
User-Agent можна підробити. Велика кількість переглядів не дорівнює великій аудиторії. 499 у журналі Nginx не доводить, що Laravel записав перегляд. А країна не є достатньою підставою для блокування.
Натомість журнали показали поведінку, яку аналітичний лічильник сам по собі не міг пояснити: повторювані джерела, послідовний обхід сторінок і перевірку чутливих шляхів.
А дивний 500 на /app/.env додатково відкрив прогалину в правилах Nginx.
Я довіряю не одному ярлику, а сукупності сигналів із різних рівнів системи.
Наступним кроком стало вже не розслідування, а захист: зупиняти високодостовірні небезпечні запити ще на рівні вебсервера і тимчасово блокувати джерела повторних атак.
Аналітика показує, як система класифікувала трафік. Журнали допомагають перевірити, як цей трафік насправді поводився.



