Звёздочка в середине пути Vault — не wildcard
Протухший токен уронил оператор секретов. Его перевыпуск вскрыл более старый баг: 27 политик Vault со * в середине пути молча запрещали то, что объявляли, и прятали избыточные права.
В Gambchamp общий HashiCorp Vault обслуживает несколько клиентских окружений, а оператор секретов в одном из кластеров синхронизирует из него значения в Kubernetes. Однажды оператор уже шесть часов был в crash-loop — 68 рестартов, и каждый reconcile падал с:
PUT /v1/auth/token/renew-self → 403
* permission denied
* invalid token
Две проблемы, а не одна
Легко прочитать только первую строку и пойти искать проблему в политике. Решающая — вторая: на невалидный токен Vault отвечает обоими сообщениями. Сама авария была просто в том, что статический токен оператора истёк или был отозван.
Перевыпуск должен был быть механическим. Не получилось. Свежий токен с тремя задокументированными read-only политиками окружения читал три из четырёх нужных оператору путей. Четвёртый отвечал permission denied, хотя политика прямо его называла.
Glob, который никогда не был glob’ом
В ACL Vault * — это wildcard только последним символом пути. + совпадает ровно с одним сегментом. Во всех остальных местах * — это просто сегмент с буквальным именем *:
# выглядит как wildcard, совпадает только с сегментом по имени "*"
path "…/frontend/*/config" { capabilities = ["read"] }
# как должно было быть
path "…/frontend/+/config" { capabilities = ["read"] }
Прогон по всем политикам нашёл 27 со * в середине пути. Vault принимает такую политику как валидную — никакого линтинга «этот шаблон никогда не совпадёт» нет.
Признак того, что это ошибка шаблона, а не замысел: соответствующая широкая политика на всех клиентов была написана правильно, с +. Значит, старый токен всё это время носил именно её — доступ на чтение секретов фронтенда всех клиентов, а не только своего. Истечение токена вскрыло избыточные права, которые были невидимы, пока всё работало.
Фикс
Править 27 общих политик посреди аварии я не стал. Вместо этого создал одну отдельную политику с правильным синтаксисом +, ровно на четыре пути, которые читает оператор, и выпустил новый токен с тремя важными для оператора свойствами:
- оставить
defaultв списке политик — там живутrenew-selfиlookup-self, без них та же авария повторится по таймеру; -periodвместо-ttl, чтобы токен продлевался, пока жив потребитель;-orphan, чтобы отзыв создавшего его токена не каскадировал.
Перед подключением я выполнил ровно тот вызов, что падал (token renew), прочитал четыре нужных пути и проверил, что путь другого окружения запрещён. Исправление общих шаблонов стало отдельной, неспешной задачей.
Выводы
- 403 от Vault с двумя строками ошибки — читайте вторую.
invalid token— не проблема политики. *в середине пути — литерал. Для одного сегмента —+, и помните, что Vault не предупредит.vault token capabilities <path>— единственное, что говорит правду о политике.- Никогда не «чините» доступ, навешивая более широкую политику. Это работает мгновенно и молча даёт чтение чужих окружений.
- Настоящая хрупкость — статические токены. Kubernetes auth через ServiceAccount убирает этот класс отказов целиком.