Долго я думал, что мне это не нужно. Маленький личный сайт, никаких платежей, никаких пользовательских данных — кому он интересен? Безопасность казалась задачей для больших компаний с чужими деньгами на кону. Потом я посмотрел в логи и увидел, что интересен он вовсе не людям.
Один час из жизни сайта
Я включил мониторинг посещений, который раз в час склеивает всех новых гостей: с какого адреса пришли, сколько запросов, какие страницы. Сначала это выглядело как обычная статистика, пока я не отсортировал по числу запросов.
Первая строка: адрес 94.154.46.250. Как посетитель он зашёл в 15:43 и ушёл в 15:44 — одна минута. За эту минуту он сделал 2100 запросов и открыл 1042 разные страницы. И это были не статьи блога.
Выглядело это так: /.env, /.env.production, /.env.backup, /.env~, /.git/config, /.git/HEAD, /.ssh/id_rsa, /.ssh/authorized_keys, /.aws/credentials, /.npmrc, docker-compose.yml, wp-config.php. Полный каталог того, где люди обычно оставляют секреты: пароли от сервисов, приватные ключи, креды облака, доступ к базе.
Отдельно стоит про представление. Этот гость подписался как Googlebot — «я, дескать, поисковый бот Google». Поисковый бот Google не запрашивает /.ssh/id_rsa тысячу раз за минуту. Это маскировка в расчёте на то, что владелец решит «свой» и пройдёт мимо.
Вторая строка: 85.121.126.51. Этот зашёл аккуратнее — работал девять минут и сделал 598 запросов. Он искал одно: файл базы данных. Перебрал db.sqlitedb, bak.sqlitedb, database.sqlite, dump.sql, backup.sql, users.sql — сотни имён, под которыми мог бы лежать дамп, чтобы просто скачать его целиком.
Это был поиск уязвимостей
То, что я наблюдал, — классическое автоматическое сканирование. Программа не «взламывала» меня, она проверяла по списку: нет ли где-нибудь забытого файла с секретами. Список этот не случайный — это выжимка из реальных утечек, которые случались у тысяч других проектов. Если файл находят — его скачивают, и дальше начинается уже настоящий взлом.
За те сутки в логе нашлось ещё несколько таких «искателей». Кое-кто с пустым представлением перебирал сотни чужих адресов вида /wp-content/..., /adminer.php, /debug/pprof — это поиск по чужим известным дырам, на случай если у меня стоит не тот движок (а у меня его нет). А один из адресов искал вообще прицельно: /.env.aws, /.env.anthropic — то есть облачные ключи и ключи нейросетевых сервисов. Сканеры уже понимают, что у разработчиков бывает.
Итог за одни сутки: больше десяти тысяч запросов только в логе новых адресов, и большая их часть — по путям, которых на сайте нет. Это не атака на меня конкретно. Это фон, на который попадает любой адрес в интернете.
Хорошая новость: ничего не утекло
Первое, что я сделал, — проверил не «пришли ли», а не ушло ли что-нибудь. Разобрал весь лог запросов и посмотрел: дал ли сервер хотя бы раз успешный ответ на чувствительный путь?
Ответ — нет. На все обращения к .env, .git, ключам и дампам сервер отвечал «не найдено». Ни одного секрета сканеры не получили. Ни паролей, ни ключей, ни базы.
Это важно проговорить, потому что паника в такой ситуации — плохой советчик. Сканеры стучатся в дверь постоянно; это признак того, что вы в интернете, а не того, что вы уже взломаны. Настоящий вопрос другой: заперта ли дверь и видно ли вам, что в неё стучат.
А вот что было плохо
Спокойствие за секреты не отменяло других проблем, и некоторые из них серьёзнее, чем кажется.
Не было ограничения на поток. Тот гость с 2100 запросами за минуту прошёл беспрепятственно: ничто не мешало ему устроить такую нагрузку. Для сервера на два гигабайта памяти это реальный способ уронить сайт — даже без всякой попытки взлома, просто потоком.
Приборные панели были открыты наружу. Дашборды мониторинга висели в интернете без пароля. Они не выдают секретов, но показывают внутреннее устройство системы: сколько у меня сервисов, как устроена инфраструктура. Готовая карта для того, кто ищет слабое место.
Служебная поверхность была публичной. Схема всех эндпоинтов, документация API — всё доступно любому. Удобно в разработке, но на боевом сервере это подсказка.
Служебные эндпоинты не проверяли, кто их зовёт. Через пару из них можно было отправить произвольное уведомление мне на телефон. Мелочь, но неприятная: чужие сообщения в канале, которому я доверяю — как раз то, чем можно маскировать настоящий инцидент.
Что я сделал
Я разложил защиту по слоям и закрывал по одному.
На входе — ограничение и отсечение. Появился предел на число запросов с одного адреса. Обычный читатель его не замечает, а сканер упирается в стену — ровно тот сценарий с 2100 запросами теперь не проходит. Чувствительные пути отвечают «не найдено», и, что важно, одинаково «не найдено» на всё: по ответу нельзя понять, есть файл или нет. Классические адреса чужих движков, которых у меня нет, отсекаются сразу.
Аутентификация там, где она нужна. Панели мониторинга закрыты паролем. Документация API выключена на проде. Служебные эндпоинты уведомлений проверяют общий секрет и без него молчат — банальная проверка, но она убирает целый класс злоупотреблений.
Заголовки безопасности. Браузеру теперь явно сказано, что можно, а что нельзя на этом сайте: откуда грузить скрипты, можно ли встраивать страницу в чужую. Это защищает уже не меня, а читателя.
Периметр сервера. Отключены ненужные возможности SSH, вход по паролю выключен в пользу ключа. Добавлен автобан адресов, которые перебирают пароли или устраивают поток запросов. И добавлен резервный своп — чтобы внезапный пик по памяти не убил приложение, а мягко ушёл в запас.
Гигиена. Версии контейнеров зафиксированы, а не «последняя всегда». Секреты больше не имеют значения по умолчанию: сервис не запустится, если реальный секрет не задан. Это защита от сценария «забыли указать — а оно тихо работает с известным всем паролем».
Как я теперь вижу, что происходит
Самое полезное изменение — не замки, а зрение. Тот же мониторинг, который показал мне сканеров, я переделал так, чтобы он различал читателя и сканера. Теперь новый адрес оценивается по поведению: если он лезет по чувствительным путям или устраивает поток запросов — мне приходит отдельное срочное уведомление, а не строчка в общей сводке. Обычные новые читатели приходят, как и раньше, спокойно.
Плюс есть отдельный сигнал на случай, если сам монитор замолчит. Молчание тоже нужно уметь замечать — иначе легко принять «тишину» за «всё хорошо».
Именно с логов всё и началось. Пока я в них не смотрел, я думал, что меня никто не трогает.
Что понял
Безопасность маленького сайта — это не про то, чтобы спрятаться. Спрятаться не выйдет: сканеры найдут вас раньше, чем вы успеете решить, что никому не интересны. Это про предсказуемость: заперты ли двери, не отдаётся ли лишнее, упирается ли поток, и главное — видно ли вам, что происходит.
Раньше мне казалось, что это большая работа, которую делают один раз. На деле она не заканчивается: меняются попытки, меняются инструменты, находится что-то ещё. Но всё сразу делать и не нужно. Достаточно закрыть очевидное, включить зрение и возвращаться к этому регулярно — как к уборке, а не как к ремонту.
Самый ценный урок оказался не про замки. Он про видимость: пока я не стал смотреть в логи, всё казалось спокойным. Теперь я каждый день вижу, что это не так, — и именно от этого сплю спокойнее.
Куда смотреть дальше
- Разбор на настоящем стенде. Проверить защиту целенаправленно, а не наблюдать фон.
- Учётки вместо общего пароля. Сейчас вход в админку — один общий пароль; правильнее — отдельные учётные записи и, где возможно, вход через внешнего провайдера.
- Меньше привилегий у процессов. Часть контейнеров работает с избыточными правами — это следующая цель.
- Шифрование данных на диске. Если на сервере появятся чувствительные данные, а не только статьи.
- Регулярный пересмотр. Зафиксировать привычку время от времени проходить по чек-листу заново.
Загрузка…