Vault JWT auth из GitLab CI: без статических токенов
Published: 2026-05-21
Статические Vault-токены в CI-переменных — это риск: они не истекают, лишены контекста, и один утёкший .gitlab-ci.yml открывает доступ ко всем секретам, которые может прочитать токен. Метод JWT auth решает эту проблему: GitLab выдаёт подписанный OIDC-токен на каждый job, Vault проверяет его по JWKS-эндпоинту GitLab и возвращает краткосрочный Vault-токен ровно с той политикой, которая задана для роли.
Как работает GitLab JWT
В каждом GitLab CI job доступна переменная CI_JOB_JWT — подписанный JWT с клеймами project_path, ref, environment и user_login. Метод JWT auth в Vault настраивается на JWKS URL GitLab и проверяет подпись токена — никаких заранее разделяемых секретов не требуется.
Последовательность:
- GitLab выпускает JWT, подписанный своим приватным ключом
- CI job вызывает
vault write auth/jwt/login role=... jwt=$CI_JOB_JWT - Vault скачивает JWKS GitLab и проверяет подпись JWT
- Vault сверяет
bound_claimsс клеймами в payload JWT - Если все проверки пройдены — Vault возвращает краткосрочный токен
Конфигурация Vault
bash# Включить JWT auth
vault auth enable jwt
# Указать на JWKS GitLab
vault write auth/jwt/config \
jwks_url="https://gitlab.example.com/-/jwks" \
bound_issuer="https://gitlab.example.com"
Роли для каждой среды
bashvault write auth/jwt/role/access-Development \
role_type="jwt" \
bound_audiences="https://vault.test.antonnovikov.com" \
user_claim="user_login" \
bound_claims='{"project_path": ["company/backend/*"], "ref": ["dev", "feat/*"]}' \
policies="app-development" \
ttl="1h"
vault write auth/jwt/role/access-Production \
role_type="jwt" \
bound_audiences="https://vault.test.antonnovikov.com" \
user_claim="user_login" \
bound_claims='{"project_path": ["company/backend/*"], "ref": ["rel/*"]}' \
policies="app-production" \
ttl="30m"
Ключевые моменты:
bound_claims.refограничивает, с каких git-веток можно деплоить в prod — толькоrel/*.ttl=30mдля production. Job должен завершиться в этом окне.- Каждая среда получает собственную политику с отдельными KV-путями.
- Glob-паттерны в
project_pathпозволяют нескольким сервисам использовать одну роль.
Политика Vault
hcl# Политика app-development
path "Microservices/+/Development/*" {
capabilities = ["read"]
}
path "common-secret/Development/" {
capabilities = ["read"]
}
hcl# Политика app-production
path "Microservices/+/Production/*" {
capabilities = ["read"]
}
path "common-secret/Production/" {
capabilities = ["read"]
}
Политики дают только read. Никакого create, update, delete — CI никогда не должен изменять секреты.
Сторона CI
yamlvariables:
VAULT_ADDR: https://vault.test.antonnovikov.com
DOCKER_IMAGE: registry.example.com/ci-images/vault-cli:1.0.1
.auth:jwt: &auth_jwt |
export VAULT_TOKEN="$(vault write -field=token auth/jwt/login \
role=access-${APP_ENV} \
jwt=$CI_JOB_JWT)"
vault kv get -field=kubeconfig_${NAMESPACE} common-secret/${APP_ENV} > kube_config
chmod 600 kube_config
export KUBECONFIG=kube_config
deploy_dev:
image: ${DOCKER_IMAGE}
stage: deploy
before_script:
- *auth_jwt
script:
- helm upgrade --install ...
variables:
APP_ENV: Development
NAMESPACE: app
deploy_prod:
image: ${DOCKER_IMAGE}
stage: deploy
before_script:
- *auth_jwt
script:
- helm upgrade --install ...
variables:
APP_ENV: Production
NAMESPACE: app
rules:
- if: $CI_COMMIT_REF_NAME =~ /^rel\//
YAML-якорь *auth_jwt объявляется один раз и переиспользуется в каждом deploy job. Меняется только APP_ENV.
Кастомный CI-образ
Образ vault-cli содержит vault, consul-template, helm, kubectl и curl. Тег в DOCKER_IMAGE зафиксирован — каждый deploy job получает одно и то же окружение независимо от изменений в upstream:
dockerfileFROM hashicorp/vault:1.15
RUN apk add --no-cache curl helm kubectl
COPY --from=hashicorp/consul-template:0.33 /bin/consul-template /bin/consul-template
Kubeconfig из Vault
Вместо хранения kubeconfig в CI-переменной GitLab (там он виден открытым текстом в интерфейсе) — хранить его в Vault под common-secret/<env>/kubeconfig_<cluster> и забирать при деплое:
bashvault kv get -field=kubeconfig_app common-secret/Production > kube_config
chmod 600 kube_config
export KUBECONFIG=kube_config
Kubeconfig содержит service account token, ограниченный правом helm upgrade в конкретном namespace. Никакого cluster-admin. Никакого постоянного доступа к shell.
Что нельзя сделать с токеном
| Возможность | Статус |
|---|---|
| Доступ к секретам другой среды | Невозможно — bound_claims не пропустит |
| Использовать токен после завершения job | Нет — ttl истёк |
| Деплоить prod с feature-ветки | Нет — bound_claims.ref допускает только rel/* |
| Изменять секреты Vault | Нет — политика даёт только read |
| Доступ к секретам других проектов | Нет — клейм project_path ограничен |
Отладка ошибок аутентификации
bash# Ручная проверка JWT auth
vault write auth/jwt/login \
role=access-Development \
jwt=$CI_JOB_JWT
# Декодировать клеймы JWT (без проверки подписи)
echo $CI_JOB_JWT | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Список настроенных ролей
vault list auth/jwt/role
# Детали роли
vault read auth/jwt/role/access-Development
Типичные ошибки:
bound claim mismatch—refилиproject_pathв JWT не соответствуетbound_claimsролиrole not found— опечатка вAPP_ENVinvalid audience—bound_audiencesне совпадает с URL вашего Vault