Перейти к содержимому
Hogin Hogin
Назад

SPIFFE/SPIRE: криптографическая identity для подов вместо статичных секретов

9 мин чтения

Каждый external-secrets-operator, каждый Vault Agent Injector и каждый CI job, который раздаёт подам ключи и токены, решают одну и ту же задачу с одним и тем же слабым местом: где-то должен лежать секрет, который прочитает любой, кто получит доступ к его хранилищу или к самому поду. SPIFFE переворачивает вопрос — вместо «что под знает» он спрашивает «кем под может криптографически доказать, что является», и делает это без единого pre-shared значения. SPIRE, референсная реализация SPIFFE, уже стала стандартным слоем identity под сервис-мешами вроде Cilium и Istio. Разбираем, как это устроено и чем принципиально отличается от раздачи секретов.

Содержание

Открыть содержание

Почему это не игнорировать в 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.

X.509-SVID и JWT-SVID: разные форматы для разных сценариев

SPIFFE определяет два формата SVID, и выбор между ними — не вопрос вкуса:

Оба выдаются одним и тем же SPIRE Agent через один и тот же API — выбор формата определяется тем, кто на другом конце, а не тем, какая инфраструктура у вас развёрнута.

Node attestation и workload attestation: откуда identity без секрета

Ключевой вопрос: если под не хранит секрет, откуда SPIRE вообще знает, что перед ним легитимный workload, а не самозванец? Ответ — двухуровневая аттестация, ни один из уровней которой не требует pre-shared значения.

Двухуровневая аттестация: node attestation → workload attestation → SVID

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-operatorSPIFFE/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 по ошибке и не переживает под, который её получил.


Поделиться:

Следующая статья
SpinKube: запускаем WebAssembly-нагрузки в Kubernetes без своего рантайма