Секреты в распределённых системах

Урок о том, куда деваются пароли и ключи в мире из десятков сервисов и как раздавать их безопасно.

Секрет — любой чувствительный материал доступа: пароль БД, API-ключ, приватный ключ, токен. В распределённой системе главный вопрос не «как зашифровать», а «как доставить нужный секрет нужному сервису и вовремя его сменить».

В монолите секрет можно было положить в один конфиг и забыть. Когда сервисов десятки, каждый из них должен где-то взять свои креды, причём так, чтобы секрет не утёк через git-историю, слой Docker-образа или дамп переменных окружения. Утечка одного ключа в распределённой системе нередко открывает доступ к данным, к которым атакующий шёл бы месяцами.

Зачем это знать защитнику

Захардкоженные секреты — одна из самых частых и дорогих ошибок. Боты непрерывно сканируют публичные репозитории на предмет ключей и срабатывают за минуты после коммита. Понимание, как устроено управление секретами, позволяет проектировать систему так, чтобы утечка была и менее вероятной, и менее болезненной.

Где секретам не место

В исходном коде

Ключ в коде попадает в систему контроля версий навсегда — даже если вы удалите его следующим коммитом, он остаётся в истории:

# ОПАСНО: секрет в коде попадёт в git-историю и в каждый клон репозитория
DB_PASSWORD = "S3cr3t-Prod-Pa55!"
conn = connect(host="db", password=DB_PASSWORD)

Защита начинается с того, что секреты не пишутся в код, а .env и подобные файлы добавляются в .gitignore. Если секрет всё же закоммитили — его недостаточно удалить, его нужно считать скомпрометированным и немедленно сменить (rotate).

В Docker-образе

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

# ОПАСНО: секрет вшит в слой образа и достаётся из истории слоёв
FROM python:3.12-slim
ENV API_KEY="live_sk_9f3a..."     # попадёт в каждый pull образа
COPY id_rsa /root/.ssh/id_rsa     # приватный ключ внутри образа

Образ должен быть «обезличенным»: один и тот же артефакт запускается в dev, staging и prod, а секреты подставляются снаружи в момент запуска. Это и безопаснее, и правильнее с точки зрения переносимости.

Про переменные окружения

ENV-переменные удобнее кода, но это не панацея: они видны в дампах процесса, в логах при отладке, наследуются дочерними процессами и порой утекают через сообщения об ошибках. Их допустимо использовать для доставки секрета, но не как «безопасное хранилище». Лучший подход — получать секрет в рантайме из специализированной системы.

Vault-подход: централизованное управление секретами

Идея в том, чтобы вынести секреты в отдельную защищённую систему (HashiCorp Vault, облачные Secrets Manager и аналоги), а сервисы получали их по запросу, доказав свою личность:

1. Сервис аутентифицируется в хранилище (по своей идентичности из service mesh / по
   платформенному токену), а НЕ по заранее вшитому паролю.
2. Хранилище проверяет, что этому сервису разрешён данный секрет (политика доступа).
3. Выдаёт секрет в памяти на ограниченный срок (TTL).
4. Доступ логируется: кто, что и когда запросил (аудит).

Что это даёт: секрет находится в одном защищённом месте, доступ к нему гранулярно разграничен политиками, каждое обращение записывается, а сменить ключ можно централизованно, не пересобирая сервисы. Возникает законный вопрос — «а как сервис аутентифицируется в хранилище, это же тоже секрет?». Ответ — secret zero: первичная идентичность выдаётся платформой (тот самый SPIFFE-идентитет из прошлого урока, токен Kubernetes ServiceAccount, роль облачного провайдера), её не нужно хранить как пароль.

Короткоживущие креды

Самый мощный приём — динамические секреты. Вместо постоянного пароля БД хранилище создаёт временную учётную запись прямо в момент запроса и удаляет её по истечении срока:

Сервис -> Vault: "нужен доступ к БД заказов"
Vault   -> БД:    CREATE временный пользователь, права read-only, TTL 1 час
Vault   -> Сервис: логин/пароль (живут 1 час, потом отзываются автоматически)

Философия меняется: секрет больше не «долгоживущая ценность, которую надо беречь годами», а «расходник на час». Украденная такая учётка протухает сама; компрометация не требует ручной гонки по смене паролей во всех сервисах.

Как это работает под капотом

Хранилище секретов шифрует данные в покое и держит мастер-ключ в специальном защищённом состоянии (его восстановление требует нескольких доверенных лиц — принцип разделения секрета). Доступ строится поверх той же идеи проверяемой идентичности, что и аутентификация сервисов: сначала сервис доказывает, кто он, затем политика решает, что ему можно. Аудит-журнал делает каждое обращение наблюдаемым — это превращает «кто-то взял ключ» из невидимого события в строку лога, по которой строится алертинг.

Как защититься

  • Никаких секретов в коде, истории git, Dockerfile и слоях образа; держите образ обезличенным.
  • Используйте централизованное хранилище секретов с политиками доступа и аудит-журналом.
  • Стремитесь к коротким TTL и динамическим секретам; постоянные креды — крайний случай.
  • Решайте проблему secret zero через идентичность платформы, а не через ещё один вшитый пароль.
  • Настройте сканер секретов (pre-commit hook, CI), чтобы ключ не попал в репозиторий.
  • Любой засветившийся секрет считайте скомпрометированным и немедленно ротируйте.

Все эксперименты — только в своих окружениях и учебных стендах. Использование чужих утёкших ключей и неправомерный доступ к данным наказуемы (в РФ — ст. 272 УК РФ).

Итоги

  • В распределённых системах ключевой вопрос — безопасная доставка и своевременная смена секретов.
  • Код, git-история и слои Docker-образа — места, где секрет утекает навсегда; ENV — не хранилище.
  • Vault-подход даёт единое место, политики доступа, аудит и централизованную ротацию.
  • Короткоживущие динамические креды превращают секрет в расходник и снижают цену утечки.
Проверьте себя
1. Почему задать секрет через ENV API_KEY="..." в Dockerfile — плохая идея?
AENV-переменные не поддерживаются в контейнерах
BСекрет вшивается в слой образа и достаётся из истории слоёв при каждом pull
CЭто замедляет сборку образа
DDocker автоматически публикует такие ключи в реестр
2. В чём главное преимущество динамических (короткоживущих) секретов из хранилища типа Vault?
AОни никогда не передаются по сети
BХранилище создаёт временную учётку с коротким TTL, поэтому украденный секрет быстро протухает сам
CОни не требуют никакой аутентификации
DИх можно хранить прямо в git, так как они зашифрованы