Тестовое окружение живёт на боевом сервере: как найти и обезвредить
Заходите на боевой сервер разобраться с нагрузкой на диск — и находите каталог /opt/app-staging, процесс на порту 8081, который никто из команды не узнаёт, и базу app_test рядом с боевой app_prod в том же экземпляре PostgreSQL. Разработчик, который это поднял, полгода как уволился, а стенд всё это время тихо жил на той же машине, что и продакшен, деля с ним CPU, память и диск. Это одна из самых частых находок при аудите унаследованного или давно не пересматриваемого сервера. Разберём, как такое окружение обнаружить, чем оно опасно и как безопасно изолировать или вынести его, не уронив прод в процессе.
Содержание
- Почему тестовое окружение вообще оказывается на боевом сервере
- Где искать: практический чек-лист обнаружения
- Чем реально опасно совместное проживание прода и теста
- Первый шаг: изоляция на месте, если переезд невозможен прямо сейчас
- Как безопасно перенести тестовое окружение на отдельный сервер
- Как не допустить повторения
Почему тестовое окружение вообще оказывается на боевом сервере
Здесь почти никогда нет злого умысла — есть цепочка разумных на вид решений, которые никто не пересмотрел:
- «Быстро проверить на живых данных». Разработчику нужно было отладить баг, который воспроизводится только на реальных объёмах — проще поднять второй инстанс приложения рядом с боевым, чем поднимать отдельный сервер и переносить туда дамп.
- Сервера не хватило в бюджете. На старте проекта отдельный VPS под тесты казался лишней тратой — «места и так много», один сервер тянет и прод, и тест.
- Временное решение, которое забыли снять. Стенд подняли на неделю ради конкретной задачи (проверить миграцию, показать демо инвестору) и не снесли, потому что снятие никто не взял на себя ответственностью.
- Сервер сменил владельца. Проект перешёл по наследству — от фрилансера, от предыдущей команды, от компании при покупке бизнеса. Новый владелец видит только то, что показывает мониторинг прод-сервиса, а тестовый процесс на соседнем порту в мониторинг никогда не попадал.
Итог один: на машине, которая по документации и в голове команды — «прод-сервер», физически живёт вторая, никем не управляемая система. Она не в инвентаре, не в плане бэкапов, не в зоне ответственности дежурного — и именно поэтому опасна.
Где искать: практический чек-лист обнаружения
Прежде чем что-то чинить, нужно honestly понять, что вообще есть на машине. Ниже — последовательность проверок, которая обычно вскрывает подобные находки за 20–30 минут.
Процессы и порты. Смотрим, что реально слушает сеть, а не что написано в документации:
ss -tlnp
# или, если ss недоступен
netstat -tlnp
Любой порт, который не входит в известный список боевых сервисов (обычно это 80/443 для веба, стандартный порт БД, порт метрик) — кандидат на расследование. Дальше смотрим, какой процесс и от чьего имени:
ps aux | grep <PID_из_ss>
lsof -i :8081
Systemd-юниты и cron. Тестовый процесс часто держат живым через systemd или cron, чтобы он переживал перезагрузку:
systemctl list-units --type=service --all | grep -viE "ssh|cron|systemd|network"
for u in $(systemctl list-unit-files --type=service | awk '{print $1}'); do systemctl show -p ExecStart "$u"; done | grep -i test
Cron проверяем не только для текущего пользователя, а для всех:
for user in $(cut -f1 -d: /etc/passwd); do
echo "== $user =="; crontab -u "$user" -l 2>/dev/null
done
Базы данных. Ищем «лишние» базы или схемы рядом с боевой:
# PostgreSQL
psql -U postgres -c "\l"
# MySQL/MariaDB
mysql -e "SHOW DATABASES;"
Название вроде app_test, app_stage, app_dev, app_copy в списке рядом с app_prod — почти наверняка тестовая копия, которую в своё время подключили к тому же серверу БД вместо отдельного инстанса.
Файловая система. Проверяем каталоги с характерными именами и их возраст:
find / -maxdepth 4 -iname "*test*" -o -iname "*stage*" -o -iname "*staging*" 2>/dev/null | grep -v proc
du -sh /opt/* /srv/* /var/www/* 2>/dev/null | sort -rh | head -20
Второй вариант помогает найти каталоги, которые незаметно распухли — частый признак заброшенного тестового окружения, куда годами сваливали логи и дампы без ротации.
Веб-сервер. В конфигурации nginx или Apache ищем server-блоки, которые не относятся к продовым доменам:
grep -rl "server_name" /etc/nginx/sites-enabled/ | xargs grep -l -iE "test|stage|dev"
Если такой блок слушает на 80/443 наравне с боевым — это уже не просто фоновый процесс, а публично доступный сервис, о существовании которого никто не знает.
Итог всех проверок стоит свести в один список: что за процесс, кто его владелец (uid), какие у него сетевые порты, к какой БД он подключается, сколько места занимает и когда последний раз к нему обращались (по atime файлов или логам).
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверЧем реально опасно совместное проживание прода и теста
Проблема не в том, что тест «занимает место» — а в конкретных механизмах взаимного влияния, которые срабатывают в самый неподходящий момент:
- Общий пул ресурсов. Если тестовый процесс не ограничен по CPU и памяти, случайный нагрузочный прогон или зависший тест с утечкой памяти напрямую съедает ресурсы, которые нужны боевому приложению. На сервере без cgroup-лимитов один процесс легко забирает всю доступную память, и OOM killer может выбрать для завершения не тестовый, а боевой процесс — по своей внутренней логике, а не по вашим приоритетам.
- Общая база или общий инстанс СУБД. Даже если базы формально разные (
app_testиapp_prod), они часто сидят в одном процессе PostgreSQL или MySQL — а значит делят connection pool, буферный кеш и I/O к диску. Тяжёлый тестовый запрос без индекса может вытеснить из кеша горячие данные прода или упереться в тот же диск, замедлив боевые запросы без видимой причины на стороне прода. - Общая сеть и общий firewall-периметр. Если тестовый и боевой сервисы висят на одном хосте без сетевой изоляции, скомпрометированный тестовый стенд (обычно защищённый слабее — старые пароли, debug-режим, отсутствие WAF) становится трамплином к боевым данным. Подробнее о том, как это реализуется на практике, разобрано в статье про тестовый стенд с боевыми доступами, ставший точкой входа.
- Копия боевых данных в тестовом контуре. Если тестовая база — это дамп прода без анонимизации, вы получаете полноценные персональные и платёжные данные на менее защищённой площадке. Это отдельный риск, который стоит разбирать прицельно — см. материал про тестовый сервер с копией боевой базы.
- Непредсказуемые окна обслуживания. Перезапуск тестового сервиса, ротация его логов, ночной cron с бэкапом тестовой БД — всё это конкурирует за диск и I/O с боевыми процессами именно тогда, когда никто не следит за метриками, потому что «это же просто тест».
Пока такое окружение не в инвентаре и не в мониторинге, у него нет владельца в моменте инцидента: дежурный тратит первые критичные минуты не на устранение проблемы, а на выяснение, что это вообще за процесс.
Первый шаг: изоляция на месте, если переезд невозможен прямо сейчас
Идеальный результат — отдельный сервер под тест. Но если перенести всё сразу нельзя (нет бюджета, нет времени, нужно сначала стабилизировать ситуацию), можно и нужно снизить риск на месте, не дожидаясь миграции.
Отдельная системная учётка. Первый шаг — гарантировать, что тестовый процесс не может физически трогать файлы боевого приложения:
useradd -r -s /usr/sbin/nologin -d /opt/app-staging testenv
chown -R testenv:testenv /opt/app-staging
Дальше проверяем права на всё, к чему тестовый пользователь не должен иметь доступа — в первую очередь на каталоги боевого приложения и на файлы с секретами прода.
Контейнеризация вместо процесса на голом хосте. Даже без переноса на отдельный сервер, перевод тестового окружения в контейнер сразу даёт изоляцию файловой системы, процессов и (частично) сети:
docker run -d --name app-test \
--memory=1g --cpus=1 \
--network test-net \
-p 127.0.0.1:8081:8080 \
myapp:test
Обратите внимание на -p 127.0.0.1:8081:8080 — порт публикуется только на loopback, а не на всех интерфейсах, так что тестовый сервис перестаёт быть доступен снаружи, если это не нужно намеренно.
Лимиты ресурсов через cgroups. Если контейнеризация пока невозможна (процесс завязан на специфичное окружение хоста), ограничить его можно и через systemd-слайс:
mkdir -p /etc/systemd/system/app-test.service.d
cat > /etc/systemd/system/app-test.service.d/limits.conf <<'EOF'
[Service]
MemoryMax=1G
CPUQuota=50%
IOWeight=50
EOF
systemctl daemon-reload
systemctl restart app-test
Это не полная изоляция, но гарантированный потолок: даже при утечке памяти или зависшем цикле тестовый процесс не сможет забрать ресурсы у боевого сверх заданного лимита.
Отдельная СУБД-учётка с урезанными правами. Если база пока общая физически, минимум — развести пользователей и права:
-- PostgreSQL
CREATE USER test_owner WITH PASSWORD '...';
GRANT ALL PRIVILEGES ON DATABASE app_test TO test_owner;
REVOKE ALL ON DATABASE app_prod FROM test_owner;
Проверьте это явно, а не полагайтесь на то, что «раз пользователь для теста, значит и права только на тест» — по умолчанию новый пользователь в PostgreSQL получает доступ на подключение ко всем базам, если это не ограничено отдельно.
Файрвол между сегментами. Даже на одной машине можно логически развести трафик через iptables/nftables, запретив тестовому процессу инициировать соединения к портам боевой БД и наоборот:
iptables -A OUTPUT -m owner --uid-owner testenv -d 10.0.0.5 -p tcp --dport 5432 -j DROP
Это грубее, чем полноценная сетевая сегментация, но закрывает самый частый сценарий бокового перемещения — тестовый процесс, который «на всякий случай» имеет доступ к боевой базе.
Как безопасно перенести тестовое окружение на отдельный сервер
Локальная изоляция снижает риск, но не убирает главную причину — общий физический ресурс. Полноценное решение — отдельная машина. Перенос стоит делать по шагам, не выключая старое окружение до полной проверки нового.
- Инвентаризация зависимостей. Прежде чем переносить, зафиксируйте всё, от чего зависит тестовое приложение: версии рантайма, переменные окружения, внешние интеграции (даже тестовые — почтовый sandbox, платёжный sandbox), объём данных в БД.
- Подготовка нового сервера. Разворачиваете чистое окружение — тот же стек, что и на старой площадке, но с самого начала на отдельной машине, с собственным firewall-периметром и без общих сетевых сегментов с продом.
- Перенос данных без прямого доступа к проду. Тестовую БД переносите дампом, а не прямым репликационным подключением к боевому серверу:
pg_dump -U test_owner -Fc app_test > app_test.dump
scp app_test.dump new-test-server:/tmp/
ssh new-test-server "pg_restore -U test_owner -d app_test /tmp/app_test.dump"
- Переключение DNS и внутренних адресов. Если на тестовое окружение ссылались по внутреннему хостнейму или IP старого сервера, обновите записи заранее и держите какое-то время оба адреса рабочими, чтобы не сломать интеграции, о которых забыли.
- Контрольный период. Не выключайте тестовый процесс на старом сервере сразу — оставьте его в режиме «только чтение» или полностью остановленным, но не удалённым, на 1–2 недели, пока не убедитесь, что все, кто им пользовался, переключились на новый адрес.
- Финальная зачистка боевого сервера. Только после подтверждённого периода без обращений — удаляете каталоги, останавливаете и удаляете systemd-юниты, отзываете доступ тестовой учётки к БД прода, если он там был, и убираете тестовые server-блоки из конфигурации веб-сервера.
Отдельный практический момент: если у команды есть похожая, но обратная потребность — не «убрать чужой тест с прода», а «поднять себе несколько тестовых окружений с нуля на своём сервере» — этот сценарий подробно разобран в статье про тестировщика с пятью окружениями на своём сервере. Это хороший ориентир, каким должен быть тестовый контур, когда его строят осознанно, а не находят постфактум.
Как не допустить повторения
Найти и вынести один такой стенд недостаточно — без изменения процесса на его месте через год-два появится следующий. Несколько практик, которые снижают вероятность рецидива:
- Регулярный аудит листенеров. Раз в квартал прогоняйте
ss -tlnpна всех боевых серверах и сверяйте список портов с эталонным — любое расхождение разбирается сразу, а не когда кто-то случайно наткнётся. - Инвентарь серверов с назначением каждой машины. Явный список «эта машина только под прод, тестам сюда путь закрыт организационно» снимает саму возможность «поставить временно, потом разберёмся».
- Регламент жизненного цикла тестовых стендов. Если у вас уже есть выделенный тестовый контур, стоит формализовать, как он поддерживается в актуальном состоянии — это отдельная тема, разобранная в материале про регламент тестового окружения и стейджа.
- Тег
ownerиexpiresна любом временном ресурсе. Если процесс или контейнер поднимается «на неделю», зафиксируйте это явно — в метаданных, в тикете, в системе мониторинга с датой автоматического напоминания. «Временное без даты» имеет свойство становиться постоянным. - Алерты на новые listen-порты. Простое правило в мониторинге — оповещать при появлении нового TCP-порта в LISTEN на боевом сервере — ловит подобные ситуации на этапе появления, а не через полгода при случайном аудите.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли просто удалить найденный тестовый процесс, не разбираясь?
Не сразу. Сначала проверьте, не завязаны ли на него внешние интеграции или веб-хуки — например, платёжная система в тестовом режиме, к которой всё ещё стучится сторонний сервис. Резервную копию данных и конфигурации снимите перед любыми действиями, даже если планируете снести окружение полностью.
Что делать, если тестовая и боевая база — буквально одна и та же СУБД без возможности быстро развести?
Начните с прав доступа и лимитов на уровне пользователя БД (см. раздел про изоляцию на месте), это не требует простоя. Полное физическое разделение — на отдельный инстанс или отдельный сервер — планируйте отдельным окном с уведомлением команды, потому что миграция дампа занимает время, пропорциональное объёму данных.
Как быстро понять, использует ли кто-то ещё найденный тестовый сервис, прежде чем его выключать?
Посмотрите логи доступа (nginx access log, логи приложения) за последние недели — если обращений нет, риск ниже. Дополнительно можно временно перевести сервис в режим, который явно логирует и сразу возвращает ошибку с пояснением, а не тихо продолжает работать — это выявит забытых потребителей за пару дней вместо гадания по логам.
Стоит ли переносить тестовое окружение на тот же провайдер, где сейчас прод, или обязательно на другого?
Технически можно на того же — важна не смена провайдера, а физическое или как минимум виртуальное разделение ресурсов (отдельный VPS вместо процесса на хосте прода) и отдельный сетевой периметр. Разные провайдеры добавляют устойчивость на случай проблем у одного из них, но это отдельное решение, не обязательное условие безопасной изоляции.
Что если после переноса выяснилось, что тестовое окружение вообще никому не нужно?
Такое случается чаще, чем кажется — стенд держали «на всякий случай» годами. Если за контрольный период (см. шаг 5 миграции) не нашлось ни одного реального обращения, безопаснее полностью остановить и архивировать данные, а не оставлять работающим «раз уж перенесли».
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →