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

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

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

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

  • 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.

В этом процессе участвуют три разные идентичности. Их смешение — самый распространённый источник путаницы:

Идентичность Используется для Требования ServiceAccount проверяющего Vault Вызов Vault API TokenReview Привязка к system:auth-delegator ServiceAccount из VaultAuth Идентичность, которую Vault сопоставляет с ролью Должен существовать; RBAC-права не требуются ServiceAccount контроллера VSO Создание токенов и запись Secret Разрешения create для serviceaccounts/token и запись в secrets

IdentityUsed forNeeds
Vault’s reviewer service accountVault calling TokenReviewsystem:auth-delegator binding
The service account in VaultAuthThe identity Vault maps to a roleTo exist. No RBAC required.
The VSO controller’s service accountMinting tokens and writing Secretsserviceaccounts/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 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Привязки роли не соответствуют ServiceAccount из VaultAuth
invalid audience (aud) claimaudiences в VaultAuth отличается от audience роли

Основные выводы#

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