Как секрет Vault становится Kubernetes Secret
Как секрет Vault становится Kubernetes Secret#
Погружаемся в глубины Homelab. Bare-metal-кластер Kubernetes v1.36, Vault, запущенный в том же кластере через Helm-чарт, и Vault Secrets Operator (VSO), синхронизирующий секреты для GitLab Runner.
Сразу после обновления моего домашнего кластера до Kubernetes v1.36 каждый VaultStaticSecret начал записывать одну и ту же ошибку:
Failed to get Vault auth login: Error making API request.
URL: PUT http://vault.hvault.svc.cluster.local:8200/v1/auth/kubernetes/login
Code: 403. Errors: * permission denied
Это сообщение почти ничего не объясняет. Для отладки нужно чётко понимать, что происходит между моментом, когда «VSO хочет получить секрет», и моментом, когда объект Kubernetes Secret появляется в etcd. В этой статье рассматривается вся цепочка.
Конфигурация#

Всё работает в одном K8s кластере:
- Vault работает в пространстве имён
hvault, установлен из Helm-чарта. В нём, в KV v2-хранилище, находится токен GitLab Runner (вместе с другими секретами). - VSO работает в собственном пространстве имён и отслеживает три пользовательских ресурса:
VaultConnection,VaultAuthandVaultStaticSecret. - GitLab runner pods бегают в пространстве имён
gitlabи используют обычный KubernetesSecret, который VSO поддерживает в актуальном состоянии.
Два разных механизма#
Синхронизация выглядит как одна функция, но на самом деле состоит из двух отдельных механизмов:
- Метод Kubernetes-аутентификации Vault отвечает на вопрос: «Кто обращается ко мне?»
- Цикл синхронизации VSO отвечает на вопрос: «Как открытый текст секрета оказывается в объекте
Secret?»
Ошибка 403 выше возникает в первом механизме. До второго дело даже не доходит.
Фаза 1: «проверка личности» VSO#

Шаги 1 и 2: получение JWT. VSO не использует токен, смонтированный в собственный pod. Он обращается к Kubernetes TokenRequest API для ServiceAccount, указанного в ресурсе VaultAuth, и запрашивает короткоживущий JWT с аудиторией vault. API-сервер подписывает его ключом service account кластера.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata: { name: vault-auth, namespace: gitlab }
spec:
vaultConnectionRef: default
method: kubernetes
mount: kubernetes
kubernetes:
role: gitlab-runner
serviceAccount: vso-gitlab
audiences: ["vault"]
Шаг 3: вход в Vault. VSO отправляет имя роли и JWT в auth/kubernetes/login. Пока Vault не может доверять этому JWT, поскольку у него нет ключа подписи кластера.
Шаги 4 и 5: TokenReview. Vault делегирует проверку Kubernetes. Он вызывает API TokenReview, используя собственные учётные данные проверяющего. Это токен service account, которому разрешено выполнять операцию create tokenreviews. На практике это означает наличие ClusterRoleBinding к system:auth-delegator. Когда Vault работает внутри кластера и параметр token_reviewer_jwt не настроен, он использует токен собственного pod.
API-сервер выполняет две проверки в следующем порядке:
- Может ли проверяющий отправлять такойИмеет ли проверяющий право отправлять такой запрос? Если нет, сервер возвращает 403 или 401 ещё до проверки самого JWT.
- Действителен ли JWT? Проверяются подпись, срок действия, аудитория, а также существование ServiceAccount с тем же UID.
Шаг 6: проверка роли и выдача клиентского токена. Vault сравнивает полученную идентичность со значениями:
- bound_service_account_names;
- bound_service_account_namespaces;
- audience.
Если все три совпадают, Vault выдает токен доступа с прописанной для данного service account ролью доступа и возвращает его VSO.
В этом процессе участвуют три разных identity. Перепутать их проще простого.
| Identity | Для чего используется | Требования |
|---|---|---|
| service account проверяющего сервиса Vault | Vault вызывающий TokenReview | system:auth-delegator привязка |
service account in VaultAuth | identity, которую Vault использует для привязки к роли | Существовать. RBAC не нужен. |
| service account контроллера VSO | Выпускание токенов и создание Secret | serviceaccounts/token create, secrets write |
Почему ошибка 403 почти ничего не говорит#
Vault возвращает общее сообщение permission denied для любой неудачной попытки входа:
- неизвестная роль;
- ServiceAccount не привязан к роли;
- несовпадение издателя;
- несовпадение аудитории;
- запрещённый запрос TokenReview.
VSO видит только общий ответ. Настоящую причину можно найти в журнале сервера Vault.
Фаза 2: чтение секрета и запись Secret#
После того как VSO получает клиентский токен, всё остальное достаточно просто.
Шаги 7 и 8: чтение. VSO отправляет запрос: GET /v1/secret/data/... с клиентским токеном. Vault проверяет политику токена для указанного пути и возвращает данные KV v2.
Шаг 9: запись. VSO кодирует каждое значение в base64, создаёт объект Secret, устанавливает owner reference для VaultStaticSecret и записывает его через Kubernetes API. Для этого используются собственные RBAC-права VSO. Vault в этой части не участвует.
Phase 3: обновление секретов и устаревшие секреты#
По умолчанию VSO выполняет периодический опрос. На каждом интервале refreshAfter он повторяет чтение и снова выполняет вход, если сохранённый токен Vault больше недействителен.
Это объясняет два наблюдения:
- число ошибок продолжало расти, поскольку каждая новая попытка обновления повторяла неудачный вход;
Secretне исчезал: VSO не удаляетSecret, если обновление завершилось ошибкой, поэтому последнее синхронизированное значение остаётся в кластере и постепенно устаревает.
Что видят сервисы, использующие секреты#
Как быстро новое значение Secret попадает в сервисы его использующие зависит от способа использования Secret:
- Mounted volumes обновляются kubelet примерно через минуту и обычно не требуют перезапуска pod.
- Переменные окружения считываются один раз при запуске контейнера, поэтому для получения нового значения нужен перезапуск.
imagePullSecretsсчитываются при планировании pod, поэтому после ротации токена изменится только следующий pod.
Краткая памятка по отладке#
Начинать лучше с журнала Vault, поскольку VSO не сообщает настоящую причину ошибки:
kubectl -n hvault logs <vault-pod> | grep -iE "kubernetes|tokenreview|issuer|audience"
| Log message | Usual cause |
|---|---|
invalid issuer / claim "iss" is invalid | Изменился издатель API-сервера или параметр issuer не настроен |
| role not found | Опечатка в имени роли в VaultAuth |
tokenreviews is forbidden / Unauthorized | Отсутствует привязка auth-delegator или используется устаревший token_reviewer_jwt |
x509: certificate signed by unknown authority | Настроенный CA больше не соответствует API-серверу |
service account name not authorized / namespace not authorized | Привязки роли не соответствуют service account из VaultAuth |
invalid audience (aud) claim | audiences в VaultAuth отличается от audience роли |
Выводы#
permission deniedот VSO — это реальная проблема. Причину нужно искать в журнале Vault.- Не путать три identity: Vault’s reviewer,
VaultAuthservice account, и контроллер VSO. - После обновления кластера сначала проверить RBAC-привязку проверяющего, его токен, CA и issuer, и только потом менять остальные настройки.
- Неудачное обновление оставляет старый Secret на месте, поэтому следует отслеживать ошибки VSO, а не считать значение актуальным автоматически.