8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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>"
|
||
}
|