Как секрет Vault становится Kubernetes Secret
Как секрет Vault становится Kubernetes Secret#
Погружаемся в глубины Homelab. Bare-metal-кластер Kubernetes v1.36.4, Vault, запущенный в том же кластере через официальный Helm-чарт, и Vault Secrets Operator (VSO), синхронизирующий секреты для GitLab Runner.
Сразу после обновления моего домашнего кластера до Kubernetes v1.36.4 каждый 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. В этой статье рассматривается вся цепочка.
Конфигурация#

Всё работает в одном bare-metal-кластере:
- 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.
В этом процессе участвуют три разные идентичности. Их смешение — самый распространённый источник путаницы:
Идентичность Используется для Требования ServiceAccount проверяющего Vault Вызов Vault API TokenReview Привязка к system:auth-delegator ServiceAccount из VaultAuth Идентичность, которую Vault сопоставляет с ролью Должен существовать; RBAC-права не требуются ServiceAccount контроллера VSO Создание токенов и запись Secret Разрешения create для serviceaccounts/token и запись в secrets
| Identity | Used for | Needs |
|---|---|---|
| Vault’s reviewer service account | Vault calling TokenReview | system:auth-delegator binding |
The service account in VaultAuth | The identity Vault maps to a role | To exist. No RBAC required. |
| The VSO controller’s service account | Minting tokens and writing Secrets | serviceaccounts/token create, secrets write |
Почему ошибка 403 почти ничего не говорит#
Vault возвращает общее сообщение permission denied для любой неудачной попытки входа:
- неизвестная роль;
- ServiceAccount не привязан к роли;
- несовпадение издателя;
- несовпадение аудитории;
- запрещённый запрос TokenReview.
VSO видит только общий ответ. Настоящая причина находится в журнале сервера Vault.
Фаза 2: чтение секрета и запись Secret#
После того как VSO получает клиентский токен, всё остальное происходит просто.
Шаги 7 и 8: чтение VSO отправляет запрос:
text 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:
- Тома обновляются 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 | Привязки роли не соответствуют ServiceAccount из VaultAuth |
invalid audience (aud) claim | audiences в VaultAuth отличается от audience роли |
Основные выводы#
- permission denied от VSO — это симптом. Причину нужно искать в журнале Vault.
- Не смешивайте три идентичности: проверяющий ServiceAccount Vault, ServiceAccount из VaultAuth и ServiceAccount контроллера VSO.
- После обновления кластера сначала проверьте RBAC-привязку проверяющего, его токен, CA и издателя, и только потом меняйте остальные настройки.
- Неудачное обновление оставляет старый Secret на месте, поэтому следует отслеживать ошибки VSO, а не считать значение актуальным автоматически.