Короткий ответ
Секреты каждого окружения должны храниться отдельно (например, отдельные пути/namespace в Vault или отдельные защищённые CI-environments), выдаваться джобам только через scoped-доступ, привязанный к конкретному окружению и ветке (protected environments/branches), с ограниченным TTL и возможностью ротации без изменения кода пайплайна.
Как это работает подробнее
системы вроде HashiCorp Vault или облачные Secrets Manager позволяют выдавать краткоживущие токены/учётные данные конкретно под джобу деплоя в конкретное окружение (например, через Vault AppRole или OIDC-федерацию CI-системы, без долгоживущих статических ключей в CI-переменных). В GitHub Actions/GitLab CI это дополняется механизмом protected environments, который разрешает доступ к секретам окружения только джобам, запущенным для защищённой ветки/тега.
Пример кода
deploy_prod:
stage: deploy
environment:
name: production
script:
- vault write auth/approle/login role_id=$PROD_ROLE_ID secret_id=$PROD_SECRET_ID
- ./deploy.sh prod
rules:
- if: '$CI_COMMIT_BRANCH == "main"'Что использовать на практике
- На практике. разделение по окружениям снижает blast radius — если скомпрометирован раннер или секрет, используемый для dev, он физически не имеет доступа к prod-учётным данным. OIDC-федерация (например, GitHub Actions → AWS IAM Role через OIDC) вообще убирает долгоживущие секреты из CI-переменных, выдавая токен на время выполнения конкретного job.
- Пример. Ожидаемое поведение: джоба получает доступ к prod-секретам Vault только если выполняется для защищённого окружения `production` на ветке `main`; попытка получить тот же токен из джобы feature-ветки завершится отказом авторизации.
Подводные камни
- Частые ошибки. используют один и тот же сервисный аккаунт/ключ для всех окружений «для простоты настройки»; хранят prod-секреты в обычных (не protected) CI-переменных, доступных любой ветке, включая feature-ветки разработчиков.