Алексей Меленчук
вся записка
дата
объём 5 мин чтения

Секрет в переменной окружения — это не секрет

Переменные окружения видны в дампе, в отчёте о падении, в списке процессов и в панели оркестратора. Разбор того, где они утекают и что ставить вместо.

Коротко. Переменная окружения наследуется дочерними процессами и попадает в места, о которых не думают: трассировки, дампы, отладочные страницы. Это лучше пароля в репозитории и хуже, чем принято считать.

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

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

Где они видны

В списке процессов. На Linux окружение процесса лежит в /proc/PID/environ. Читать может владелец процесса и root. Значит, любой процесс от того же пользователя видит ваши секреты. В контейнере с одним приложением это менее страшно, но контейнеры с сайдкаром или с агентом мониторинга — обычное дело.

В дочерних процессах. Окружение наследуется. Запустили из приложения convert, ffmpeg, скрипт резервного копирования — они получили пароль от базы. Не потому что он им нужен, а потому что так работает fork.

Это самый недооценённый канал. Уязвимость в стороннем бинарнике, который вы вызываете для конвертации картинок, становится способом прочитать ваши ключи.

В отчётах о падении. Многие обработчики ошибок прикладывают окружение к отчёту, чтобы было легче разбираться. Дальше отчёт улетает во внешний сервис. Классика жанра — переменные попадают в трассировку целиком.

В отладочных страницах. Страница с диагностикой фреймворка, включённая на проде «на пять минут», печатает окружение целиком.

В панели оркестратора. В Kubernetes переменные из манифеста пода видит каждый, у кого есть право читать поды в этом пространстве имён. Права на чтение подов дают широко, потому что без них нельзя посмотреть журнал.

В дампе памяти. Окружение лежит в памяти процесса. Дамп, снятый для отладки утечки, содержит все секреты и обычно кладётся туда, где хранение никто не продумывал.

Что ставить вместо

По возрастанию сложности. Первый шаг закрывает большинство перечисленного.

Файл, а не переменная. Секрет монтируется файлом с правами 0400 на пользователя приложения, приложение читает его при старте. Файл не наследуется дочерними процессами, не попадает в environ, не печатается на отладочной странице.

Переменной остаётся только путь к файлу. Именно так устроены docker-совместимые *_FILE-переменные, и это дешёвое улучшение: поддержка добавляется десятью строками.

Хранилище секретов. Vault, KMS, secrets manager облака. Приложение получает не сам секрет, а право его запросить, аутентифицируясь ролью машины или сервисным аккаунтом. Даёт две вещи, которых нет у файла: журнал обращений и возможность отозвать доступ, не перевыкатывая приложение.

Короткоживущие учётные данные. Верхний уровень: приложение получает пароль к базе на час, дальше продлевает. Украденный секрет протухает сам. Требует поддержки со стороны базы и заметной работы.

Секреты в образе и в истории

Два канала утечки, которые не про переменные окружения, но всплывают в том же разговоре.

Слои образа помнят всё. COPY файла с ключом, а потом RUN rm в следующей инструкции не удаляет его: файл остался в предыдущем слое, и его достанет любой, кто скачал образ. То же с ARG, переданным на сборку, — он виден в истории образа.

Рабочий способ — монтирование секрета только на время выполнения инструкции, без попадания в слой:

RUN --mount=type=secret,id=npmrc \
    npm ci --userconfig=/run/secrets/npmrc

История Git не забывает. Удалённый коммитом пароль остаётся в истории, и переписывание истории на общей ветке — операция, которую редко доводят до конца. Правильная реакция на пароль в репозитории всегда одна: сменить пароль. Чистка истории — вторым шагом и не вместо первого.

Что важнее выбора хранилища

Три вещи, которые обычно откладывают, а они дают больше, чем переход с переменных на Vault.

Ротация, которую пробовали. Секрет, который никогда не меняли, — это секрет, который вы не сможете поменять быстро в день, когда придётся. Процедура смены должна быть выполнена хотя бы раз, в спокойный день, по написанной инструкции. Обычно на первой попытке выясняется, что пароль зашит ещё в трёх местах.

Разные секреты для разных сред. Одинаковый пароль в тестовой и боевой среде превращает доступ к тестовой в доступ к боевой. Тестовая среда всегда защищена хуже — это её нормальное свойство.

Пароль, который был в переписке, считается скомпрометированным. Не «наверное». Считается. Мессенджеры хранят историю, устройства теряются, чаты пересылают. Единственная разумная реакция — сменить, и сменить до того, как это станет темой отдельного разговора.

Чего я не делаю

Не шифрую секреты в репозитории «для удобства». Зашифрованный файл в репозитории требует ключа расшифровки, и он тоже где-то лежит. Иногда это осмысленно, но чаще получается перекладывание задачи на один шаг с ощущением, что она решена.

Не пишу секреты в журнал даже на уровне отладки. Строка конфигурации с паролем, выведенная при старте «чтобы проверить, что подхватилось», живёт в журнале ровно столько же, сколько всё остальное.

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