Две недели назад я закончил домашний проект — небольшую утилиту под Windows, — и решил, что ей нужна страница в интернете. Взял самый дешёвый VPS, так как много мощностей для моих задач не требуется, поднял nginx, выложил статическую страницу без единого внешнего скрипта. Счётчики вроде популярных метрик ставить не хотел принципиально: утилита не собирает никаких данных о пользователе, и было бы странно начинать “слежку” прямо со страницы о ней. Поэтому аналитику я сделал самую честную из возможных — разбор собственных логов nginx.
Через трое суток открыл сводку и увидел: 273 уникальных адреса, из них 165 открыли главную страницу. Для проекта, о котором не знает ровно никто — ни одной ссылки нигде, ни единого анонса — это выглядело как маленькое чудо. Чуда, разумеется, не было: живых посетителей за эти двое суток набралось около двадцати, а из четырёх скачиваний моего установщика ни одно не сделал человек!
Дальше больше: как я это выяснил, почему первый подход не сработал и что в итоге стоит на сервере?
Что вообще происходит на свежем домене?
Первым делом я посмотрел не на посещаемость, а на распределение кодов ответа — оно оказалось красноречивее любой сводки. Из 2 129 запросов успешными были только 613, а больше двух третей пришлось на ошибки. Причём ошибки не случайные, и это сразу видно по тому, за какими адресами ходили клиенты.
|
Код ответа |
Сколько раз |
|---|---|
|
403 |
934 |
|
200 |
613 |
|
404 |
487 |
|
304 |
35 |
|
400 |
20 |
|
405 |
19 |
|
401 |
5 |
Вот десятка самых популярных путей среди этих ошибок — классический перебор в чистом виде:
|
Количество |
Путь |
|---|---|
|
12 |
/.env |
|
6 |
/api/.env |
|
6 |
/.env.production |
|
6 |
/.env.backup |
|
6 |
/.git/config |
|
5 |
/web/.env |
|
5 |
/tmp/.env |
|
5 |
/server/.env |
|
5 |
/public/.env |
|
5 |
/private/.env |
Дальше по списку идут phpinfo.php во всех мыслимых и не мыслимых подпапках: /wp-admin/, /vendor/, /administrator/. Ботам нужен файл с паролями к базе, выгрузка git-репозитория или забытая админка — они обходят диапазоны адресов и пробуют наугад найти дыру в защите. На моём сайте нет ни PHP, ни базы, ни WordPress, только статические файлы, поэтому все попытки ушли в пустоту. Но проблема в том, что запросы-то сделаны, и в наивную статистику посещаемости они попадают наравне с живыми людьми.
Отдельно порадовали четыре адреса, давшие по: 276, 276, 269 и 269 запросов. Каждый из них за пару часов простучал под три сотни путей, и в сводке это выглядит как четыре заинтересованных очень настойчивых “посетителя”. Числа почти совпадают неспроста: это один и тот же сканер, запущенный с четырёх машин одного облачного провайдера. По отдельности каждый выглядит как посетитель с уникальным адресом, а вместе они дали больше половины всего «трафика» за сутки.
Попытка первая: фильтровать по User-Agent
Решение, которое приходит в голову первым: а что если отсеять всех, кто честно назвался роботом. В nginx — это делается картой, которая помечает запрос, плюс второй картой, дающей обратный флаг. Вторая нужна из-за особенности директивы if=: nginx пишет строку в лог, если переменная непустая и не равна нулю, поэтому «человечность» удобнее считать явно, а не полагаться на интуицию.
Карты для SET и GET флаг на запрос
map $http_user_agent $wc_bot { default 0; "~*(googlebot|yandexbot|bingbot|applebot|petalbot)" 1; "~*(gptbot|claudebot|ccbot|perplexitybot|bytespider)" 1; "~*(ahrefsbot|semrushbot|mj12bot|dotbot|blexbot)" 1; "~*(curl|wget|python-requests|go-http-client|okhttp)" 1; "~*(zgrab|masscan|censys|nuclei|sqlmap)" 1; "" 1; "-" 1;}map $wc_bot $wc_human { 1 0; default 1; }
Дальше в блоке server пишем два лога вместо одного: полный — для разбора инцидентов и «человеческий» — для статистики.
Логи
access_log /var/log/nginx/site.access.log;access_log /var/log/nginx/site.humans.log combined if=$wc_human;
Запустил, подождал, посмотрел — и разочаровался. По User-Agent отсеялось 267 запросов из 2 129, то есть около 12 %. При этом я своими глазами видел в логе адреса, которые ломились в /.env и представлялись как браузер Chrome под Windows. Вывод получился обидный, но полезный: честные роботы представляются честно, а нечестные — нет. Казалось бы очевидно? Но не все так прозрачно как кажется на первый взгляд. Поисковым системам, ИИ-краулерам и утилитам вроде curl репутация нужна, их можно заблокировать по имени, и они это знают. Сканеру уязвимостей на ресурсе репутация не нужна вовсе — он просто подставляет строку настоящего браузера, и любой фильтр по имени агента обходится без проблем.
Попытка вторая: смотреть не кто представился, а что сделал
Не сдаемся и копаем дальше! Я зашёл с другой стороны. Меня же интересует: не «как назвался клиент», а «был ли это человек с браузером»? А браузер отличается от скрипта поведением: получив HTML, он немедленно идёт за таблицей стилей, шрифтами и картинками, иначе ему нечего показать на экране. Сканеру содержимое страницы неинтересно, ему нужен только сам факт ответа, поэтому за оформлением он не возвращается. Отсюда косвенный признак: клиент считается живым, если забрал и страницу, и её оформление.
Проверяется это по логу одним проходом awk — сначала собираем адреса, которые хоть раз запросили стили или картинки, потом оставляем только их строки:
Сбор и проверка адресов
awk ' { lines[NR] = $0; ip[NR] = $1 } $7 == "/styles.css" || $7 ~ /^\/assets\// { browser[$1] = 1 } END { for (i = 1; i <= NR; i++) if (ip[i] in browser) print lines[i] }' access.log > real.log
Результат отрезвил окончательно. Двадцать один живой клиент против двухсот семидесяти трёх «уникальных посетителей» — это ровно 8 % от того, что показывал наивный подсчёт, и в двенадцать раз меньше, чем я увидел в первой сводке. Причём фильтр по имени агента, на который я неспешно потратил вечер, почти ничего не изменил: 168 против 165 — разница в пределах шума. Всю работу сделал один поведенческий признак, который пишется в пять строк.
|
Как считать |
Сколько «посетителей» |
|---|---|
|
Все уникальные адреса |
273 |
|
Открыли главную и получили 200 |
165 |
|
Отсеяны только представившиеся роботы |
168 |
|
Забрали страницу вместе со стилями |
21 |
И это ещё не конец истории. Я прогнал оставшуюся двадцатку через определение типа сети — хотелось понять, откуда адрес: из домашнего интернета или из дата-центра. Выяснилось, что среди этой двадцатки четверо тоже роботы: два обращения от краулера одной ИИ-компании, по одному — от поискового робота и краулера другой ИИ-компании. Они грузят стили и картинки совершенно честно, потому что рендерят страницу целиком, и поведенческий признак их не ловит. Больше половины оставшихся адресов принадлежали хостинг-провайдерам, то есть тоже не людям с домашним интернетом, а чему-то запущенному на арендованной машине. Реальных живых людей за двое суток набралось около полудесятка, включая меня самого.
Самое неприятное открытие: скачивания
Дальше я посмотрел на то, ради чего сайт вообще существует, — на кнопку «Скачать». Установщик весит около 67 МБ и отдаётся с сервера напрямую, так что каждое скачивание видно в логе с точным числом переданных байт. За те же двое суток к файлу было 11 обращений, из которых 4 закончились полной отдачей. Четыре скачивания за два дня для проекта без единого анонса — я бы порадовался, если бы не посмотрел, кто их сделал.
|
Кто скачал файл целиком |
Сколько раз |
|---|---|
|
Краулер одной ИИ-компании |
3 |
|
Я сам, проверяя, что кнопка работает |
1 |
Пользовательских сторонних скачиваний — ноль. Остальные семь обращений оборвались на первых сотнях килобайт: кто-то из облака дёрнул начало файла и отвалился, поисковый робот забрал 278 КБ, видимо, определяя тип содержимого. Отдельно стоит посчитать расход: три полных выкачивания — это 200 МБ трафика, потраченных на то, чтобы бинарник уехал в чей-то датасет. На дешёвом VPS с лимитом это уже заметно, а при раздаче с платного объектного хранилища превратилось бы в счёт.
Отсюда вывод, который я бы вынес отдельно для всех, кто раздаёт файлы: счётчик скачиваний с релизной страницы или из хранилища — не метрика. Он считает всех, кто подключился, и не отличает человека от робота. Если хочется знать реальное число, считать придётся самому: завершённую отдачу файла по объёму переданных байт и только с адреса, который до этого вёл себя как браузер. При таком подходе конечно не будут учитываться живые скачивания, не доведенные до конца — передумал, но думаю, что это не самый худший компромисс.
Что я сделал с этим дальше
Понимание пониманием, но 70 % мусорных запросов никуда не делись: они жгут процессор, забивают логи и мешают смотреть на реальную картину. Городить капчу или ставить перед сайтом чужой антибот-сервис я не хотел — это посредник между читателем и статикой, да и странно бороться со слежкой, добавляя ещё одного наблюдателя. Поэтому обошёлся штатными средствами: четыре меры, от самой мягкой к самой жёсткой, и ни одна из них не мешает поисковой индексации.
Первое — обрывать заведомо чужие запросы
У меня нет и не будет ни PHP, ни WordPress, ни git на сервере, поэтому любой такой запрос заведомо чужой. Отдавать на него честную страницу 404 незачем: это лишняя работа, лишний ответ и лишняя строка в общем логе. Нестандартный код 444 специфичен для nginx и означает «закрыть соединение, ничего не отвечая» — сканер не получает никакой обратной связи, а я получаю отдельный лог с адресами тех, кто туда лез. Приятный бонус для аналитики.
Сбор информации по коду 444
location ~* (^/\.env|^/\.git|\.php$|^/wp-|^/vendor/|^/phpmyadmin) { access_log /var/log/nginx/site.blocked.log; return 444;}
Второе — ограничение частоты
Человек, открывая страницу, делает десяток запросов разом (HTML, стили, иконки, картинки) и затихает, а перебор идёт ровным потоком в сотни запросов. Эта разница ловится штатным модулем: всплеск в 30 запросов покрывает загрузку страницы с запасом, а равномерный поток упирается в лимит и получает 429. Я проверил на себе — пятнадцать запросов подряд к разным файлам прошли без единого отказа.
Ограничение частоты запросов
limit_req_zone $binary_remote_addr zone=main:10m rate=10r/s;limit_req_status 429;# в server{}limit_req zone=main burst=30 nodelay;
Третье — банить упорных
Раз мусорные запросы теперь пишутся в отдельный лог, для fail2ban не нужен хитрый шаблон: подозрительна любая строка в этом файле, надо лишь вытащить из неё адрес. Три попытки за час — и клиент уходит в бан на сутки. Ложные срабатывания тут практически исключены: живой человек в /.env не заходит никогда, а поисковые роботы такие пути не запрашивают, так что индексации это не вредит.
Фильтр и правило для fail2ban
Фильтр /etc/fail2ban/filter.d/scanners.conf:ini[Definition]failregex = ^ -.*"(GET|POST|HEAD|PUT|DELETE|OPTIONS|PATCH).*"ignoreregex =datepattern = ^[^\[]*\[({DATE})Правило /etc/fail2ban/jail.d/site.conf:ini[scanners]enabled = trueport = http,httpsfilter = scannerslogpath = /var/log/nginx/site.blocked.logmaxretry = 3findtime = 1hbantime = 24h
Четвёртое — попросить роботов не трогать тяжёлый файл
Те, кто уважает robots.txt, а поисковые и ИИ-краулеры его уважают, послушаются. Заодно я закрыл целиком несколько сервисов SEO-разведки: они выкачивают сайт ради чужой аналитики и не приносят мне ничего, кроме трафика.
настройка в robots.txt
User-agent: *Disallow: /download
Что в итоге
Через сутки после всех мер логи стали заметно чище, но главное даже не в этом: я перестал обманывать себя цифрами. Сейчас у меня три уровня статистики вместо одной сводки: живые посетители, расширенный отчёт без опознанных роботов и полный трафик со всеми сканерами. Решения принимаются по первому, второй нужен для сравнения, третий — чтобы разбирать инциденты.
Пять вещей, которые я вынес из этой истории
1. Уникальные адреса — это не посетители. На свежем сайте без единой входящей ссылки разница оказалась двенадцатикратной, и весь «трафик» состоял из роботов.
2. Фильтр по User-Agent отсекает только вежливых. Он поймал 12 % запросов и пропустил всех, кто маскируется под браузер, то есть именно тех, из-за кого цифры и раздуваются.
3. Поведение надёжнее самоназвания. Один признак: «забрал страницу вместе со стилями» дал больше, чем список из полусотни известных ботов, и его не нужно дописывать при появлении новых сканеров.
4. Счётчик скачиваний врёт по умолчанию. Пока вы не считаете завершённую отдачу файла с адреса живого посетителя, вы считаете роботов и оборванные соединения.
5. Смотреть надо в свои логи. Все ответы уже лежали у меня на диске — я двое суток доверял агрегированной сводке вместо того, чтобы один раз взглянуть на распределение кодов ответа и другую информацию. Да долго и нудно, но результат повысит общую эффективность и достоверность данных.
Про ИИ-краулеров добавлю отдельно, чтобы это не прозвучало обвинением: они ходили по robots.txt, представлялись честно и ничего не ломали, претензий к ним у меня нет. Просто если вы раздаёте с сайта тяжёлые файлы, будьте готовы, что скачают их не только люди, и решите заранее, устраивает вас это или нет. Меня в итоге устроило чтение страниц, а бинарник я от них закрыл.
Спасибо, если прочитали до конца!
Надеюсь, статья окажется для вас полезной.
ссылка на оригинал статьи https://habr.com/ru/articles/1063364/