Files
progcode/editorial/agent-rewrites/285.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 285,
"slug": "editorial-2020-02-practice-configs-secrets",
"title": "Настройки и секреты: как провести конфигурацию до процесса без утечки",
"excerpt": "Сервис запускается локально, но в CI получает пустой URL, а токен может остаться в Git, Docker-образе или логе. Разбираем границы конфигурации, проверяем их короткими командами и задаём критерий готовности.",
"contentHtml": "<p>Сервис запускается на ноутбуке, но после выпуска получает пустой URL или неверный уровень логирования. Команда открывает CI-лог, печатает окружение и находит там токен. Иногда секрет уже попал в Docker-образ или старый commit. Цена ошибки складывается из простоя и ротации доступа. Нужно выпустить новую пару ключей, найти всех потребителей старой, проверить кэш и логи, а затем повторить релиз. Обычная настройка превращается в инцидент.</p>\n<p>Причина обычно не в синтаксисе <code>.env</code>. Проект смешивает два разных класса данных: настройка меняет поведение, а секрет даёт право на действие. Оба значения проходят через несколько поверхностей: репозиторий, сборку, image, канал доставки, процесс и журнал. У каждой поверхности своя аудитория и срок жизни. Без явной границы секрет перемещается туда, где его удобнее отладить.</p>\n<p>Тезис простой: храните в коде имена и безопасные примеры, передавайте чувствительные значения при запуске, валидируйте их на границе runtime и выводите только безопасный отчёт. Это не заменяет систему управления секретами и права доступа. Зато такой контракт делает утечку заметной до первого запроса.</p>\n<h2>Разделите значение по назначению</h2>\n<p><code>LOG_LEVEL=info</code> — настройка поведения. Её имя и пример можно хранить рядом с кодом. <code>PAYMENTS_API_URL</code> — адрес зависимости. Он не обязан быть секретом, но внутренний адрес всё равно может раскрывать топологию. <code>PAYMENTS_TOKEN</code> — credential. Его имя нужно приложению, а значение не нужно репозиторию, образу или общему логу. <code>CONFIG_REVISION</code> — технический идентификатор набора. Он помогает сопоставить запуск и конфигурацию, но не заменяет секрет.</p>\n<div class=\"table-scroll\"><table><caption>Минимальный контракт конфигурации</caption><thead><tr><th scope=\"col\">Класс</th><th scope=\"col\">Пример</th><th scope=\"col\">Допустимый носитель</th><th scope=\"col\">Диагностика</th></tr></thead><tbody><tr><td>Поведение</td><td><code>LOG_LEVEL=info</code></td><td>Шаблон, документация, runtime</td><td>Имя и выбранное значение</td></tr><tr><td>Адрес</td><td><code>PAYMENTS_API_URL</code></td><td>Шаблон с фиктивным адресом и runtime</td><td>Имя; значение только при безопасной видимости</td></tr><tr><td>Секрет</td><td><code>PAYMENTS_TOKEN</code></td><td>Отдельный защищённый канал запуска</td><td>Имя и <code>[REDACTED]</code></td></tr><tr><td>Идентификатор</td><td><code>CONFIG_REVISION=sample-42</code></td><td>Шаблон и запись выпуска</td><td>Идентификатор набора без credential</td></tr></tbody></table></div>\n<p>Таблица задаёт рабочую гипотезу, а не универсальную классификацию. Внутренний URL может быть чувствительным. Идентификатор может раскрывать детали релиза. Перед переносом значения спросите: кто его читает, сколько оно живёт, нужно ли ему попадать в этот носитель и кто отвечает за замену.</p>\n<figure><img src=\"/assets/editorial/2020/config-secret-boundary-2020.svg\" alt=\"Схема границы конфигурации: репозиторий хранит имена, delivery передаёт значение, runtime валидирует его, а лог получает маску\" loading=\"lazy\" /><figcaption>Конфигурация проходит четыре границы. Секрет останавливается в runtime и не становится частью репозитория, образа или общего диагностического вывода.</figcaption></figure>\n<h2>Как значение доходит до процесса</h2>\n<p>Репозиторий должен содержать шаблон. В нём есть имена, назначение и фиктивные defaults. Настоящий токен в шаблон не подставляют. Build собирает код и зависимости. Он не должен запекать credential в bundle или Docker image. Delivery выбирает конкретную среду и передаёт runtime нужные значения своим защищённым способом. Loader получает строки, проверяет обязательные имена и создаёт объект, который код использует дальше.</p>\n<p>Переменная окружения — только канал передачи. Она не гарантирует секретность. Процесс может передать окружение дочерней команде, библиотека может записать его в ошибку, а shell-скрипт может напечатать команду целиком. Поэтому проверяйте не только источник, но и все места, куда значение копируется.</p>\n<h2>Учебный пример: шаблон и loader</h2>\n<p>Ниже приведён учебный пример. Значение токена фиктивное, домен <code>.invalid</code> не обозначает реальный сервис. Пример показывает границу контракта и не доказывает безопасность конкретного production-контура.</p>\n<pre><code># 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</code></pre>\n<pre><code>const 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}</code></pre>\n<p>Loader останавливает запуск до первого внешнего запроса, если обязательное имя отсутствует. Ошибка сообщает имя настройки, но не значение. Отчёт сохраняет полезные поля и маскирует token. Маска не закрывает доступ к процессу. Она убирает один распространённый путь случайной публикации через лог или support-диагностику.</p>\n<p>Тест loader может передать обычный объект вместо <code>process.env</code>. Один сценарий удаляет <code>PAYMENTS_TOKEN</code> и ожидает ошибку. Другой проверяет URL и уровень логирования. Третий проверяет, что отчёт содержит <code>[REDACTED]</code>. Эти сценарии не обращаются к платёжной системе и не используют настоящий credential.</p>\n<h2>Почему одного .gitignore недостаточно</h2>\n<p>Добавьте локальный файл в <code>.gitignore</code>, чтобы новый <code>.env.local</code> не попал в индекс случайно. Но ignore-правило не удаляет уже отслеживаемый файл и не стирает значение из истории. Если секрет был закоммичен, cleanup файла не завершает работу. Сначала отзовите credential и выпустите новый. Затем проверьте историю, кэши, артефакты и журналы по правилам вашего контура.</p>\n<pre><code>git check-ignore -v .env.local\ngit ls-files --error-unmatch .env.local</code></pre>\n<p>Первая команда показывает правило для неотслеживаемого пути. Вторая должна завершиться ошибкой, если файл не tracked. Эти команды проверяют только Git. Они ничего не говорят о Docker context, CI cache и старых образах. Для каждой поверхности нужен отдельный check.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Короткая диагностика конфигурации</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Локально работает, в среде пустой URL</td><td>Имя или источник не совпадает между средами</td><td>Сверить контракт loader и итоговые имена без вывода значений</td><td>Зафиксировать источник и добавить fail-fast на старте</td></tr><tr><td>Токен виден в commit</td><td>Файл был tracked или значение попало в шаблон</td><td>Проверить <code>git ls-files</code> и историю</td><td>Отозвать токен, выпустить новый, затем удалить носители</td></tr><tr><td>Токен виден в image</td><td>Credential передали через <code>ENV</code>, <code>ARG</code> или build script</td><td>Проверить Dockerfile, build args, слои и метаданные образа</td><td>Убрать значение из build и передавать его при запуске</td></tr><tr><td>Ошибка содержит весь env</td><td>Logger или serializer получил объект окружения целиком</td><td>Пройти error path и поискать dump в коде и логах</td><td>Передавать safe report и тестировать редактирование полей</td></tr><tr><td>После смены ключа часть запросов падает</td><td>Потребители используют разные каналы или версии</td><td>Сопоставить revision, владельцев и сроки действия</td><td>Составить порядок ротации и проверить каждый потребитель</td></tr></tbody></table></div>\n<h2>Порядок проверки перед выпуском</h2>\n<ol><li>Составьте инвентарь переменных. Для каждой запишите класс, потребителя, источник, срок жизни и владельца. Не переносите неизвестное значение «на всякий случай».</li><li>Создайте versioned-шаблон. Оставьте имена, безопасные defaults и фиктивные адреса. Не вставляйте реальные значения даже в комментарии и учебные fixtures.</li><li>Добавьте один loader на границе runtime. Он проверяет обязательные имена, типы и допустимые значения. Он не печатает credential при ошибке.</li><li>Проверьте канал delivery. Зафиксируйте, кто выдаёт секрет процессу, кто меняет его и как команда получает уведомление о ротации.</li><li>Проверьте build boundary. Уберите секреты из Dockerfile, build args, generated bundle, shell-команд и CI-комментариев. Проверьте, что лог не печатает окружение.</li><li>Проверьте отрицательный путь. Удалите обязательное значение, прервите запуск и убедитесь, что процесс не сделал внешний запрос и не раскрыл значение.</li><li>Запустите зависимый учебный или тестовый сценарий с фиктивным credential. Сопоставьте только результат, имя конфигурации и безопасный идентификатор revision.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Этот порядок не выбирает за команду vault, CI-секреты или файл на хосте. У разных контуров разные требования к доступу, аудиту, резервированию и ротации. Переменная окружения не становится безопасной только потому, что она не видна в исходниках. Пользователь процесса, администратор хоста и библиотека с правом чтения окружения всё ещё могут получить её.</p>\n<p>Если credential уже утёк, не ограничивайтесь добавлением <code>.gitignore</code> или маской в новом логе. Такой путь отрицателен: старый ключ продолжает действовать, а старые копии остаются доступными. Сначала отзовите и замените credential. Потом определите носители и сроки удаления. После этого добавьте проверку, которая не даст повторить тот же путь.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Работа готова, если команда может назвать источник каждого обязательного значения, показать versioned-шаблон без реальных секретов, запустить loader с фиктивными данными и получить безопасный отчёт. В Git локальный файл не tracked. В Dockerfile и build output нет credential. При отсутствии обязательного значения процесс завершается до внешнего запроса. В логах остаются имя поля, результат проверки и revision, но не значение. Если хотя бы один пункт нельзя проверить повторяемой командой или тестом, конфигурация ещё не готова к выпуску.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://git-scm.com/docs/gitignore\" target=\"_blank\" rel=\"noopener noreferrer\">Git documentation: gitignore</a> — правила для неотслеживаемых путей и границы их действия.</li><li><a href=\"https://docs.docker.com/reference/dockerfile/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Docs: Dockerfile reference</a> — поведение <code>ENV</code> и <code>ARG</code>, а также ограничения передачи чувствительных значений при сборке.</li><li><a href=\"https://nodejs.org/dist/latest-v12.x/docs/api/process.html#process_process_env\" target=\"_blank\" rel=\"noopener noreferrer\">Node.js documentation: process.env</a> — доступ процесса к переменным окружения.</li></ul>"
}