На прошлогоднем incident response в финтех-компании вся цепочка атаки уместилась в три строки CloudTrail: SSRF в микросервисе дёрнул IMDS на 169.251.163.254, атакующий вытащил IAM-ключ из метаданных EC2-инстанса, за 47 минут выгрузил клиентские данные из S3. Ключ был создан 14 месяцев назад и ни разу не ротировался. Когда я разбирал этот postmortem, картина сложилась быстро: проблема не в SSRF и не в отсутствии WAF. Проблема в том, что под в Kubernetes имел доступ к статичному IAM-ключу, которому не было причин существовать - workload identity делает такие ключи ненужными.
incident response - отчёт об инциденте
Далее в статье - про то, какие attack paths закрывает переход на short-lived credentials через SPIFFE/SPIRE и нативные механизмы облачных провайдеров, и как мониторить всё это в SIEM.IAM-ключ - это набор секретных данных, используемый для безопасной аутентификации пользователя, приложения или бота в облачных сервисах или корпоративных системах.
Статичные credentials в Kubernetes: карта атак через MITRE ATT&CK
Kubernetes-кластер в облаке по умолчанию подсовывает подам несколько векторов для получения credentials. Каждый маппится на конкретную технику MITRE ATT&CK, и без понимания этой карты нормальные detection-правила не выстроить.Cloud Instance Metadata API (T1552.005, Credential Access). Под на EC2-инстансе без IMDSv2 обращается к
169.251.163.254/latest/meta-data/iam/security-credentials/ и получает временные ключи роли ноды. При IMDSv2 с hop limit = 1 обычные поды (без hostNetwork) не получают ответ от IMDS - трафик проходит через veth + bridge, это 2 L3-хопа, TTL истекает. А вот контейнер с hostNetwork: true делит сетевой namespace с хостом напрямую - для него IMDS на 1 hop и всегда отдаёт credentials роли ноды. Именно так развивался инцидент из вступления.Container API (T1552.007, Credential Access). ServiceAccount token монтируется в каждый под по умолчанию в
/var/run/secrets/kubernetes.io/serviceaccount/token. Если у ServiceAccount избыточные RBAC-привилегии, атакующий из скомпрометированного пода получает доступ к API-серверу. В моей практике kubectl describe pod с env-переменными вида AWS_ACCESS_KEY_ID=AKIA... встречается удручающе регулярно.Steal Application Access Token (T1528, Credential Access). В средах без workload identity разработчики прописывают OAuth-токены и API-ключи в ConfigMap или Secret. Напомню: Secret в Kubernetes хранится в etcd в base64 (не зашифрованном по умолчанию), и любой пользователь с правами
get secrets видит содержимое в plaintext. Base64 - это не шифрование, это кодировка. Разница примерно как между замком и наклейкой «не открывать».Cloud Accounts (T1078.004, Initial Access / Persistence / Privilege Escalation). Утёкший статичный IAM-ключ используется для закрепления в облаке: создание новых ролей, расширение policy, развёртывание compute-ресурсов. По классификации MITRE ATT&CK эта техника одновременно применима на этапах initial access, persistence и privilege escalation - один утёкший ключ покрывает сразу три тактики.
Финансовый контекст для российского рынка: если утечка затрагивает PII граждан РФ, в игру вступают оборотные штрафы по 152-ФЗ - до 3% годовой выручки. Один скомпрометированный IAM-ключ с доступом к бакету с клиентскими данными генерирует штраф, несоизмеримый с затратами на внедрение workload identity.
SPIFFE и SPIRE: workload identity без секретов в Kubernetes
SPIFFE (Secure Production Identity Framework For Everyone) - открытый стандарт CNCF для идентификации workload'ов. Идея простая: вместо секретов, которые нужно ротировать и где-то хранить, каждый workload получает криптографически верифицируемую идентичность, привязанную к его runtime-окружению. Согласно исследованию на arxiv, SPIFFE «decouples identity from infrastructure, enabling strong, portable authentication across job runners and deployed workloads» - идентичность отвязана от конкретного облака и переносима между кластерами.
SPIRE (SPIFFE Runtime Environment) - production-ready реализация SPIFFE, graduated-проект CNCF.
Проблему «turtles all the way down» - цепочку секретов, управляющих другими секретами - хорошо описал инженер из Microsoft-экосистемы: «каждый скрипт для ротации секрета требует другой секрет для аутентификации, и этот секрет тоже нужно ротировать». Знакомо? SPIFFE разрывает этот цикл: идентичность выдаётся на основе аттестации окружения, а не предъявления секрета.
Архитектура SPIRE и выдача SVID
SPIRE Server - trust anchor домена. Хранит реестр Registration Entries (маппинг workload -> identity), управляет CA, выдаёт SVID. В Kubernetes разворачивается как StatefulSet. В nested-топологии для мультикластерных сред root SPIRE Server выдаёт промежуточные CA дочерним серверам - все workload'ы получают SVID из одного trust domain, mTLS между кластерами работает прозрачно.SPIRE Agent - DaemonSet на каждой ноде. Проводит node attestation через плагин
k8s_psat: агент предъявляет свой projected ServiceAccount token (с audience SPIRE Server), сервер валидирует его через TokenReview API Kubernetes, затем - workload attestation (проверяет pod labels, service account, namespace). После успешной аттестации workload получает SVID через Unix Domain Socket.SVID (SPIFFE Verifiable Identity Document) - короткоживущий X.509-сертификат или JWT-токен с SPIFFE ID вида
spiffe://trust-domain/ns/payments/sa/payment-service. TTL по умолчанию - 1 час, автоматическая ротация без участия разработчика.Пример создания Registration Entry:
Bash:
spire-server entry create \
-spiffeID spiffe://example.org/ns/default/sa/myapp \
-parentID spiffe://example.org/ns/spire/sa/spire-agent \
-selector k8s:ns:default \
-selector k8s:sa:myapp
k8s:ns:default и k8s:sa:myapp - механизм аттестации: SPIRE Agent проверяет, что под запущен в namespace default с ServiceAccount myapp. Подделать это из соседнего пода - задача нетривиальная, требующая escape to host (T1611, Privilege Escalation).Замечание для команд на Istio: service mesh жёстко требует формата SPIFFE ID
spiffe://<trust.domain>/ns/<namespace>/sa/<service-account>. Инженеры Indeed столкнулись с тем, что их существующий формат идентификаторов в SPIRE не совпадал с требованиями Istio - миграция потребовала временного отключения mTLS в mesh и поэтапного обновления конфигурации по кластерам. При планировании интеграции SPIRE с Istio этот constraint нужно закладывать на этапе архитектуры, а не обнаруживать в production.Pod identity: IRSA, Workload Identity Federation, Entra Workload ID
Каждый крупный облачный провайдер реализовал нативный механизм привязки Kubernetes ServiceAccount к облачной IAM-роли. Все три подхода решают одну задачу: pod получает short-lived token для доступа к облачным ресурсам без статичных ключей.
AWS: IRSA (IAM Roles for Service Accounts). ServiceAccount аннотируется ARN IAM-роли. Webhook инъецирует в под projected token и env-переменные для SDK. Pod получает temporary credentials через STS
AssumeRoleWithWebIdentity. TTL STS-сессии - от 900 секунд до значения MaxSessionDuration IAM-роли (по умолчанию 3600 сек, максимум 43200 сек при явной настройке роли).GCP: GKE Workload Identity / Workload Identity Federation. Для GKE-кластеров - GKE Workload Identity: Kubernetes ServiceAccount аннотируется через
iam.gke.io/gcp-service-account, pod получает токен Google Cloud без статичных ключей. Для не-GKE и мультиоблачных кластеров - Workload Identity Federation с workload identity pools: ServiceAccount token обменивается на Google Cloud token через Security Token Service. Attribute conditions с CEL-выражениями - например, assertion['kubernetes.io']['namespace'] in ['backend', 'monitoring'] - применимы именно к Workload Identity Federation, а не к GKE Workload Identity. Не путать.Azure: Microsoft Entra Workload ID. AKS-кластер выступает OIDC-издателем токенов. Entra ID валидирует подпись ServiceAccount token через OIDC discovery и обменивает его на Entra token. Обязательный label для подов:
azure.workload.identity/use: "true". Есть лимит federated identity credentials на один user-assigned managed identity или app registration (см. актуальную документацию Microsoft Entra) - при масштабировании на сотни сервисов это ограничение всплывает неожиданно.С точки зрения detection все три механизма генерируют события в CloudTrail, Cloud Audit Logs и Azure Activity Log соответственно - каждый
AssumeRoleWithWebIdentity или обмен токена привязан к конкретному ServiceAccount, namespace и кластеру. Принципиальное преимущество перед статичными ключами: атрибуция на уровне workload'а, которой раньше просто не было.Attack paths: как атакующий использует workload identity
Переход на short-lived credentials сужает окно атаки, но attack surface никуда не девается. Вот конкретные TTP, которые работают в средах с настроенным pod identity.Escape to Host -> кража SVID (T1611, Privilege Escalation). Container escape через уязвимость в runc или kubelet даёт атакующему доступ к host filesystem. SPIRE Agent socket (
/run/spire/sockets/agent.sock) на хосте доступен - агент выдаёт SVID процессу, прошедшему attestation, но на уровне хоста проверки менее гранулярные. Если агент запущен с hostPID: true - считай, attestation для хостового процесса пройдёт.Additional Container Cluster Roles (T1098.006, Persistence / Privilege Escalation). Атакующий с доступом к Kubernetes API создаёт ClusterRoleBinding, дающий произвольному ServiceAccount расширенные привилегии. Этот ServiceAccount получает SVID с широкими полномочиями или аннотацию с IAM-ролью администратора - и всё через легитимный Kubernetes API. Чисто, аккуратно, в логах выглядит как штатная операция.
Application Access Token replay (T1550.001, Lateral Movement). Перехваченный JWT-SVID или облачный token валиден до истечения TTL. При
default_svid_ttl = "1h" - это час для lateral movement к другим сервисам внутри trust domain. Для IRSA с durationSeconds=3600 - тот же час. Час - это много.Insider threat: скомпрометированный легитимный pod. Разработчик с доступом к namespace создаёт под с ServiceAccount, аннотированным на IAM-роль с избыточными привилегиями. Под проходит attestation легитимно, получает SVID и short-lived credentials. Без baseline поведения workload'а этот вектор невидим для стандартных detection-правил - всё выглядит как нормальная операция. Именно поэтому «у нас SPIRE, мы защищены» - опасная мантра.
Detection short-lived credentials и workload identity в SIEM
Для SOC-команды задача конкретная - построить baseline нормального поведения workload identity и детектировать отклонения. Источники и правила корреляции ниже.Источники событий для сбора:
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Результат - ваш опорный документ. Любое изменение без соответствующего change request - повод для расследования. Согласно NIST CSF DE.AE-01, baseline сетевых операций и ожидаемых потоков данных должен быть установлен и актуализироваться - для workload identity это означает регулярную сверку маппингов.
Чеклист hardening: сервисные аккаунты и zero trust аутентификация в Kubernetes
Чеклист пригоден для передачи инфраструктурной команде или включения в отчёт по аудиту. Основан на DISA Kubernetes STIG и NIST CSF PR.AA-01 (управление идентичностями и credentials).- Отключить автомонтирование SA-токена для подов без доступа к Kubernetes API:
automountServiceAccountToken: falseв PodSpec. - Включить IMDSv2 с hop limit = 1 на всех EC2-нодах EKS. Проверка:
aws ec2 describe-instances --query "Reservations[].Instances[].MetadataOptions". - Мигрировать со статичных IAM-ключей на IRSA / Workload Identity Federation / Entra Workload ID. Каждый
AKIA*в env-переменных пода - инцидент в ожидании. - Ограничить TTL credentials: для IRSA -
durationSecondsне более 3600 сек (минимум платформы - 900 сек); для SPIRE -default_svid_ttl = "15m"для критичных workload'ов. - Настроить attribute conditions для Workload Identity Federation: явный whitelist namespace'ов через CEL-выражения.
- Включить Kubernetes Audit Log с уровнем RequestResponse для: serviceaccounts, clusterrolebindings, rolebindings, secrets.
- Развернуть Falco с правилами на обращение к IMDS, чтение
/var/run/secrets/, доступ к SPIRE Agent socket из неожиданных процессов. - Запретить создание SA с аннотацией IAM-роли вне CI/CD pipeline через OPA/Gatekeeper или Kyverno policy.
- Провести инвентаризацию всех маппингов SA -> Cloud IAM Role (NIST CSF ID.AM-01). Формат: таблица «namespace/SA -> Role ARN -> минимально необходимые permissions».
- Мониторить ротацию SPIRE CA через Prometheus-метрику SPIRE Server, настроить alert на приближение expiration.
grep -r "AKIA" . по кластеру - и результат обычно неприятно удивляет.Главная иллюзия, которую я вижу у команд после внедрения pod identity: «у нас short-lived credentials, мы защищены». Опасное заблуждение. Short-lived credentials сужают окно атаки с месяцев до минут, но не закрывают его. JWT-SVID с TTL в час - это час, в течение которого украденный токен полностью валиден для lateral movement. IRSA-токен с
durationSeconds=3600 - тот же час. Без behavioral detection на уровне SIEM - baseline API-вызовов для каждого ServiceAccount, аномальные паттерны, нетипичные source IP - вы просто заменили один класс неконтролируемых credentials на другой, более короткий, но по-прежнему слепой для мониторинга.Реальный zero trust в cloud-native среде начинается не с SPIFFE/SPIRE и не с IRSA, а с момента, когда SOC может ответить на вопрос: «Какой набор API-вызовов нормален для ServiceAccount X в namespace Y и что является отклонением?» Пока этого baseline нет - identity layer работает вхолостую, создавая ложное чувство защищённости. Если калибруете правила для мониторинга workload identity аномалий под конкретный SIEM-стек - тематический тред с playbook'ом по cloud-native TTP ведётся на codeby.net.
Последнее редактирование модератором: