Секрет в переменной окружения — это не секрет
Переменные окружения видны в дампе, в отчёте о падении, в списке процессов и в панели оркестратора. Разбор того, где они утекают и что ставить вместо.
Коротко. Переменная окружения наследуется дочерними процессами и попадает в места, о которых не думают: трассировки, дампы, отладочные страницы. Это лучше пароля в репозитории и хуже, чем принято считать.
Двенадцатифакторное приложение советует хранить конфигурацию в переменных окружения. Совет хороший и в своё время закрыл главную проблему — пароли в репозитории.
Дальше произошло то, что происходит с любым хорошим советом: его распространили на случай, для которого он не предназначался. Конфигурация и секрет — не одно и то же, и обращения требуют разного.
Где они видны
В списке процессов. На 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 там — избыточная работа. Разговор начинается там, где к машине имеет доступ больше одного человека или где приложение запускает чужие бинарники.