Трендовые github проекты в нашем телеграм канале. Подпишись → Открытая база в Compose: цепочка атаки и чеклист на каждый деплой
Небольшой VPS, на нём несколько пет-проектов. Один из них — Telegram-бот для тренировки устного счёта, код которого целиком написал Claude Code. Другой проект на том же сервере следит за нагрузкой и шлёт уведомление, когда машина уходит в перегрузку.
Первое уведомление легко списать на слабое железо и количество сервисов. Но они приходят второй день, третий. Нагруженные процессы убиваются, сервисы перезапускаются, нагрузка падает и через какое-то время снова ползёт вверх. На третий день находится причина — строка в docker-compose.yml бота:
services:
db:
image: postgres
ports:
- "5432:5432"
Боту этот порт на хосте был не нужен: приложение ходит в базу по внутренней сети Compose. При проверке всё выглядело нормально — бот запускается и отвечает. Кому ещё доступна база, никто не проверил.
Как выглядит атака
Полных логов, сетевого дампа и снимка диска не сохранилось, поэтому цепочку можно восстановить только по косвенным признакам. Она типовая и повторяется на тысячах серверов:
- Сканер перебирает адреса в интернете и проверяет открытые порты.
- На 5432 отвечает PostgreSQL — по ответу сервиса сканер понимает, что это база и она доступна извне.
- Атакующий пытается войти. Пароль мог быть подобран, найден в утёкшей конфигурации или получен иначе — в этой истории он оказался рабочим.
- Официальный образ
postgresсоздаёт пользователя изPOSTGRES_USERс правами суперпользователя. Значит, данные бота можно читать и менять. - Суперпользователь PostgreSQL может выполнять команды на сервере через
COPY ... PROGRAM. Это штатная административная функция, уязвимостью она не считается. - Через неё внутри контейнера скачивается и запускается майнер, который съедает CPU.
Полноценное расследование в такой ситуации мало что даёт: глубина проникновения неизвестна, доверять состоянию сервера нельзя. Переустановка практичнее, чем бесконечно убивать процессы в надежде, что больше ничего не осталось.
В этой цепочке три независимых слабых места: порт опубликован на хосте, пароль скомпрометирован, приложение работает под суперпользователем. Закрытие любого из них обрывает атаку на своём шаге, поэтому чинить стоит все три.
Сначала схема, потом код
Ошибку в сетевых границах проще поймать до генерации кода. Подробный документ для этого не нужен, хватит ответов на четыре вопроса:
- Основной путь запроса. Нарисуйте компоненты и стрелки между ними, подпишите, кто инициирует соединение и какие данные передаются.
- Входы. Какие интерфейсы доступны из интернета, какие только администраторам, какие остаются внутренними. Для каждого внешнего — назначение, источник соединения и способ проверки доступа.
- Права компонентов. Разделите обычную работу и администрирование. Сервис получает только те разрешения, без которых он не выполняет задачу.
- Сбои. Что происходит при тайм-ауте внешней зависимости или потере данных, где лежат логи, как проверяется восстановление из бэкапа.
После генерации кода конфигурацию, права и фактические соединения сверяют с этой схемой. Ревью тогда отвечает на конкретный вопрос: совпадает ли построенное с задуманным.
Сеть: база остаётся внутри
Firewall решает, откуда и на какой порт вообще можно подключиться. Аутентификация проверяет, кто вошёл. Права сервиса определяют, что этому клиенту доступно. Слои друг друга не заменяют, и сложный пароль не оправдывает публикацию внутренней службы в интернет.
Базовая таблица для типичного сервера:
| Сервис | Кто подключается | Что разрешать снаружи |
|---|---|---|
| Сайт или API | Посетители | 443/TCP; 80/TCP только для HTTP или редиректа на HTTPS |
| SSH, админ-панели | Администратор | Нужный порт и только с доверенного IP |
| БД, кэш, очередь | Приложение во внутренней сети | Ничего, порт на хосте не публикуется |
То же правило касается Redis, Docker API и внутренних панелей: сначала определяете клиента, потом разрешаете соединение.
Несколько деталей облачных firewall, на которых легко ошибиться:
- «Все адреса» обычно означает
0.0.0.0/0только для IPv4. Для IPv6 нужно отдельное правило с::/0, и про него часто забывают. - Пустая группа правил у некоторых провайдеров не фильтрует ничего. Считать её запретом на весь трафик нельзя.
- Если сервер состоит в нескольких группах, разрешения складываются. Широкое правило в старой группе продолжит работать.
- Правила для внешнего трафика обычно не действуют на приватные сети между серверами, их нужно контролировать отдельно.
- Первое исходящее правило включает фильтрацию исходящих соединений. Без отдельной необходимости блок исходящего трафика лучше не трогать.
- Перед сохранением перепроверьте IP и порт SSH, иначе можно закрыть доступ себе самому.
Проверять результат нужно из другой сети, например с домашней машины:
nc -vz <SERVER_IP> 443
nc -vz <SERVER_IP> 5432
В PowerShell то же самое делает Test-NetConnection <SERVER_IP> -Port 5432. Для работающего сайта 443 должен открываться, 5432 — нет. Проверка с самого сервера ничего не доказывает.
Как ставить задачу агенту
Запрос «добавь PostgreSQL в Compose, чтобы бот запускался» задаёт результат и ничего не говорит о сетевых границах и правах. Агент заполнит пробелы сам и добьётся запуска, в том числе ценой лишнего открытого порта.
Для изменений инфраструктуры лучше работает двухшаговая схема: сначала агент изучает текущую конфигурацию и предлагает план, файлы меняются только после подтверждения. В задаче фиксируются цель, ограничения, критерии готовности и условия остановки. Пример требований для того же бота:
- приложение подключается к PostgreSQL по имени сервиса во внутренней сети Compose;
- порт 5432 на хосте не публикуется;
- для обычной работы приложения роль суперпользователя не используется; нужные права определяются по коду и миграциям, при нехватке данных агент задаёт вопросы;
- данные, тома и реальные секреты не трогаются, деплой не выполняется;
- если понадобился внешний порт, пересоздание тома или более широкие права — агент останавливается и объясняет причину;
- в отчёте: diff, список опубликованных портов, выполненные проверки и то, что проверить не удалось;
- полный вывод
docker compose configне печатается, в нём могут оказаться секреты; - утверждать, что база недоступна из интернета, по одной локальной проверке нельзя.
Такой запрос безопасность не гарантирует. Он задаёт критерии, по которым результат можно проверить. Diff всё равно смотрит человек.
Правила, которые повторяются из задачи в задачу, переезжают в инструкции проекта (CLAUDE.md, AGENTS.md и аналоги). Там описывается архитектура конкретного репозитория и ограничения: не выводить значения секретов, перед удалением данных, деплоем или изменением боевой среды запрашивать подтверждение. Общие пожелания вроде «пиши качественно» там бесполезны.
Текстовые правила при этом не меняют технических прав агента. Если у него есть доступ к production, инструкция его не отнимет — ограничения нужны на уровне среды.
Аудит перед публикацией
Повторяющиеся проверки удобно оформить в skill: SKILL.md с описанием задачи, что проверить и в каком виде вернуть результат, плюс скрипты рядом. Для аудита перед деплоем разумно разделить два направления:
- Утечки. Секреты, ключи, токены, персональные данные. Перед открытием репозитория проверяется и история Git: удалённый ключ живёт в старом коммите. Найденное маскируется в отчёте.
- Код и инфраструктура. Обработка ввода, авторизация, опасные вызовы, десериализация, настройки контейнеров, зависимости, CI.
Итог аудита лучше делать однозначным: FAILED при подтверждённых Critical или High, PASSED WITH WARNINGS при находках средней серьёзности, INCOMPLETE, если обязательные проверки не завершились. Исправлять находки должен человек, после чего проверка запускается повторно.
Строка ports: - "5432:5432" попала в этот бот потому, что критерием готовности был «бот отвечает». Добавьте в этот критерий «снаружи открыт только 443» и проверяйте его командой nc из другой сети после каждого деплоя.