MAATRIX / Блог / Скопировали конфиг с тестового и получили режим отладки на проде

Скопировали конфиг с тестового и получили режим отладки на проде

MAATRIX

Классический сценарий: нужно было быстро докинуть одну переменную в прод-конфиг, и вместо аккуратной правки кто-то скопировал целиком .env со стенда. Через несколько минут сайт начал тормозить, а потом пользователи стали видеть куски питоновского трейсбека вместо страницы ошибки. Разбираем по шагам, как это выглядело со стороны мониторинга, какие версии отбрасывали одну за другой и почему включённый по ошибке debug-режим — это не про «немного менее аккуратные логи», а про реальный риск утечки данных. Числа ниже — иллюстративные, у вас они будут другими, суть не в них, а в логике поиска причины.

Что сломалось: первые симптомы

Прод — обычная связка на VPS: Django-приложение под gunicorn, перед ним nginx, PostgreSQL и Redis на соседнем сервере, деплой через systemd-юнит с EnvironmentFile=/opt/app/.env. Второй такой же стек поднят на тестовом сервере — с теми же именами сервисов, но с другими значениями переменных.

Релиз был плановый: нужно было добавить в прод переменную для нового платёжного интеграционного ключа. Инженер, который делал релиз, торопился и вместо nano /opt/app/.env на проде решил «синхронизировать» файл со стенда, где переменная уже стояла:

scp stage-server:/opt/app/.env prod-server:/opt/app/.env
sudo systemctl restart myapp.service

Файл .env на стенде отличался от прод-версии не одной строкой, а десятком — там годами копились удобства для локальной отладки: DEBUG=True, ALLOWED_HOSTS=*, EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend, включённый django-debug-toolbar, эхо всех SQL-запросов в лог. Именно эти строки и прилетели на прод вместе с нужным ключом.

Первые жалобы пришли от поддержки: главная страница и карточки товаров стали заметно медленнее открываться. Через 10–15 минут в мониторинге пополз вверх график памяти на воркерах gunicorn — не скачком, а ровным «пилообразным» ростом: память растёт, потом резкий сброс. Чуть позже в саппорт прилетел скриншот от пользователя — вместо страницы 500 он увидел полноценную страницу с питоновским traceback, именами файлов на сервере и локальными переменными функции, где эта ошибка случилась.

Что показали логи и метрики

Дальше — обычная работа дежурного: смотреть на графики и логи, а не гадать.

Nginx access-лог. Количество запросов в минуту было в норме, никакого всплеска трафика. Зато время ответа апстрима ($upstream_response_time) заметно выросло, и появились редкие 502 — с интервалом, похожим на перезапуски воркеров.

Метрики хоста. График RSS-памяти процессов gunicorn рос почти линейно после каждого перезапуска и упирался в лимит cgroup, после чего OOM killer гасил воркер, systemd поднимал новый — отсюда и «пила» на графике, и точечные 502 в момент рестарта.

Лог PostgreSQL. В pg_stat_statements и slow query log начали появляться повторяющиеся однотипные SELECT-запросы — заметно чаще, чем обычно на то же количество открытых страниц. На первый взгляд похоже на N+1-проблему из свежего кода.

Redis. redis-cli INFO stats показывал упавший hit ratio по кешу — почти до нуля. Начали подозревать проблему на стороне Redis: не истекает TTL, не работает eviction, что-то с сетью до кеш-сервера.

Sentry. Количество событий выросло в разы, но что важнее — в карточках ошибок появились расширенные данные: значения локальных переменных, полные пути к файлам на сервере, куски SQL с параметрами подключения. Раньше такого не было.

Набор симптомов указывал в разные стороны сразу: рост памяти, странные повторяющиеся запросы к базе, обнулившийся кеш, разросшиеся трейсы в Sentry. Отсюда и пошли гипотезы.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Гипотезы, которые отбросили

Разбирали по порядку, от вероятного к экзотическому.

  1. DDoS или всплеск легитимного трафика. Проверили nginx access.log через goaccess и count запросов по IP — распределение обычное, никакой аномалии по количеству и географии запросов. Отбросили в первые же минуты.
  1. Регресс в последнем релизе кода. Слишком похоже на совпадение по времени с деплоем. Откатили приложение на предыдущий тег (git revert + передеплой), перезапустили — симптомы никуда не делись. Значит, дело не в самом коде приложения, который catтили этим релизом.
  1. Ночной бэкап или cron-задача забили диск/CPU. Проверили iostat -x 1 и список активных cron-джобов — инцидент случился в середине дня, бэкапы отработали ночью и давно закончились. Не подходит.
  1. Проблема на стороне Redis. Раз hit ratio упал почти до нуля, первая мысль — сломался кеш-сервер. Зашли на Redis, PING отвечает, INFO memory в порядке, сеть до него нормальная. Кеш физически жив — просто приложение перестало им пользоваться. Это уже было ближе к правде, но пока непонятно, почему.
  1. Утечка памяти из-за новой библиотеки. Сверили pip freeze на проде с состоянием на момент прошлого стабильного релиза — различий не было, зависимости не менялись уже неделю. Тоже мимо.

К этому моменту первые полтора часа ушли на проверку версий, которые обычно и оказываются причиной — трафик, код, инфраструктура, зависимости. Ни одна не подтвердилась, а тем временем один из разработчиков, зашедший на прод посмотреть страницу заказа, написал в чат: «а почему тут выехала панель django-debug-toolbar сбоку?» Именно эта реплика и стала поворотной точкой.

Как нашли настоящую причину

Панель debug-toolbar в углу экрана — это фича, которая физически не может появиться, если DEBUG=False: она регистрируется в приложении только при включённом debug-режиме. Раз она видна на проде — где-то в конфиге DEBUG=True.

Проверили окружение самого процесса, а не файл на диске (мало ли, systemd читает что-то другое):

sudo cat /proc/$(pgrep -f "gunicorn: master" | head -1)/environ | tr '\0' '\n' | grep -i DEBUG

Вывод: DEBUG=True. Дальше — сравнить, что реально лежит в файле, с тем, что должно быть по резервной копии, которую держит скрипт настройки окружения перед каждым деплоем:

diff /opt/app/.env /opt/deploy/backups/.env.prod.bak.$(date +%F)

Diff показал ровно то, что и ожидалось: файл со стенда полностью затёр прод-версию. Кроме DEBUG=True туда прилетели ALLOWED_HOSTS=*, тестовый SECRET_KEY, консольный backend для почты и включённый debug-toolbar — то есть весь «удобный для разработки» набор.

Дальше все симптомы сложились в понятную картину:

  • Рост памяти — debug-toolbar перехватывает каждый SQL-запрос и держит историю в памяти процесса на весь его жизненный цикл, отсюда линейный рост RSS от рестарта до рестарта.
  • Повторяющиеся SQL-запросы — тот же toolbar оборачивает курсор БД, чтобы собрать статистику по запросам для своей панели; в логе PostgreSQL это выглядит как лишняя нагрузка, хотя бизнес-логика ни при чём.
  • Обнулившийся кеш — тестовый .env переопределял CACHES на LocMemCache (локальный кеш в памяти процесса для удобства локальной разработки) вместо общего Redis. Каждый воркер держал свой изолированный кеш, поэтому hit ratio на реальном Redis и провалился — приложение просто перестало туда стучаться.
  • Секреты в Sentry и в трейсбеках у пользователей — стандартное поведение Django при DEBUG=True: на любой необработанной ошибке отдаётся подробная страница с локальными переменными, путями и куском стека вызовов. Тот же контент утекает и в Sentry, если он настроен цеплять локальные переменные из трейса.

Почему DEBUG=True на проде — это больше, чем тормоза

Стоит закрыть вопрос «а что тут вообще критичного, ну потормозило» отдельно, потому что это самая опасная часть инцидента, а не самая заметная.

  • Утечка внутренней структуры и секретов. Страница ошибки в debug-режиме показывает локальные переменные функции на момент падения — это могут быть куски строки подключения к БД, значения API-ключей, переданные аргументы. Если такую страницу увидел случайный пользователь или бот-сканер — считайте, что эти значения потенциально скомпрометированы.
  • Отключённая проверка ALLOWED_HOSTS. С ALLOWED_HOSTS=* приложение принимает запросы с любым заголовком Host, что открывает почву для атак вроде host header injection и отравления ссылок для сброса пароля, которые формируются на основе Host.
  • Тестовый SECRET_KEY на проде. Django использует этот ключ для подписи сессионных cookie и токенов. Если тестовый ключ известен шире, чем должен быть (например, лежит в репозитории или на общем стенде), под угрозой оказываются сессии пользователей.
  • Тихий сбой бизнес-логики. Consol-backend для почты означает, что письма (подтверждение заказа, сброс пароля) не уходят наружу, а просто печатаются в лог процесса. Внешне всё «работает» — запрос обработан 200-м ответом, — а получатель никакого письма не увидел. Это отдельный, менее заметный инцидент внутри первого.
  • Более медленная раздача статики. В debug-режиме шаблоны не кешируются и перекомпилируются на каждый запрос, а часть оптимизаций (сжатие, condition-запросы) в тестовой конфигурации осознанно отключена ради удобства отладки — на проде это прямая просадка по скорости отклика.

Про похожий по механике случай, когда чувствительные данные утекли не через конфиг, а через лог сборки CI, есть отдельный разбор: утечка секрета в лог сборки и шесть часов ротации ключей. Логика реагирования там та же: как только есть подозрение, что секрет мог засветиться где-то за пределами доверенного контура — его нужно считать скомпрометированным и менять, а не гадать, увидел ли его кто-то на самом деле.

Что изменили после инцидента

Сразу после того, как причина подтвердилась, вернули корректный .env из бэкапа и перезапустили сервис — сайт вернулся к обычной скорости в течение минуты. Дальше — уже спокойная работа над тем, чтобы так не повторилось.

Ротация секретов. SECRET_KEY, пароль от БД и API-ключи, которые потенциально засветились в трейсбеках и в Sentry, заменили на новые, а старые сессии инвалидировали. Подробный чек-лист для такой ситуации — в статье про ротацию секретов и ключей на практике.

Явный guard от запуска с DEBUG на проде. В settings.py добавили проверку, которая не даёт приложению вообще подняться в неправильной конфигурации:

if ENVIRONMENT == "production" and DEBUG:
    raise ImproperlyConfigured(
        "DEBUG=True недопустим в production. Проверьте .env."
    )

Теперь ошибка конфигурации останавливает деплой сразу, а не проявляется через полчаса деградацией производительности.

Разделение зависимостей. django-debug-toolbar и другие dev-инструменты вынесли в отдельный requirements-dev.txt, которого нет в прод-образе. Даже если переменная окружения снова окажется не той, физически нечему будет подключиться.

Отказ от ручного scp конфигов между серверами. Раньше правки .env на проде и «синхронизация» со стенда руками были обычной практикой — и это в чистом виде та самая антипаттерн-ситуация, разобранная в статье конфиги правятся прямо на проде. Теперь прод-конфиг генерируется деплой-пайплайном из отдельного зашифрованного репозитория секретов, а прямой доступ по SSH для правки файлов конфигурации закрыт для большинства участников команды — доступ и роли для таких серверов лучше сразу фиксировать отдельным регламентом, а не держать в головах.

Автоматическая проверка после деплоя. В CI/CD добавили шаг, который после выкладки дергает заведомо «падающий» тестовый URL и проверяет, что в ответе нет слова Traceback и нет debug-toolbar в разметке страницы:

RESPONSE=$(curl -s https://app.example.com/__healthcheck-error/)
if echo "$RESPONSE" | grep -qi "Traceback\|debug_toolbar"; then
  echo "DEBUG-режим обнаружен на проде, деплой остановлен"
  exit 1
fi

Мониторинг памяти воркеров как отдельный сигнал. Раньше алерты были настроены только на CPU и на количество 5xx. Добавили отдельный алерт на linear-рост RSS процессов gunicorn и на количество OOM-килов в journalctl -k, потому что именно эта картина — «память растёт ровно, потом сброс» — оказалась самым ранним и самым надёжным признаком проблемы, задолго до жалоб пользователей.

Тестовые окружения теперь физически изолированы по конфигу. Тестовый и прод .env стали генерироваться из разных источников без общего файла-первоисточника — по образцу изоляции окружений, описанной в статье про настройку staging-копии сайта. Совпадающие по смыслу переменные (домен, ключи, флаги окружения) больше не хранятся в одном общем файле, который можно случайно скопировать целиком.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро проверить, не включён ли DEBUG на проде, если под рукой нет доступа в код?

Откройте в браузере заведомо несуществующий URL вашего сайта (например, /this-page-does-not-exist-12345/). Если вместо обычной страницы 404 или общего экрана ошибки вы видите подробный трейсбек с кодом и путями к файлам — debug-режим включён, и это нужно чинить немедленно, а не «на следующей неделе».

Почему включённый DEBUG приводит именно к утечке памяти, а не просто к более подробным ошибкам?

Сам по себе DEBUG=True память не ест — проблему создают инструменты, которые включаются вместе с ним, в первую очередь debug-toolbar: он перехватывает каждый SQL-запрос и держит историю в памяти процесса, пока тот жив. На проде с постоянной нагрузкой это превращается в постепенный рост RSS до упора в лимит и перезапуск по OOM.

Можно ли просто запретить копирование .env между серверами на уровне прав доступа?

Отчасти да — chmod 400 и владелец только у деплой-пользователя снижают риск случайной правки руками, но полностью проблему это не закрывает: у человека с доступом к серверу всегда остаётся возможность сделать scp не в ту сторону. Более надёжно — убрать саму возможность ручной правки прод-конфига, генерируя его только пайплайном из источника секретов, и добавить guard в код приложения, который откажется стартовать в неправильной конфигурации.

Что делать, если стектрейс с секретами уже кто-то увидел — скриншот в соцсетях, кеш поисковика?

Считать соответствующие секреты скомпрометированными и менять их, не дожидаясь доказательств, что их кто-то использовал. Дальше — проверить логи доступа за период, когда debug-режим был включён, на предмет запросов к чувствительным URL, и просмотреть Sentry/APM на предмет того, какие ещё данные могли попасть в захваченный контекст ошибок.

Стоит ли вообще держать django-debug-toolbar и подобные инструменты на тестовом сервере, если это создаёт риск такой утечки?

Стоит, но при условии, что тестовое и прод-окружение физически не могут перепутаться конфигами — отдельные репозитории или директории секретов, разные пользователи деплоя, разные ключи доступа. Сама по себе отладочная панель на стенде — не проблема, проблема в том, что конфиги двух сред хранились в файле, который можно было скопировать одной командой целиком.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →