ГлавнаяБлогРозетта: как я перестал гадать об AccessDenied в AWS
Алгоритмы

Розетта: как я перестал гадать об AccessDenied в AWS

Разбираем AccessDenied в AWS без гаданий: IAM SimulatePrincipalPolicy, CloudTrail и ранжирование причин. Научитесь находить точную причину за минуты.

Al
Редакция Algolitalgolit.ru
8 мин чтения10 августа 2026 г.

Почему AccessDenied в AWS — это всегда загадка

Каждый, кто работал с AWS, сталкивался с ошибкой AccessDenied. Вы копируете текст ошибки, вставляете его в чат с ИИ и получаете список из пяти возможных причин. Все они выглядят правдоподобно, но ни одна не проверена. Это не ответ, а приглашение к долгому клику по консоли IAM. Я решил эту проблему, создав инструмент Rosetta, который не гадает, а проверяет. В этой статье я расскажу, как он устроен, и покажу, как вы можете использовать его в своих проектах.

Что такое AccessDenied и почему стандартные подходы не работают

Текст ошибки AccessDenied при вызове GetObject может означать множество разных проблем: отсутствие Allow в политике, явный Deny, ограничение SCP, permissions boundary, политика ресурса или отсутствие KMS-ключа. Шесть разных причин — одно сообщение. Ни одна модель ИИ не сможет определить точную причину, потому что в самом тексте просто нет нужной информации. Единственный способ — запросить аккаунт, который выдал ошибку.

Ключевой API: SimulatePrincipalPolicy

AWS предоставляет API iam:SimulatePrincipalPolicy, который оценивает политики точно так же, как это делает IAM во время запроса. Он возвращает решение allow/deny, а также точное выражение, вызвавшее решение, и отдельные флаги для SCP и boundary. Вот как это выглядит в коде:

def simulate(session, principal_arn, action, resource):
    if not (principal_arn and action):
        return []
    iam = session.client('iam')
    resp = iam.simulate_principal_policy(
        PolicySourceArn=principal_arn,
        ActionNames=[action],
        ResourceArns=[resource] if resource and resource.startswith('arn:aws') else ['*'],
    )
    out = []
    for r in resp.get('EvaluationResults', []):
        detail = {
            'action': r.get('EvalActionName'),
            'decision': r.get('EvalDecision'),
            'allowed_by_organizations': r.get('OrganizationsDecisionDetail', {}).get('AllowedByOrganizations'),
            'allowed_by_boundary': r.get('PermissionsBoundaryDecisionDetail', {}).get('AllowedByPermissionsBoundary'),
            'matched_statements': [m.get('SourcePolicyId') for m in r.get('MatchedStatements', [])],
        }
        out.append(Evidence(kind='simulate', source='iam:SimulatePrincipalPolicy', detail=json.dumps(detail), authoritative=True))
    return out

Обратите внимание на поля allowed_by_organizations и allowed_by_boundary. Они позволяют различить, кто именно заблокировал доступ: SCP, permissions boundary или обычная политика. Это три разных исправления, и API даёт их напрямую.

Архитектура Rosetta: четыре шага к точному ответу

Rosetta — это конвейер из четырёх этапов: парсинг ошибки, обогащение через simulate и CloudTrail, опциональное рассуждение через Bedrock и ранжирование причин. Каждый этап — чистая функция, что упрощает тестирование. Главное решение — гибридный подход: текстовый режим не требует никаких прав доступа и работает с любой ошибкой, а обогащённый режим использует read-only роль для проверки.

Регексы для парсинга ошибки

Парсер — это простые регулярные выражения, которые извлекают сервис, операцию, principal и ресурс из текста ошибки. Например:

_CODE = re.compile(r"An error occurred \(([A-Za-z0-9.]+)\)")
_OP = re.compile(r"when calling the (\w+) operation")
_PRINCIPAL = re.compile(r"(?:User|Role):\s*(arn:aws[^\s]+)")
_ACTION = re.compile(r"perform:\s*([a-zA-Z0-9]+:[A-Za-z0-9*]+)")
_RESOURCE = re.compile(r"on resource:\s*(arn:aws[^\s]+|\*|[^\s]+)")

Если парсер ошибётся в извлечении ARN principal, весь последующий анализ будет неверным, поэтому эти регексы — ключевой элемент.

Создание read-only роли для проверки

Для обогащённого режима нужна роль, которую Rosetta может принять. Она должна иметь только права на чтение: simulate, чтение политик, CloudTrail. Пример политики:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RosettaSimulate",
      "Effect": "Allow",
      "Action": [
        "iam:SimulatePrincipalPolicy",
        "iam:GetPolicy",
        "iam:GetPolicyVersion",
        "iam:ListAttachedRolePolicies",
        "iam:ListAttachedUserPolicies"
      ],
      "Resource": "*"
    },
    {
      "Sid": "RosettaResourcePolicies",
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketPolicy",
        "kms:GetKeyPolicy"
      ],
      "Resource": "*"
    },
    {
      "Sid": "RosettaCloudTrail",
      "Effect": "Allow",
      "Action": ["cloudtrail:LookupEvents"],
      "Resource": "*"
    }
  ]
}

Роль ничего не может изменить в аккаунте. Вы передаёте её ARN, Rosetta принимает её через STS, выполняет чтение и завершает сессию.

Как воспроизвести демо самостоятельно

Чтобы не тестировать на реальной инфраструктуре, я создал CloudFormation стек с заведомо сломанным IAM. Пользователь имеет Allow на S3, но явный Deny на один bucket. Это позволяет увидеть, как Rosetta находит причину. Пример шаблона:

VictimUser:
  Type: AWS::IAM::User
  Properties:
    UserName: rosetta-victim
    Policies:
      - PolicyName: rosetta-victim-policy
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Sid: AllowS3Read
              Effect: Allow
              Action: s3:GetObject
              Resource: "*"
            - Sid: DenySecretBucket
              Effect: Deny
              Action: s3:GetObject
              Resource: arn:aws:s3:::rosetta-demo-secret/*

Разверните стек, запустите Rosetta, получите результат, затем удалите стек — и никакого мусора.

Хитрость с CloudTrail: как найти точный запрос

Хотите показать реальный запрос, который был отклонён? CloudTrail не позволяет искать по requestID. Вместо этого ищите по EventName и фильтруйте по requestID внутри события. Вот код:

def cloudtrail_match(session, operation, principal_arn, request_id):
    ct = session.client('cloudtrail')
    resp = ct.lookup_events(
        LookupAttributes=[{"AttributeKey": "EventName", "AttributeValue": operation}],
        MaxResults=20,
    )
    for ev in resp.get('Events', []):
        record = json.loads(ev.get('CloudTrailEvent', '{}'))
        if request_id and record.get('requestID') != request_id:
            continue
        # нашли точный вызов
        ...

Учтите, что CloudTrail имеет задержку — свежая ошибка может не иметь события. Rosetta честно сообщает об этом, а не выдумывает.

Баг, который я нашёл, тестируя свой инструмент

Когда я запустил Rosetta на своём сломанном стеке, она уверенно сказала: «Никакая политика не разрешает это действие (неявный deny)». Но это было не так. Оказалось, что мой код ранжирования неправильно обрабатывал случай, когда simulate возвращает explicitDeny. Я исправил это, добавив приоритет для authoritative evidence. Это напоминание, что даже хорошие инструменты нужно тестировать на реальных сценариях.

Практический вывод: что делать прямо сейчас

Не тратьте время на гадание. Используйте iam:SimulatePrincipalPolicy для проверки. Создайте read-only роль, настройте Rosetta или напишите свой скрипт. Это сэкономит часы отладки и сделает вашу работу с AWS надёжнее.

#AWS#IAM#AccessDenied#диагностика
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →