{ "index": 285, "slug": "editorial-2020-02-practice-configs-secrets", "title": "Настройки и секреты: как провести конфигурацию до процесса без утечки", "excerpt": "Сервис запускается локально, но в CI получает пустой URL, а токен может остаться в Git, Docker-образе или логе. Разбираем границы конфигурации, проверяем их короткими командами и задаём критерий готовности.", "contentHtml": "
Сервис запускается на ноутбуке, но после выпуска получает пустой URL или неверный уровень логирования. Команда открывает CI-лог, печатает окружение и находит там токен. Иногда секрет уже попал в Docker-образ или старый commit. Цена ошибки складывается из простоя и ротации доступа. Нужно выпустить новую пару ключей, найти всех потребителей старой, проверить кэш и логи, а затем повторить релиз. Обычная настройка превращается в инцидент.
\nПричина обычно не в синтаксисе .env. Проект смешивает два разных класса данных: настройка меняет поведение, а секрет даёт право на действие. Оба значения проходят через несколько поверхностей: репозиторий, сборку, image, канал доставки, процесс и журнал. У каждой поверхности своя аудитория и срок жизни. Без явной границы секрет перемещается туда, где его удобнее отладить.
Тезис простой: храните в коде имена и безопасные примеры, передавайте чувствительные значения при запуске, валидируйте их на границе runtime и выводите только безопасный отчёт. Это не заменяет систему управления секретами и права доступа. Зато такой контракт делает утечку заметной до первого запроса.
\nLOG_LEVEL=info — настройка поведения. Её имя и пример можно хранить рядом с кодом. PAYMENTS_API_URL — адрес зависимости. Он не обязан быть секретом, но внутренний адрес всё равно может раскрывать топологию. PAYMENTS_TOKEN — credential. Его имя нужно приложению, а значение не нужно репозиторию, образу или общему логу. CONFIG_REVISION — технический идентификатор набора. Он помогает сопоставить запуск и конфигурацию, но не заменяет секрет.
| Класс | Пример | Допустимый носитель | Диагностика |
|---|---|---|---|
| Поведение | LOG_LEVEL=info | Шаблон, документация, runtime | Имя и выбранное значение |
| Адрес | PAYMENTS_API_URL | Шаблон с фиктивным адресом и runtime | Имя; значение только при безопасной видимости |
| Секрет | PAYMENTS_TOKEN | Отдельный защищённый канал запуска | Имя и [REDACTED] |
| Идентификатор | CONFIG_REVISION=sample-42 | Шаблон и запись выпуска | Идентификатор набора без credential |
Таблица задаёт рабочую гипотезу, а не универсальную классификацию. Внутренний URL может быть чувствительным. Идентификатор может раскрывать детали релиза. Перед переносом значения спросите: кто его читает, сколько оно живёт, нужно ли ему попадать в этот носитель и кто отвечает за замену.
\nРепозиторий должен содержать шаблон. В нём есть имена, назначение и фиктивные defaults. Настоящий токен в шаблон не подставляют. Build собирает код и зависимости. Он не должен запекать credential в bundle или Docker image. Delivery выбирает конкретную среду и передаёт runtime нужные значения своим защищённым способом. Loader получает строки, проверяет обязательные имена и создаёт объект, который код использует дальше.
\nПеременная окружения — только канал передачи. Она не гарантирует секретность. Процесс может передать окружение дочерней команде, библиотека может записать его в ошибку, а shell-скрипт может напечатать команду целиком. Поэтому проверяйте не только источник, но и все места, куда значение копируется.
\nНиже приведён учебный пример. Значение токена фиктивное, домен .invalid не обозначает реальный сервис. Пример показывает границу контракта и не доказывает безопасность конкретного production-контура.
# config.example.env — можно хранить в репозитории\nAPP_ENV=development\nPAYMENTS_API_URL=https://gateway.invalid\nPAYMENTS_TOKEN=DEMO_ONLY_NOT_A_SECRET\nLOG_LEVEL=info\nCONFIG_REVISION=sample-42\nconst required = [\"APP_ENV\", \"PAYMENTS_API_URL\", \"PAYMENTS_TOKEN\"];\n\nexport function loadConfig(env = process.env) {\n for (const name of required) {\n if (!env[name]) {\n throw new Error(`Missing required setting: ${name}`);\n }\n }\n\n return {\n appEnv: env.APP_ENV,\n paymentsApiUrl: env.PAYMENTS_API_URL,\n paymentsToken: env.PAYMENTS_TOKEN,\n logLevel: env.LOG_LEVEL || \"info\",\n revision: env.CONFIG_REVISION || \"unknown\",\n };\n}\n\nexport function safeConfigReport(config) {\n return {\n appEnv: config.appEnv,\n paymentsApiUrl: config.paymentsApiUrl,\n paymentsToken: \"[REDACTED]\",\n logLevel: config.logLevel,\n revision: config.revision,\n };\n}\nLoader останавливает запуск до первого внешнего запроса, если обязательное имя отсутствует. Ошибка сообщает имя настройки, но не значение. Отчёт сохраняет полезные поля и маскирует token. Маска не закрывает доступ к процессу. Она убирает один распространённый путь случайной публикации через лог или support-диагностику.
\nТест loader может передать обычный объект вместо process.env. Один сценарий удаляет PAYMENTS_TOKEN и ожидает ошибку. Другой проверяет URL и уровень логирования. Третий проверяет, что отчёт содержит [REDACTED]. Эти сценарии не обращаются к платёжной системе и не используют настоящий credential.
Добавьте локальный файл в .gitignore, чтобы новый .env.local не попал в индекс случайно. Но ignore-правило не удаляет уже отслеживаемый файл и не стирает значение из истории. Если секрет был закоммичен, cleanup файла не завершает работу. Сначала отзовите credential и выпустите новый. Затем проверьте историю, кэши, артефакты и журналы по правилам вашего контура.
git check-ignore -v .env.local\ngit ls-files --error-unmatch .env.local\nПервая команда показывает правило для неотслеживаемого пути. Вторая должна завершиться ошибкой, если файл не tracked. Эти команды проверяют только Git. Они ничего не говорят о Docker context, CI cache и старых образах. Для каждой поверхности нужен отдельный check.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Локально работает, в среде пустой URL | Имя или источник не совпадает между средами | Сверить контракт loader и итоговые имена без вывода значений | Зафиксировать источник и добавить fail-fast на старте |
| Токен виден в commit | Файл был tracked или значение попало в шаблон | Проверить git ls-files и историю | Отозвать токен, выпустить новый, затем удалить носители |
| Токен виден в image | Credential передали через ENV, ARG или build script | Проверить Dockerfile, build args, слои и метаданные образа | Убрать значение из build и передавать его при запуске |
| Ошибка содержит весь env | Logger или serializer получил объект окружения целиком | Пройти error path и поискать dump в коде и логах | Передавать safe report и тестировать редактирование полей |
| После смены ключа часть запросов падает | Потребители используют разные каналы или версии | Сопоставить revision, владельцев и сроки действия | Составить порядок ротации и проверить каждый потребитель |
Этот порядок не выбирает за команду vault, CI-секреты или файл на хосте. У разных контуров разные требования к доступу, аудиту, резервированию и ротации. Переменная окружения не становится безопасной только потому, что она не видна в исходниках. Пользователь процесса, администратор хоста и библиотека с правом чтения окружения всё ещё могут получить её.
\nЕсли credential уже утёк, не ограничивайтесь добавлением .gitignore или маской в новом логе. Такой путь отрицателен: старый ключ продолжает действовать, а старые копии остаются доступными. Сначала отзовите и замените credential. Потом определите носители и сроки удаления. После этого добавьте проверку, которая не даст повторить тот же путь.
Работа готова, если команда может назвать источник каждого обязательного значения, показать versioned-шаблон без реальных секретов, запустить loader с фиктивными данными и получить безопасный отчёт. В Git локальный файл не tracked. В Dockerfile и build output нет credential. При отсутствии обязательного значения процесс завершается до внешнего запроса. В логах остаются имя поля, результат проверки и revision, но не значение. Если хотя бы один пункт нельзя проверить повторяемой командой или тестом, конфигурация ещё не готова к выпуску.
\nENV и ARG, а также ограничения передачи чувствительных значений при сборке.