Тестовый стенд с боевыми доступами стал точкой входа в прод
Тестовый стенд почти всегда живёт по остаточному принципу: его подняли на скорую руку, чтобы что-то проверить, а потом забыли. Мы разберём конкретный случай конца августа 2026 года, когда именно такой стенд — со скопированными боевыми паролями и без сегментации сети — стал воротами в продакшен. История поучительна тем, что каждое отдельное решение по пути казалось разумным, а вместе они дали дыру, которую нашли только по аномалии в логах базы данных.
Содержание
Что случилось: первый сигнал
Инцидент начался не с алерта на стенде, а с алерта на боевой базе данных. Мониторинг (в связке Zabbix + встроенный pg_stat_statements для PostgreSQL) поднял тревогу по двум метрикам одновременно:
- резкий рост исходящего трафика с сервера БД — за несколько минут отдано в разы больше данных, чем обычно отдаётся за час;
- новое TCP-соединение к порту 5432 с IP-адреса, которого не было в списке привычных источников за последние недели.
Дежурный инженер получил алерт около полуночи по московскому времени. Первая реакция — посмотреть, не идёт ли плановый бэкап или экспорт для аналитиков. Ни то, ни другое в расписании не значилось. На этом этапе стало ясно, что это не рутинная нагрузка, а что-то, требующее разбора здесь и сейчас.
Важная деталь: сама база не «легла» и не показала деградации для пользователей. Именно поэтому классический алерт по доступности сайта не сработал — тревогу подняла только метрика сетевого трафика и, чуть позже, лог подключений PostgreSQL. Если бы мониторинг был настроен только на аптайм сервиса, инцидент заметили бы значительно позже, при следующем плановом аудите.
Что было видно в логах и метриках
Дальше — методичный разбор источников, которые были под рукой.
Лог подключений PostgreSQL (log_connections = on, log_disconnections = on были включены заранее — это отдельно стоит проверить у себя, по умолчанию они часто выключены):
2026-08-24 23:58:12 UTC LOG: connection received: host=10.20.3.47 port=51422
2026-08-24 23:58:12 UTC LOG: connection authorized: user=app_readwrite database=prod_main
2026-08-24 23:58:13 UTC LOG: duration: 0.412 ms statement: SELECT current_setting('server_version')
2026-08-24 23:58:41 UTC LOG: duration: 41822.903 ms statement: SELECT * FROM users
Адрес 10.20.3.47 не входил в подсеть боевых приложений (10.20.1.0/24), а принадлежал подсети 10.20.3.0/24 — там, как выяснилось позже, жил тестовый контур. Пользователь app_readwrite — это боевая учётная запись приложения с правами на чтение и запись в основные таблицы, включая users.
Сетевые метрики на сервере БД (собирались node_exporter + Prometheus) показали всплеск исходящего трафика ровно в момент выполнения того самого SELECT * FROM users — экспорт занял почти 42 секунды и отдал по сети объём, сопоставимый с полной выгрузкой таблицы пользователей.
auth.log на самом сервере БД — здесь было тихо: SSH-логинов не было, вход шёл строго через порт PostgreSQL, а не через шелл. Это сразу сузило круг версий: скомпрометирован не сам сервер БД, а кто-то получил валидные учётные данные приложения и просто подключился напрямую, как обычный клиент.
Security group / firewall-правила боевой базы разрешали входящие подключения на 5432 не с конкретных IP приложения, а с диапазона 10.20.0.0/16 — то есть со всей внутренней сети компании, включая тестовый контур. Это правило было заведено больше года назад «на всякий случай, чтобы не блокировать себе доступ при отладке» и с тех пор никто его не сужал.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверВерсии, которые отбросили
Прежде чем дойти до реальной причины, команда проверила три более вероятные на первый взгляд гипотезы — и на каждую ушло время, но именно последовательное их закрытие дало уверенность в финальном выводе.
Версия 1: кто-то из инженеров запустил тяжёлый отчёт. Проверили список запланированных задач в BI-системе и переписку в рабочем чате — никто не согласовывал и не запускал выгрузку в это время. Спросили напрямую у команды аналитики — ответ отрицательный. Версию закрыли за десять минут.
Версия 2: скомпрометирован VPN или рабочий ноутбук сотрудника. У компании был WireGuard-сервер для удалённого доступа к внутренней сети. Проверили логи wg show и системный журнал WireGuard-сервера — в момент инцидента активных туннелей с необычной активностью не было, а сам адрес 10.20.3.47 не входил в диапазон, который VPN-сервер выдаёт клиентам. Версию отбросили, хотя именно эта проверка заняла больше всего времени: пришлось поднимать историю подключений за сутки и сверять с рабочими графиками сотрудников.
Версия 3: SQL-инъекция через веб-приложение. Проверили логи nginx и логи приложения на боевых серверах на предмет аномальных запросов, XSS/SQLi-паттернов, необычных User-Agent. Ничего похожего на инъекцию не нашли — приложение вообще не инициировало это соединение, запрос пришёл не через API, а прямым TCP-подключением к порту базы. Это стало решающим аргументом: раз соединение установлено напрямую, а не через прикладной слой, значит у кого-то на руках оказались боевые учётные данные и сетевая видимость до порта БД.
Только после того как отпали «внутренний источник», «скомпрометированный VPN» и «инъекция через приложение», расследование сместилось в сторону вопроса: у кого физически был сетевой доступ до 10.20.0.0/16 и откуда взялись боевые credentials на хосте 10.20.3.47.
Как нашли реальную причину
Адрес 10.20.3.47 подняли по внутренней CMDB — это оказался тестовый стенд, поднятый восемь месяцев назад для проверки миграции на новую версию фреймворка. Формально стенд считался «временным» и «внутренним», поэтому не проходил через тот же процесс хардненинга, что боевые серверы, и не значился в регулярном security-скане.
Дальше — цепочка, которую восстановили по истории команд и содержимому файловой системы стенда:
- При создании стенда восемь месяцев назад инженер, чтобы «быстро получить рабочее окружение», выполнил
rsyncконфигурации с боевого сервера, включая файл.envс боевыми паролями к базе данных:
rsync -avz prod-app-01:/srv/app/.env ./stand/.env
- Пароль
app_readwriteс тех пор ни разу не менялся — ни на проде, ни тем более на копии в.envтестового стенда. - Сам стенд смотрел наружу публичным IP и держал открытым Grafana старой версии (обновлённую год назад, но не после того), доступную по умолчанию с логином
admin/admin— эту деталь при разворачивании стенда никто не считал критичной, ведь «это же не прод». - По внешним признакам (массовое сканирование интернета по известным CVE для панелей мониторинга) стенд был скомпрометирован атакующим, который получил шелл-доступ через уязвимость в устаревшей Grafana.
- Оказавшись внутри, атакующий не стал искать ничего изощрённого — файл
.envс боевыми учётными данными лежал в корне проекта, а сетевые правила боевой базы данных пускали весь диапазон10.20.0.0/16, в который входил и скомпрометированный стенд. - Дальше — прямое подключение
psqlк боевой базе и попытка выгрузить таблицу пользователей.
Отдельно стоит подчеркнуть: атакующему не пришлось ничего «взламывать» в криптографическом смысле. Он взломал слабое звено (устаревшую панель мониторинга с дефолтным паролем), а дальше просто воспользовался тем, что на скомпрометированной машине лежали действующие боевые секреты и не было сетевой изоляции между тестовым и боевым контурами. Именно поэтому подобные инциденты обычно называют не «взломом базы данных», а «взломом периметра с горизонтальным перемещением» — база данных сама по себе не имела уязвимостей.
Как стенд получил боевые доступы: корень проблемы
Если вынести за скобки конкретную уязвимость в Grafana (её можно было закрыть обновлением), у инцидента есть более фундаментальная причина — организационная, а не техническая. Тестовые стенды в компании создавались без единого регламента: каждый инженер поднимал окружение так, как ему было удобнее в моменте, и «скопировать .env с прода» было самым быстрым способом получить рабочую конфигурацию для отладки.
Это распространённая практика, и именно поэтому она опасна — она выглядит безобидно на каждом отдельном шаге:
- скопировать
.envбыстрее, чем заводить отдельные учётные данные для тестовой базы; - держать широкое правило firewall (
10.20.0.0/16вместо конкретных IP) удобнее, чем разбираться, какие именно адреса реально нужны; - не обновлять «временный» стенд разумно, если по нему и так «никто не ходит».
Проблема в том, что тестовый стенд с прод-доступами перестаёт быть тестовым с точки зрения модели угроз — по факту это ещё один боевой узел, только с более слабой защитой. Если сравнить, как обычно относятся к безопасности прод-сервера и стенда, разница обычно такая:
| Параметр | Боевой сервер | Тестовый стенд (типичная практика) |
|---|---|---|
| Учётные данные к БД | Отдельные, с минимально необходимыми правами | Скопированы с прода, права «как у приложения» |
| Обновления и патчи | Регулярный цикл | Разворачивается один раз и забывается |
| Сетевой доступ | Ограничен конкретными IP/security group | Часто «вся внутренняя сеть» для удобства отладки |
| Мониторинг и алерты | Полный набор | Часто отсутствует или частичный |
| Публичный IP и открытые панели | Закрыты, доступ через VPN/bastion | Нередко открыты «временно» |
Разница в столбцах и есть та самая асимметрия, которую эксплуатируют в подобных инцидентах: злоумышленнику не нужно ломать хорошо защищённый прод, если рядом стоит его слабо защищённая копия с теми же ключами от двери.
Если вы разворачиваете тестовое окружение с реалистичными данными, у нас есть отдельный разбор, как делать это без утечки боевых доступов и данных — настройка staging-копии сайта: там подробно про анонимизацию дампов и отдельные секреты для контура разработки.
Что изменили после инцидента
Реакция шла в два эшелона: немедленное сдерживание и структурные изменения.
В первые часы:
- Заблокировали IP скомпрометированного стенда на firewall боевой базы данных.
- Сменили пароль
app_readwriteи все секреты, которые могли быть скопированы вместе с.envвосемь месяцев назад (это оказался не только пароль БД, но и ключ для доступа к S3-хранилищу бэкапов — тоже был в том же файле). - Просмотрели
pg_stat_activityи логи за предыдущие недели на предмет более ранних подключений с того же IP — не нашли, что говорит о том, что доступ был получен незадолго до инцидента, а не месяцами раньше. - Полностью пересобрали тестовый стенд с нуля, без переноса конфигурации из скомпрометированного образа.
Структурные изменения на следующей неделе:
Сузили правило firewall для боевой БД с целой подсети до конкретных адресов приложения:
# было
iptables -A INPUT -p tcp --dport 5432 -s 10.20.0.0/16 -j ACCEPT
# стало — только реальные боевые app-серверы
iptables -A INPUT -p tcp --dport 5432 -s 10.20.1.11 -j ACCEPT
iptables -A INPUT -p tcp --dport 5432 -s 10.20.1.12 -j ACCEPT
iptables -A INPUT -p tcp --dport 5432 -j DROP
Вынесли тестовый контур в отдельную сетевую подсеть без маршрута к боевым сервисам — стенды теперь физически не могут достучаться до продовой базы, даже если на них снова окажутся чужие учётные данные.
Ввели правило: для тестовых окружений заводятся отдельные учётные данные с урезанными правами (только на тестовую БД с синтетическими или анонимизированными данными), а не копии боевых. Проверку на «залётные» секреты в конфигах и репозиториях теперь делают регулярно инструментами вроде trufflehog или gitleaks — это отдельная практика, которую стоит завести на постоянной основе, подробнее про неё в материале про ротацию секретов и ключей.
Тестовые стенды включили в тот же цикл обновлений и патчинга, что и боевые серверы — раз в квартал они больше не считаются исключением, а раз в неделю по ним автоматически прогоняется сканер уязвимостей.
Пересмотрели, кто и как получает доступ к внутренним ресурсам вообще: раньше «доступ ко всему внутреннему контуру» выдавался по умолчанию новым инженерам, теперь — только к тому, что реально нужно по роли. Как выстроить такую модель без раздачи root всем подряд, разбирали отдельно в статье как раздать доступ команде без выдачи root.
Наконец, завели базовый чек-лист, который теперь применяют и к тестовым машинам тоже, а не только к боевым — полный список пунктов есть в материале чек-лист безопасности нового сервера: закрытые по умолчанию порты, смена дефолтных паролей у любых панелей, включённые логи подключений, отдельные учётные данные под каждое окружение.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему мониторинг не поймал компрометацию самого стенда раньше?
Потому что на тестовые машины изначально не распространялся тот же набор алертов, что на прод — это и было частью проблемы. После инцидента стенды подключили к общему мониторингу наравне с боевыми серверами.
Достаточно ли просто не копировать .env с прода на стенд?
Это необходимое, но не единственное условие. Даже с отдельными учётными данными открытая для всей внутренней сети база данных остаётся риском при компрометации любого другого узла в той же подсети — поэтому сетевая сегментация важна не меньше, чем отдельные секреты.
Как понять, что широкое правило firewall у вас уже есть?
Просмотрите правила на серверах с базами данных и внутренними сервисами и проверьте, ссылаются ли они на конкретные IP-адреса или на целые подсети класса /16 или /24. Если видите диапазон шире, чем реально необходимые источники — это кандидат на сужение.
Нужно ли вообще давать тестовым стендам выход в интернет и публичный IP?
В большинстве случаев нет. Если стенд нужен только команде для отладки, доступ к нему можно ограничить VPN или bastion-хостом, не выставляя административные панели напрямую наружу.
Как часто нужно ротировать пароли к боевой БД, если ничего подозрительного не произошло?
Единого универсального интервала нет — ориентируйтесь на регламент компании и требования комплаенса, если они есть. Но при любом событии, где боевой секрет мог оказаться скопирован куда-то ещё (включая тестовые стенды, ноутбуки инженеров, CI-раннеры), его стоит считать потенциально скомпрометированным и менять сразу, не дожидаясь планового цикла.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →