Каждый external-secrets-operator, каждый Vault Agent Injector и каждый CI job, который раздаёт подам ключи и токены, решают одну и ту же задачу с одним и тем же слабым местом: где-то должен лежать секрет, который прочитает любой, кто получит доступ к его хранилищу или к самому поду. SPIFFE переворачивает вопрос — вместо «что под знает» он спрашивает «кем под может криптографически доказать, что является», и делает это без единого pre-shared значения. SPIRE, референсная реализация SPIFFE, уже стала стандартным слоем identity под сервис-мешами вроде Cilium и Istio. Разбираем, как это устроено и чем принципиально отличается от раздачи секретов.
Содержание
Открыть содержание
- Почему это не игнорировать в 2026
- SVID: чем под доказывает, кто он
- Node attestation и workload attestation: откуда identity без секрета
- Чем это отличается от external-secrets-operator
- Федерация: identity между кластерами и облаками
- Таблица сравнения
- Что нужно, чтобы развернуть SPIRE и получить первый SVID
- Как проверить, что identity работает
- Итог
Почему это не игнорировать в 2026
Zero-trust архитектура строится на постоянной проверке «кто это» на каждом сетевом вызове, а не на периметре. Проблема в том, что большинство существующих механизмов identity для workload-ов — API-ключи, статичные сертификаты, service account токены с долгим TTL — это ровно то, от чего zero trust должен уходить: значение, которое можно скопировать, положить в конфиг и использовать откуда угодно, пока кто-то не заметит утечку. SPIFFE (Secure Production Identity Framework For Everyone) стандартизирует другой подход — identity, которая выдаётся на лету на основе того, чем workload действительно является в рантайме (под в namespace, под сервис-аккаунтом, на конкретном узле), а не того, что ему когда-то положили в переменную окружения.
Практический сигнал, что это уже мейнстрим, а не экспериментальная спецификация: Cilium — один из самых распространённых CNI и сервис-мешей в Kubernetes — интегрируется со SPIFFE/SPIRE именно как со слоем workload identity для mTLS между подами (разбор интеграции). Если вы уже строите zero-trust сеть на service mesh, вопрос не «нужен ли SPIFFE», а «на чём он у вас уже работает под капотом».
SVID: чем под доказывает, кто он
Единица identity в SPIFFE — SVID (SPIFFE Verifiable Identity Document). Это не токен, который под где-то хранит, а документ, который SPIRE Agent выдаёт по запросу и постоянно перевыпускает. SPIFFE ID внутри SVID выглядит как URI: spiffe://example.org/ns/default/sa/checkout-api — trust domain плюс путь, однозначно указывающий, что это за workload.
SPIFFE определяет два формата SVID, и выбор между ними — не вопрос вкуса:
- X.509-SVID — короткоживущий сертификат, SPIFFE ID зашит в SAN URI. Основной формат для mTLS между сервисами: Envoy/Cilium используют его для установления TLS-соединения и проверки identity собеседника без похода куда-либо ещё. Именно этот формат нужен, когда обе стороны — workload-ы в мешe.
- JWT-SVID — подписанный JWT, SPIFFE ID в claim
sub. Уместен там, где mTLS не дотягивается: вызов через API-шлюз, который понимает только bearer-токены, serverless-функция с коротким жизненным циклом, или сценарий, где TLS терминируется раньше, чем хочется передать identity дальше.
Оба выдаются одним и тем же SPIRE Agent через один и тот же API — выбор формата определяется тем, кто на другом конце, а не тем, какая инфраструктура у вас развёрнута.
Node attestation и workload attestation: откуда identity без секрета
Ключевой вопрос: если под не хранит секрет, откуда SPIRE вообще знает, что перед ним легитимный workload, а не самозванец? Ответ — двухуровневая аттестация, ни один из уровней которой не требует pre-shared значения.
Node attestation происходит один раз, когда SPIRE Agent (обычно DaemonSet) стартует на узле. Агент доказывает SPIRE Server, на каком именно узле он запущен — через платформенный признак, который нельзя подделать без компрометации самой платформы: projected service account token узла в Kubernetes (k8s_psat), instance identity document в AWS, TPM-квота на bare metal. Сервер сверяет это с реальным состоянием платформы (например, спрашивает Kubernetes API, существует ли такой под-агент в таком namespace) и выдаёт узлу Node SVID.
Workload attestation происходит на каждый запрос локального процесса к агенту. Агент не спрашивает у процесса пароль — он смотрит на сам факт существования процесса через ОС и платформу: PID, cgroup, а для Kubernetes — запрос к kubelet, из которого агент узнаёт namespace, service account и labels пода, которому принадлежит этот PID. Эти атрибуты — селекторы — сверяются с зарегистрированными на сервере entry, и если совпадение найдено, агент выдаёт SVID с соответствующим SPIFFE ID.
Ни на одном из шагов нет значения, которое нужно было бы сгенерировать заранее, разложить по секретным хранилищам и надеяться, что оно не утечёт. Identity выводится из наблюдаемого состояния платформы в момент запроса.
Чем это отличается от external-secrets-operator
Это не конкурирующие инструменты — они решают разные задачи, и в большинстве zero-trust архитектур нужны оба.
external-secrets-operator синхронизирует значения из внешнего секрет-стора (Vault, AWS Secrets Manager, GCP Secret Manager) в Kubernetes Secret, чтобы под их смонтировал или прочитал как переменную окружения. Это распределение секретов: credential material материализуется, лежит в etcd (пусть и encrypted at rest), и его нужно ротировать по расписанию — вручную или через refreshInterval.
SPIRE ничего не распределяет. SVID выдаётся по запросу через Workload API и нигде не персистится дольше своего TTL. Это доказательство identity: под не хранит credential material вообще — он каждый раз заново доказывает, кто он есть, предъявляя факт своего существования в кластере.
Разница особенно заметна при компрометации пода. Если атакующий получает доступ к поду с секретом из ESO, он получает значение с полным сроком жизни до следующей плановой ротации — часы, а часто и дни. Если атакующий получает доступ к поду с SPIRE, он получает SVID с TTL в пределах текущего окна (обычно минуты), и продлить его дальше без доступа к самому процессу пода уже нельзя — как только Workload API перестаёт видеть легитимный процесс с нужными селекторами, перевыпуск прекращается.
Федерация: identity между кластерами и облаками
Trust domain — это единица доверия SPIFFE, обычно один SPIRE-деплоймент со своим корневым CA. Внутри одного trust domain всё просто: сервер выдаёт SVID, workload-ы доверяют друг другу через общий корень. Сложность начинается, когда сервисы в разных кластерах или разных облаках (то есть в разных trust domain) должны установить mTLS друг с другом.
Федерация решает это без слияния PKI и без общего секрета между кластерами: два SPIRE Server обмениваются trust bundle — набором доверенных корневых сертификатов друг друга, публикуемым через SPIFFE Federation API. После настройки федерации workload в trust domain A может проверить X.509-SVID workload-а из trust domain B точно так же, как проверял бы SVID из своего домена — потому что теперь у него в бандле есть корень, которым подписан чужой SVID. Каждый кластер по-прежнему полностью управляет своим собственным CA и своими собственными registration entry — федерация про обмен доверием, а не про делегирование управления.
Таблица сравнения
| Параметр | external-secrets-operator | SPIFFE/SPIRE |
|---|---|---|
| Что получает под | статичный секрет (API-ключ, пароль, токен) | короткоживущий SVID — под ничего не хранит |
| Credential material в кластере | да, в Kubernetes Secret (даже если encrypted at rest) | нет — выдаётся по запросу, не персистится |
| Источник доверия | внешний секрет-стор (Vault, AWS/GCP SM) | SPIRE Server + двухуровневая аттестация |
| Ротация | по расписанию, часто требует пересоздания Secret | автоматическая, до истечения TTL, без рестарта пода |
| При компрометации пода | секрет с полным сроком жизни до следующей ротации | SVID с TTL в пределах текущего окна, дальше не продлевается |
| Что доказывается | «у меня есть значение из Secret» | «я процесс, прошедший node+workload аттестацию в этом кластере» |
| Кросс-кластерность | отдельный секрет-стор или реплика на каждый кластер | федерация через обмен trust bundle |
Что нужно, чтобы развернуть SPIRE и получить первый SVID
Минимальный стенд — SPIRE Server (StatefulSet, один под с диском под CA-ключ) и SPIRE Agent (DaemonSet, по поду на узел), плюс один registration entry для тестового пода.
Конфиг сервера задаёт trust domain и способ node attestation:
# server.conf (фрагмент)
server {
trust_domain = "example.org"
data_dir = "/run/spire/data"
bind_address = "0.0.0.0"
bind_port = "8081"
}
plugins {
NodeAttestor "k8s_psat" {
plugin_data {
clusters = {
"prod" = {
service_account_allow_list = ["spire:spire-agent"]
}
}
}
}
KeyManager "disk" { plugin_data { keys_path = "/run/spire/data/keys.json" } }
DataStore "sql" { plugin_data { database_type = "sqlite3" connection_string = "/run/spire/data/datastore.sqlite3" } }
}
Конфиг агента — тот же trust domain, адрес сервера и workload-аттестор для Kubernetes:
# agent.conf (фрагмент)
agent {
data_dir = "/run/spire/data"
log_level = "INFO"
server_address = "spire-server"
server_port = "8081"
socket_path = "/run/spire/sockets/agent.sock"
trust_domain = "example.org"
}
plugins {
NodeAttestor "k8s_psat" { plugin_data { cluster = "prod" } }
WorkloadAttestor "k8s" { plugin_data { skip_kubelet_verification = true } }
KeyManager "memory" {}
}
После kubectl apply на оба манифеста регистрируем тестовый под — привязываем SPIFFE ID к селекторам namespace и service account:
kubectl exec -n spire spire-server-0 -- \
/opt/spire/bin/spire-server entry create \
-parentID spiffe://example.org/ns/spire/sa/spire-agent \
-spiffeID spiffe://example.org/ns/default/sa/checkout-api \
-selector k8s:ns:default \
-selector k8s:sa:checkout-api
Сам под забирает SVID через Workload API — Unix-сокет, который агент монтирует на узел через hostPath, а под — через тот же volume:
kubectl run checkout-api -n default --serviceaccount=checkout-api \
--image=ghcr.io/spiffe/spire-agent:1.10.0 --command -- sleep infinity
kubectl exec -n default checkout-api -- \
/opt/spire/bin/spire-agent api fetch x509 \
-socketPath /run/spire/sockets/agent.sock
# Received 1 svid after 12ms
# SPIFFE ID: spiffe://example.org/ns/default/sa/checkout-api
Никакого секрета в манифесте пода — только service account, из которого агент вывел селекторы, и сокет, через который под спросил «кто я».
Как проверить, что identity работает
Убедитесь, что запись зарегистрирована и что цепочка доверия действительно замкнута на корень trust domain:
kubectl exec -n spire spire-server-0 -- \
/opt/spire/bin/spire-server entry show \
-spiffeID spiffe://example.org/ns/default/sa/checkout-api
# Entry ID : 3f1c9b2a-...
# SPIFFE ID : spiffe://example.org/ns/default/sa/checkout-api
# Parent ID : spiffe://example.org/ns/spire/sa/spire-agent
# Selector : k8s:ns:default
# Selector : k8s:sa:checkout-api
# TTL : default
kubectl exec -n default checkout-api -- \
/opt/spire/bin/spire-agent api fetch x509 \
-socketPath /run/spire/sockets/agent.sock -write /tmp/svid
openssl x509 -in /tmp/svid.0.pem -noout -text | grep -A1 "Subject Alternative Name"
# URI:spiffe://example.org/ns/default/sa/checkout-api
Если entry show находит запись, а извлечённый сертификат несёт правильный SPIFFE ID в SAN URI и проверяется корнем, который выдал сам SPIRE Server — цепочка доверия замкнута от аттестации узла до конкретного пода, без единого значения, положенного туда вручную.
Итог
Секреты — это то, что вы храните и раздаёте, а значит то, что можно скопировать, забыть отозвать и потерять при компрометации хранилища. SPIFFE identity — это то, чем под каждый раз заново доказывает, кто он есть, опираясь на наблюдаемое состояние платформы, а не на значение, положенное туда заранее. В zero-trust архитектуре нужны оба слоя — секреты для доступа к внешним системам, identity для доверия между вашими же workload-ами, — но начинать разумно со второго: identity не лежит в etcd, не попадает в git по ошибке и не переживает под, который её получил.