Глубже в IAM-evaluation, envelope-шифрование KMS и Secrets Manager
IAM разрешается по порядку: дефолтный deny, явный allow даёт, явный deny всегда побеждает; boundaries и SCP — потолок, conditions сужают statement. Предпочитай роли и STS долгоживущим ключам, останавливай confused deputy через ExternalId, шифруй envelope-ключами KMS.
Разработчик коммитит .env в публичный репозиторий на девяносто секунд, прежде чем затереть его force-push’ем. Поздно: боты сканируют новые коммиты GitHub за секунды, и долгоживущий IAM-access-key внутри уже собран. Через минуты этот ключ — на пользователе, чья политика несла удобный "Action": "*", "Resource": "*", — поднимает десятки GPU-инстансов под крипто-майнинг во всех регионах и заодно выкачивает S3-бакет. Счёт переваливает за пятизначную сумму ещё до того, как сработают биллинг-алерты. Ключ никогда не ротировался, потому что не был временным; политика никогда не сужалась, потому что wildcard «просто работал»; и ничто не ограничило радиус поражения, потому что не было ни permissions boundary, ни SCP. Не хватало трёх отдельных защит, а взлому хватило бы одной из них на месте. Этот урок — про все три.
К концу урока ты поймёшь, почему каждая из трёх отсутствующих защит из истории имела значение, и сможешь поставить все три до того, как следующий credential утечёт.
Как на самом деле принимается решение по запросу
С identity-политиками ты уже познакомился в базовом уроке по IAM: principal, action, resource, effect. Сеньорская модель — это evaluation, то, как AWS сводит несколько типов политик в одно «да/нет» для одного API-вызова. Стартовая точка никогда не нейтральна: каждый запрос по умолчанию — неявный deny. Дальше у правила три хода, и порядок фиксирован.
Первый: явный allow в любой применимой политике может перевернуть дефолт в «разрешено». Второй: типы политик комбинируются — identity-based и resource-based образуют объединение (allow в любой из них достаточно в пределах одного аккаунта), тогда как permissions boundaries и Service Control Policies пересекаются: запрос должен быть разрешён всеми применимыми категориями. Третий, перекрывающий всё: явный Deny где угодно всегда побеждает. Deny в identity-политике, resource-политике, boundary или SCP не перебивается никаким числом allow’ов. Это самое важное предложение во всём IAM, и большинство тикетов «почему у меня нет доступа?» упираются в явный deny, про который кто-то забыл.
Типы политик складываются в слои, у каждого своя роль:
- Identity-based политики — привязаны к пользователю, группе или роли; дают то, что может делать этот principal.
- Resource-based политики — привязаны к ресурсу (политика S3-бакета, key policy KMS, политика SQS-очереди); задают, кто может трогать этот ресурс, и уникально включают cross-account-доступ.
- Permissions boundaries — продвинутая identity-политика, задающая максимум прав, который principal вообще может иметь. Сама по себе ничего не даёт; она ограничивает. Эффективные права principal’а — это пересечение его identity-политик и его boundary.
- Service Control Policies (SCP) — общеорганизационные ограждения на уровне AWS Organizations. Тоже ничего не дают; они ограничивают, что могут делать все аккаунты и principal’ы под ними, как бы щедро ни выдавал права локальный админ.
- Session policies — передаются inline при assume роли; они дополнительно сужают права сессии на её время жизни.
Итого: эффективные права = (объединение allow’ов), пересечённое с каждой применимой boundary и каждым применимым SCP, минус любой явный deny. Wildcard-политика из истории давала всё, но без boundary и SCP пересечение никогда не сжималось — потолок был бесконечным, поэтому утёкший ключ унаследовал бесконечный охват. Boundary PowerUserAccess или SCP, запрещающий iam:* вне break-glass-роли и действия вне одобренных регионов, превратили бы катастрофу в локализованное неудобство.
Conditions делают least privilege настоящим
Statement с "Resource": "*" — это риск; statement, суженный Condition, — это least privilege. Condition-ключи проверяют контекст запроса и пропускают statement: aws:SourceIp (только из офисного CIDR), aws:PrincipalOrgID (только principal’ы твоей организации), aws:SecureTransport (только по TLS), aws:RequestedRegion или StringEquals по тегу ресурса (только ресурсы с тегом твоей команды). Statement применяется, только когда его conditions истинны. Эта политика позволяет principal’у читать один бакет, но только по TLS и только из известной сети — убери любое из условий, и запрос будет отклонён:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadReportsTlsAndOffice",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::acme-reports/*",
"Condition": {
"Bool": { "aws:SecureTransport": "true" },
"IpAddress": { "aws:SourceIp": "203.0.113.0/24" }
}
}
]
}Более глубокая привычка, которую дают conditions, — роли вместо долгоживущих ключей. Долгоживущий access-key — это статичный, экспортируемый, часто неротируемый credential, ровно то, что утекло в завязке. IAM-роль выдаёт временные credentials через AWS STS (Security Token Service — сервис временных токенов безопасности): короткоживущие, авто-истекающие, никогда не попадающие в репозиторий, потому что существуют лишь минуты. Любая рабочая нагрузка — EC2-инстанс (через instance profile), Lambda, ECS-таск, CI-джоб (через OIDC) — должна ассьюмить роль, а не носить ключ. Когда нужно проаудитить, что вообще выдано, IAM Access Analyzer помечает resource-политики, дающие доступ внешним аккаунтам, и подсвечивает неиспользуемые права, которые можно срезать.
Cross-account-доступ и confused deputy
Cross-account-доступ строится на ролях: роль в аккаунте B имеет trust policy, называющую аккаунт A допущенным к вызову sts:AssumeRole. Principal’ы аккаунта A ассьюмят роль и действуют в B с правами B. Но называние внешнего аккаунта в trust policy открывает классическую дыру — confused deputy. По AWS, это «проблема безопасности, где сущность, не имеющая прав выполнить действие, может вынудить более привилегированную сущность выполнить его». Если ты нанимаешь сторонний SaaS, который ассьюмит роль в аккаунтах многих клиентов, а твой role ARN не секрет, другой их клиент может передать вендору твой ARN и обманом заставить вендора («deputy») действовать на твоих ресурсах.
Фикс — условие ExternalId в trust policy. Вендор генерирует уникальный ID на клиента и передаёт его в каждом вызове AssumeRole; твоя trust policy требует твой ID, поэтому запрос с чужим ID не проходит:
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::222222222222:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "a1b2c3-unique-per-customer" }
}
}
}Для AWS-сервисных principal’ов в cross-service-сценариях (например, CloudTrail, пишущий в твой бакет) аналогичные ограждения — условия aws:SourceArn и aws:SourceAccount, утверждающие, что сервис действует от имени ресурса или аккаунта, которого ты действительно ждёшь.
KMS: envelope-шифрование и key policies
Зачем шифрование на масштабе требует двух ключей вместо одного? Потому что 5-гигабайтный объект нельзя прогнать через облачный API, а раздать мастер-ключ — значит превратить каждую ротацию в катастрофу. Шифрование на масштабе использует envelope encryption (конверт-шифрование), и KMS построен вокруг него. KMS key (customer-managed key) живёт внутри hardware security module и «спроектирован так, чтобы никогда не экспортироваться из HSM в открытом виде». Байты ключа ты не получаешь никогда. Вместо этого ты вызываешь GenerateDataKey, и KMS возвращает data key в двух формах: открытую копию и копию, зашифрованную под твоим KMS key. Ты шифруешь свой большой payload локально открытым data key, тут же затираешь открытый ключ из памяти и хранишь зашифрованный data key рядом с шифротекстом. Чтобы расшифровать позже, ты отправляешь в KMS только зашифрованный data key, тот разворачивает его и возвращает открытый data key ровно для этой операции. Объёмные данные в KMS не ходят никогда; ходит только маленький data key. Поэтому шифрование S3, EBS и RDS масштабируется — каждый объект получает свой data key, и все они обёрнуты одним центральным KMS key.
Кто может использовать и управлять KMS key, задаёт его key policy — resource-based политика, которая, в отличие от большинства, является первичным контролем доступа к ключу. Опасный footgun: если key policy не даёт доступ root’у твоего аккаунта (или другому административному principal’у), ты можешь навсегда заблокировать всех от ключа — зашифрованные под ним данные становятся невосстановимыми, и даже AWS Support не перекроет политику. Помимо key policy, grants делегируют временное, узко ограниченное использование principal’у (часто AWS-сервису) без правки политики; aliases дают ключу стабильное удобное имя, чтобы ротировать материал, не меняя ссылки; автоматическая ротация (для customer-managed-ключей опциональна, по умолчанию ежегодная и теперь настраиваемая) генерирует новый backing-материал, сохраняя старые версии для расшифровки старых данных; а ключи бывают симметричными (один ключ шифрует и расшифровывает — дефолт для большинства данных) или асимметричными (пара public/private — для подписи или шифрования публичным ключом, который можно раздать). Key policy KMS выглядит так:
{
"Version": "2012-10-17",
"Id": "key-policy-app-data",
"Statement": [
{
"Sid": "EnableRootAccountAdmin",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowAppToEncryptDecrypt",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/app-runtime" },
"Action": ["kms:GenerateDataKey", "kms:Decrypt"],
"Resource": "*",
"Condition": {
"StringEquals": { "kms:ViaService": "s3.us-east-1.amazonaws.com" }
}
}
]
}Убери первый statement — и ключ осиротел.
| Слой | Что делает | Даёт или ограничивает? | Сбой, если отсутствует или неверен |
|---|---|---|---|
| Identity-политика | Что может этот principal | Даёт | Wildcard даёт всё; утёкший ключ это наследует |
| Permissions boundary | Максимум, что principal может иметь | Ограничивает (пересечение) | Нет потолка для радиуса поражения |
| SCP | Общеорганизационное ограждение | Ограничивает (пересечение) | Нет тормоза на уровне org ни для одного аккаунта |
| Явный deny | Перекрывает любой allow | Рубильник | Опасное действие остаётся разрешённым по умолчанию |
| Key policy KMS | Кто использует/управляет ключом | Даёт (resource-based) | Пропусти root аккаунта — осиротишь ключ навсегда |
▸Почему это работает
Зачем хранить зашифрованный data key рядом с данными, а не просто звать KMS зашифровать payload напрямую? Потому что KMS ограничивает размер данных, которые можно ему передать (несколько КБ), и каждый вызов стоит запроса — ты не прогонишь объект на 5 ГБ через KMS. Envelope-шифрование переворачивает поток: KMS всегда работает лишь с крошечным data key, твой собственный CPU делает массовое симметричное шифрование локально на скорости канала, а обёрнутый data key едет вместе с шифротекстом, так что любой обладатель права decrypt на KMS key восстановит его. Один центральный, аудируемый, ротируемый ключ защищает неограниченный объём данных.
Secrets Manager против Parameter Store
Утёкший .env — это более глубокий урок: секреты не место в коде, образах или env-файлах, запечённых в сборку. AWS Secrets Manager хранит секреты, зашифрованные KMS, и — главная фича — поддерживает автоматическую ротацию. Для credential’ов базы он по расписанию запускает Lambda-функцию ротации: функция создаёт новый credential, обновляет базу и переключает staging-метки (AWSPENDING становится AWSCURRENT, старый — AWSPREVIOUS), так что вызывающие всегда читают живое значение без простоя. Он напрямую интегрируется с RDS, Redshift и DocumentDB.
Более дешёвый и простой собрат — SSM Parameter Store. Его параметры SecureString тоже зашифрованы KMS, у него нет помесячной платы за секрет, и он отлично подходит для конфигов плюс секретов, которые не нужно автоматически ротировать. Трейдофф: у Parameter Store нет встроенной ротации и нет нативной интеграции с базами. Сеньорский выбор: Secrets Manager — для credential’ов, которые должны ротироваться (базы, API-ключи с жизненным циклом); Parameter Store — для статичного конфига и редко меняющихся секретов, где важна цена.
Нужно хранить пароль production-базы, который приложение читает на каждом cold start. Комплаенс требует ротировать его каждые 30 дней с нулевым простоем. Выбери хранилище.
Identity-политика разрешает s3:DeleteObject на бакете, но SCP на аккаунте содержит явный Deny для s3:DeleteObject. Что произойдёт, когда principal попробует удалить?
Ты заводишь роль, которую сторонний SaaS ассьюмит в твоём аккаунте, доверяя его AWS-account-ID. Зачем добавлять условие sts:ExternalId в trust policy?
- 01Сформулируй полное правило evaluation IAM, роль каждого типа политики и как считаются эффективные права.
- 02Объясни envelope-шифрование KMS от и до и единственную ошибку key policy, осиротяющую ключ.
Права IAM решаются фиксированным порядком evaluation, а не тем, какая политика выглядит конкретнее. Каждый запрос начинается как неявный deny; явный allow в любой применимой политике может его дать; а явный deny где угодно — в identity-политике, resource-политике, permissions boundary или SCP (Service Control Policy — политика управления сервисами на уровне организации) — всегда побеждает и не перекрывается. Теперь, когда видишь ошибку «Access Denied», а identity-политика явно даёт действие, первый рефлекс — искать явный deny или boundary/SCP, срезающий потолок, а не добавлять ещё один allow. Эффективные права — это объединение allow’ов, пересечённое с каждой применимой boundary и SCP, минус любой явный deny: boundaries и SCP ограничивают потолок, объединение allow’ов заполняет всё под ним, а deny — рубильник. Conditions (aws:SourceIp, aws:PrincipalOrgID, aws:SecureTransport, совпадения по тегам) превращают широкие statement’ы в настоящий least privilege, а роли, выдающие короткоживущие STS-credentials, должны вытеснить долгоживущие access-key’и повсюду — утёкший, неротируемый, wildcard-ключ из истории провалил сразу все три защиты. Cross-account-доступ — это роль, чья trust policy называет другой аккаунт, и confused deputy закрывается через ExternalId (или aws:SourceArn и aws:SourceAccount для сервисных principal’ов). Для данных envelope-шифрование KMS позволяет одному центральному ключу, который никогда не покидает свой HSM, оборачивать per-object data-ключи, так что шифрование масштабируется, ни разу не отправляя массив данных в KMS, — но key policy, пропустившая root аккаунта, осиротит ключ навсегда. Наконец, секреты живут в Secrets Manager, где Lambda-ротация ротирует credential’ы базы без простоя, или в более дешёвом Parameter Store SecureString для статичного, редко меняющегося конфига. Явный deny побеждает, boundaries и SCP ограничивают, conditions сужают, роли бьют ключи, ExternalId останавливает deputy, KMS оборачивает данные, а Secrets Manager держит их свежими.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.