Секреты в распределённых системах
Урок о том, куда деваются пароли и ключи в мире из десятков сервисов и как раздавать их безопасно.
Секрет — любой чувствительный материал доступа: пароль БД, 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-подход даёт единое место, политики доступа, аудит и централизованную ротацию.
- Короткоживущие динамические креды превращают секрет в расходник и снижают цену утечки.