Как секрет 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. В этой статье рассматривается вся цепочка.

Конфигурация#

Architecture of VSO, Vault and the Kubernetes API in the homelab cluster

Всё работает в одном K8s кластере:

  • Vault работает в пространстве имён hvault, установлен из Helm-чарта. В нём, в KV v2-хранилище, находится токен GitLab Runner (вместе с другими секретами).
  • VSO работает в собственном пространстве имён и отслеживает три пользовательских ресурса: VaultConnection, VaultAuth and VaultStaticSecret.
  • GitLab runner pods бегают в пространстве имён gitlab и используют обычный Kubernetes Secret, который VSO поддерживает в актуальном состоянии.

Два разных механизма#

Синхронизация выглядит как одна функция, но на самом деле состоит из двух отдельных механизмов:

  1. Метод Kubernetes-аутентификации Vault отвечает на вопрос: «Кто обращается ко мне?»
  2. Цикл синхронизации VSO отвечает на вопрос: «Как открытый текст секрета оказывается в объекте Secret?»

Ошибка 403 выше возникает в первом механизме. До второго дело даже не доходит.

Фаза 1: «проверка личности» VSO#

Sequence of the VSO login and secret sync

Шаги 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-сервер выполняет две проверки в следующем порядке:

  1. Может ли проверяющий отправлять такойИмеет ли проверяющий право отправлять такой запрос? Если нет, сервер возвращает 403 или 401 ещё до проверки самого JWT.
  2. Действителен ли JWT? Проверяются подпись, срок действия, аудитория, а также существование ServiceAccount с тем же UID.

Шаг 6: проверка роли и выдача клиентского токена. Vault сравнивает полученную идентичность со значениями:

  • bound_service_account_names;
  • bound_service_account_namespaces;
  • audience.

Если все три совпадают, Vault выдает токен доступа с прописанной для данного service account ролью доступа и возвращает его VSO.

В этом процессе участвуют три разных identity. Перепутать их проще простого.

IdentityДля чего используетсяТребования
service account проверяющего сервиса VaultVault вызывающий TokenReviewsystem:auth-delegator привязка
service account in VaultAuthidentity, которую Vault использует для привязки к ролиСуществовать. RBAC не нужен.
service account контроллера VSOВыпускание токенов и создание Secretserviceaccounts/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 messageUsual 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) claimaudiences в VaultAuth отличается от audience роли

Выводы#

  • permission denied от VSO — это реальная проблема. Причину нужно искать в журнале Vault.
  • Не путать три identity: Vault’s reviewer, VaultAuth service account, и контроллер VSO.
  • После обновления кластера сначала проверить RBAC-привязку проверяющего, его токен, CA и issuer, и только потом менять остальные настройки.
  • Неудачное обновление оставляет старый Secret на месте, поэтому следует отслеживать ошибки VSO, а не считать значение актуальным автоматически.