Две недели назад я закончил домашний проект — небольшую утилиту под 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 = true port = http,https filter = scanners logpath = /var/log/nginx/site.blocked.log maxretry = 3 findtime = 1h bantime = 24h
Четвёртое — попросить роботов не трогать тяжёлый файл
Те, кто уважает robots.txt, а поисковые и ИИ-краулеры его уважают, послушаются. Заодно я закрыл целиком несколько сервисов SEO-разведки: они выкачивают сайт ради чужой аналитики и не приносят мне ничего, кроме трафика.
настройка в robots.txt
User-agent: * Disallow: /download
Что в итоге
Через сутки после всех мер логи стали заметно чище, но главное даже не в этом: я перестал обманывать себя цифрами. Сейчас у меня три уровня статистики вместо одной сводки: живые посетители, расширенный отчёт без опознанных роботов и полный трафик со всеми сканерами. Решения принимаются по первому, второй нужен для сравнения, третий — чтобы разбирать инциденты.
Пять вещей, которые я вынес из этой истории
1. Уникальные адреса — это не посетители. На свежем сайте без единой входящей ссылки разница оказалась двенадцатикратной, и весь «трафик» состоял из роботов.
2. Фильтр по User-Agent отсекает только вежливых. Он поймал 12 % запросов и пропустил всех, кто маскируется под браузер, то есть именно тех, из-за кого цифры и раздуваются.
3. Поведение надёжнее самоназвания. Один признак: «забрал страницу вместе со стилями» дал больше, чем список из полусотни известных ботов, и его не нужно дописывать при появлении новых сканеров.
4. Счётчик скачиваний врёт по умолчанию. Пока вы не считаете завершённую отдачу файла с адреса живого посетителя, вы считаете роботов и оборванные соединения.
5. Смотреть надо в свои логи. Все ответы уже лежали у меня на диске — я двое суток доверял агрегированной сводке вместо того, чтобы один раз взглянуть на распределение кодов ответа и другую информацию. Да долго и нудно, но результат повысит общую эффективность и достоверность данных.
Про ИИ-краулеров добавлю отдельно, чтобы это не прозвучало обвинением: они ходили по robots.txt, представлялись честно и ничего не ломали, претензий к ним у меня нет. Просто если вы раздаёте с сайта тяжёлые файлы, будьте готовы, что скачают их не только люди, и решите заранее, устраивает вас это или нет. Меня в итоге устроило чтение страниц, а бинарник я от них закрыл.
Спасибо, если прочитали до конца!
Надеюсь, статья окажется для вас полезной.
Комментарии (3)

xenon
27.07.2026 08:56Я давно уже думаю о простом WAF по аналогии с Fail2Ban: смотрим логи и если видим нетипичное поведение (много ошибочных запросов) - баним IP.
Это не спасет от 0-day уязвимости, если одной командой можно получить какой-то доступ. Но это спасет от всех сканеров и попьет очень много крови у настоящего хакера, который неизбежно будет исследовать приложение множеством необычных запросов. Достаточно просто читать лог!
Можно даже (довольно легко) отлавливать в логе успешную аутентификацию и таких пользователей обрабатывать уже как-то иначе. (например, для них выше порог доверия).
Сам Fail2Ban тут не совсем удобен - он вот как раз не умеет понимать, что юзер успешно залогинен (то есть, скорее всего это именно наш юзер, а не просто мамкин хакер из Индонезии), а любые лимиты fail2ban можно "обойти" сканируя сервер слишком медленно, чтобы оказаться ниже радаров.

pae174
27.07.2026 08:56С точки зрения нагрузок на сервер несколько тысяч посторонних запросов в сутки к несуществующим страницам статического сайта - это ни о чем. Не стоит беспокоиться вообще.
С точки зрения безопасности в момент выкладки сайта на сервер всегда имет смысл убедиться в том, что вы не выложили туда чего-нибудь лишнего, например какого-нибудь файлика с какими-нибудь ключами.
Rive
Притом, такие ботнеты с поиском уязвимых страниц по списку существовали ещё лет десять-пятнадцать назад, когда я работал в техподдержке хостера и клиенты регулярно звонили с жалобами на вскрытый вордпресс или джумлу.
С тех пор к этому зоопарку добавились только скрапперы LLM.