open atlas
↑ К треку
Безопасность облака и инфраструктуры CLOUD · 01 · 03

Принятие роли и федерация

Долгоживущие ключи доступа — это то, что утекает. Assume-role выдаёт короткоживущие STS-учётки для конкретной задачи; межаккаунтный доступ использует trust-политику с external id; CI и поды федерируются через OIDC, так что красть просто нечего.

CLOUD Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

GitHub Action запускает terraform apply против продакшена. Чтобы это сработало, кто-то два года назад вставил AWS_SECRET_ACCESS_KEY в CI-секреты репозитория — долгоживущий ключ, привязанный к роли с широкими правами. Ключ никогда не истекает, про его существование уже никто не помнит, и он лежит открытым текстом в окружении раннера на каждой сборке. Один утёкший лог воркфлоу, одна вредоносная зависимость в сборке, один форк с pull_request_target — и этот ключ уходит за дверь, всё ещё валидный, всё ещё привилегированный, пригодный из любой точки планеты, пока человек не заметит и не проротирует его. Решение — не лучшее хранилище секретов. Решение — вообще перестать иметь постоянный ключ: пусть раннер докажет, кто он, и получит учётки, которые мертвы уже через пятнадцать минут.

К концу урока ты будешь понимать, как AssumeRole выдаёт короткоживущие учётки, как межаккаунтная trust-политика и external id позволяют одному аккаунту безопасно действовать в другом, и как OIDC-федерация даёт CI и Kubernetes-нагрузкам доступ в облако вообще без хранимого ключа.

Проблема: постоянные ключи — это то, что утекает

У долгоживущего ключа доступа (пара AKIA..., JSON-файл сервис-аккаунта) есть три свойства, которые делают его самым массово эксфильтруемым облачным credential: он не истекает, он bearer-only (кто держит байты — тот и есть личность, никакого доказательства владения), и он пригоден из любой сети на земле. Поэтому ключ, утёкший в строке лога в 3 часа ночи, остаётся полностью валидной личностью к полудню следующего дня. Вся цель принятия роли — заменить этот постоянный ключ на credential, который ограничен задачей, по умолчанию короток и привязан к личности, которую надо доказать заново, чтобы получить ещё.

Механизм — это временные учётки безопасности. Вместо того чтобы держать ключ под нужные тебе права, ты держишь право попросить их. Ты вызываешь Security Token Service (sts:AssumeRole), называешь роль, которую тебе разрешено принять, и STS возвращает тройку — идентификатор ключа доступа, секрет и session token, — которая истекает по таймеру, заданному тобой (от 15 минут до 12 часов; час — частый дефолт). Когда она истекает, она инертна: утёкший session token, поднятый после своего TTL, ничего не стоит. Ты превратил постоянную уязвимость в скоропортящуюся.

Как на самом деле работает assume-role

Прежде чем STS что-либо выдаст, должны сойтись две политики, и их смешение — самая частая ошибка IAM по этой теме:

  • Trust-политика (AssumeRolePolicyDocument, привязанная к роли) отвечает на вопрос «кому разрешено стать этой ролью?» Её Principal называет личности — аккаунт, пользователя, другую роль или федеративного провайдера, — которым разрешено вызывать AssumeRole для неё.
  • Политика прав (привязанная к роли) отвечает на вопрос «раз ты стал этой ролью, что ты можешь делать?» — собственно s3:GetObject, dynamodb:Query и т. д.

Запрос на принятие роли отклоняется, если вызывающий одновременно не назван в trust-политике роли и не имеет права sts:AssumeRole на своей стороне. Это двустороннее рукопожатие и есть вся модель безопасности: владелец роли решает, кто может войти, а аккаунт вызывающего решает, может ли он пытаться. Когда STS одобряет, он возвращает короткоживущие учётки с правами роли — не вызывающего. Их можно сузить ещё на этапе принятия с помощью session-политики (inline-политики, переданной в вызове AssumeRole, которая может только вычитать права) — так ты выдаёшь под-задаче даже меньше, чем даёт роль.

Межаккаунтный доступ: trust-политика и фикс «запутанного помощника»

Межаккаунтный assume-role — это как CI-аккаунт, аккаунт аудиторской тулзы или сторонний SaaS получает ограниченный доступ в твой аккаунт, при этом ты ни разу не создаёшь для них пользователя. Ты создаёшь роль в своём аккаунте, чья trust-политика называет их аккаунт принципалом, привязываешь плотную политику прав и отдаёшь им ARN роли. Они принимают её со своей стороны и работают внутри твоего аккаунта как эта роль — никогда как долгоживущий ключ, который ты сгенерировал и отправил им.

Но у межаккаунтного доверия есть классическая ловушка: запутанный помощник (confused deputy). Допустим, SaaS-вендор принимает роль в аккаунте каждого клиента, используя один и тот же вендорский аккаунт как принципал. Если вендора обманом заставят использовать ARN твоей роли, действуя в интересах другого клиента, — или атакующий, знающий ARN твоей роли, сможет дотянуться до вендора, — к твоему аккаунту обращаются под легитимно доверенной личностью вендора. Фикс — это external id: выбранная тобой секретная строка, записанная в условие твоей trust-политики (sts:ExternalId), которую вендор обязан передавать на каждом AssumeRole. Поскольку она своя для каждого клиента и неугадываема, атакующий, знающий только ARN роли, не сможет заставить помощника применить её против тебя. Для своих собственных нагрузок современный эквивалент — условия OIDC по claim-ам субъекта (ниже); паттерн с external id — именно для третьих сторон, которых нельзя федерировать.

Почему это работает

Почему час, а не минута? Короче — безопаснее на один credential, но каждое истечение вынуждает round-trip повторного принятия, а у STS есть лимиты частоты запросов на уровне аккаунта. TTL в 15 минут на болтливом сервисе означает постоянную переаутентификацию и реальный шанс на throttling под нагрузкой; TTL в 12 часов на интерактивной админ-сессии — длинное окно для украденного токена. Зрелый рефлекс — сопоставлять TTL с радиусом поражения: минуты для высокопривилегированных или межаккаунтных ролей, час для обычной сервисной работы, и пусть credential-провайдер SDK авто-обновляет учётки, чтобы ротация была невидима коду. Смысл короткоживущих учёток не в максимуме трения — а в том, чтобы утёкший токен истёк раньше, чем станет полезен.

Федерация: CI и нагрузки без единого хранимого ключа

Финал убирает bootstrap-ключ целиком. Федерация позволяет личности извне собственного IAM облака — запуску GitHub Actions, поду Kubernetes, входу через Google или Okta — обменять уже имеющийся у неё токен на короткоживущие облачные учётки, при этом ни один ключ нигде не хранится.

Доминирующий паттерн для машин — OIDC. GitHub Actions, GitLab CI и Kubernetes все действуют как OIDC-провайдеры личности: во время выполнения они выпускают подписанный короткоживущий JWT, утверждающий, кто запущен, — репозиторий, ветку, воркфлоу, сервис-аккаунт. Ты регистрируешь этого провайдера в облачном аккаунте один раз и создаёшь роль, чья trust-политика говорит: «доверять токенам от token.actions.githubusercontent.com, но только когда claim sub равен repo:acme/api:ref:refs/heads/main». CI-задача вызывает AssumeRoleWithWebIdentity, предъявляет свой JWT, STS проверяет подпись по опубликованным ключам провайдера и проверяет условия по claim-ам, и возвращает короткоживущие учётки. Никакой секрет в CI никогда не хранился. Утёкший лог воркфлоу содержит, самое большее, уже истекающий токен, привязанный к одному репозиторию и одной ветке.

Та же идея работает внутри Kubernetes как workload identity (EKS IRSA, GKE Workload Identity): сервис-аккаунт пода привязан к облачной роли, kubelet проецирует в под подписанный JWT сервис-аккаунта, и SDK обменивает его на учётки, ограниченные этой нагрузкой, — так что два пода на одном узле получают разные минимальные права вместо общей роли узла. Условие trust-политики в каждом случае делает основную работу: федерация безопасна ровно настолько, насколько плотно ты закрепил claim-ы sub/aud. Trust-политика, доверяющая всей GitHub-организации вместо одного репозитория, или опускающая aud, — это федеративный wildcard — та же ошибка радиуса поражения, что и *:*, просто в OIDC-костюме.

Паттерн доступаХранимый credentialВремя жизниВорота доверияРадиус поражения при утечке
Долгоживущий ключ доступаХранимый секрет (AKIA…)До ротации человекомBearer — нетПолный, бессрочный, откуда угодно
Assume-role (тот же аккаунт)STS session-учёткиTTL 15 мин – 12 чПраво вызывающего + trust-политикаРамки роли, только до TTL
Межаккаунтный assume-roleSTS session-учёткиTTL 15 мин – 12 чTrust-политика + external idРамки роли в твоём аккаунте, до TTL
OIDC-федерация (CI / под)Не хранится — JWT выпускается в рантаймеМинуты (токен + TTL STS)Подпись провайдера + закреп claim sub/audОдин репо/ветка/под, уже истекает

Как рассуждает зрелый инженер

Порядок предпочтений — это лестница, и ты поднимаешься настолько высоко, насколько позволяет платформа. Для своих нагрузок внутри облака привяжи роль к вычислителю (instance profile, IRSA, workload identity), чтобы учётки доставлялись и ротировались автоматически и ключ не существовал. Для CI и внешних-но-федерируемых систем используй OIDC с плотно закреплёнными условиями по claim-ам. Для третьих сторон, которых нельзя федерировать, используй межаккаунтный assume-role с external id. Статический ключ доступа — нижняя ступень, оправданная, только когда ничего выше недоступно, и тогда он обязан быть короткого TTL, узко ограниченным, с алертом на использование и ротируемым по расписанию. Каждый шаг вверх по лестнице убирает хранимый секрет, сокращает время жизни или ужесточает условие доверия — и в этом вся игра: уменьшить, что даёт утечка и сколько она длится.

Выбери лучший вариант

Твоему GitHub Actions пайплайну нужно деплоить в продакшен-аккаунт облака. Сегодня он использует долгоживущий ключ доступа, хранимый в CI-секретах. Выбери лучшую замену.

Викторина

У роли в аккаунте B есть политика прав, дающая s3:*, но её trust-политика не перечисляет ни одного принципала. Пользователь в аккаунте A с правом sts:AssumeRole пытается её принять. Что произойдёт?

Викторина

Почему OIDC-федерация для CI устраняет риск утечки ключа, который есть у хранимого долгоживущего ключа?

Расставь шаги по порядку

Упорядочи шаги OIDC-федеративного CI-деплоя, от старта до выданных учёток:

  1. 1 Один раз зарегистрировать CI-провайдера и создать роль, чья trust-политика закрепляет claim-ы sub/aud
  2. 2 В рантайме CI-задача выпускает подписанный короткоживущий OIDC JWT, утверждающий репо + ветку
  3. 3 Задача вызывает AssumeRoleWithWebIdentity, предъявляя JWT
  4. 4 STS проверяет подпись по ключам провайдера и проверяет условия по claim-ам
  5. 5 STS возвращает короткоживущие учётки в рамках роли; сборка деплоит
Вспомните перед уходом
  1. 01
    Объясни, как работает AssumeRole, какие две политики участвуют и почему возвращаемые им учётки безопаснее долгоживущего ключа доступа.
  2. 02
    Что такое проблема запутанного помощника в межаккаунтном assume-role и как external id и условия по OIDC-claim-ам от неё защищают?
Итог

Долгоживущие ключи доступа — самый эксфильтруемый облачный credential, потому что они никогда не истекают, они bearer-only и работают откуда угодно: утечка в 3 часа ночи всё ещё валидна к полудню. Принятие роли заменяет их временными STS-учётками, ограниченными задачей и истекающими по заданному тобой TTL (от 15 минут до 12 часов). AssumeRole — двустороннее рукопожатие: вызывающему нужно sts:AssumeRole на своей стороне, и он должен быть назван в trust-политике роли, которая отдельна от политики прав, решающей, что роль может делать после принятия. Межаккаунтный доступ работает через доверие другому аккаунту в trust-политике роли, а условие external id закрывает дыру запутанного помощника для третьих сторон, которых нельзя федерировать. Финал — федерация: CI-системы и Kubernetes-нагрузки действуют как OIDC-провайдеры, выпуская короткоживущие подписанные JWT в рантайме, которые STS обменивает на учётки через AssumeRoleWithWebIdentity, — так что никакой секрет никогда не хранится, и утёкший лог держит самое большее уже истекающий токен, закреплённый за одним репо, веткой или подом. Безопасность любой федеративной настройки живёт в том, насколько плотно trust-политика закрепляет claim-ы sub и aud: доверие всей организации вместо одного репо — это федеративный wildcard, та же ошибка радиуса поражения, что и политика :. Рефлекс — лестница: сначала привязанная роль, затем OIDC, затем межаккаунтный доступ с external id, и статический ключ только как оправданный последний вариант с коротким TTL — каждая ступень убирает хранимый секрет, сокращает время жизни или ужесточает условие доверия.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки
углубляется в

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.