ActiveScaleинженерная практика

IAM и resource policy: почему AccessDenied

Эксплуатация iamresource-policys3access-denied403

Коротко. Аккаунт, пользователь и ключ созданы — но S3 отвечает 403 AccessDenied даже на листинг бакета. Причина почти всегда одна: у бакета нет resource policy. Ключи без неё не дают доступа, сколько бы прав ни было у пользователя. Resource policy назначается на бакет через службу IAM (PutResourcePolicy), а не правкой модели и не системным ключом.


1. Две разные вещи: права пользователя и политика ресурса

В ActiveScale доступ к S3 складывается из двух слоёв, и путать их нельзя:

Слой Где живёт Что задаёт
Identity IAM: аккаунты, пользователи, группы, политики кто ты и что тебе разрешено в принципе
Resource policy на конкретном ресурсе (бакете) кто и что может делать с этим бакетом

Ключ доступа — это идентификатор пользователя, а не пропуск к данным. Если на бакете нет политики ресурса, запрос отклоняется независимо от того, что пользователь существует и ключ активен. Отсюда типовой симптом: aws s3 ls отвечает AccessDenied, хотя ключи «только что созданы и точно верные».

Важно: аккаунты и пользователи живут не в модели, а в службе IAM (gRPC). Поэтому /model/account/ не существует, а GET к IAM без параметров отвечает 400 — служба жива и ждёт корректный запрос. Не ищите пользователей в /model/.


2. IAM-домен: обязательное условие

IAM-вызовы работают по FQDN, не по IP-адресу. У S3 и у IAM — разные домены, и это разделение не косметическое: на узле есть карта хостов, которая маршрутизирует запрос либо в S3-шлюз, либо в службу IAM.

Механизм:

  • Конфигурация шлюза содержит отдельный параметр для IAM-домена (в наших сборках — iam_domain_set), а рядом — карту вида «хост → IAM-бэкенд».
  • Если в карту попадает S3-домен, весь HTTPS-трафик с этим Host уходит в IAM. Клиент вместо S3 получает 400 с текстом InvalidAction ... NO_VERSION_SPECIFIED — это ответ IAM-REST, а не S3.
  • Лечится разделением: в карте — только IAM-домен, в параметре IAM — только он же. S3-домена в карте быть не должно.
  • Карта читается при старте шлюза: правка файла без перезапуска службы фронта не действует. Перезапускать нужно только службу фронта, не весь стек.

Проверка различия — по коду ответа на один и тот же путь с разным Host:

curl -sk -o /dev/null -w '%{http_code}\n' https://s3.<DOMAIN>/
curl -sk -o /dev/null -w '%{http_code}\n' https://iam.<DOMAIN>/

Ожидается: S3-хост → 307 (штатный редирект фронта), IAM-хост → 400 (ответ IAM на пустой запрос). Если S3-хост отдаёт 400 InvalidAction — домен попал в IAM-карту.


3. Resource policy на бакете

Назначается вендорским путём через службу IAM. В инженерной консоли это один вызов:

iam = cli._connection_factory.create_iamd_connection()
iam.PutResourcePolicy(resourceArn='arn:aws:s3:::<BUCKET>',
                      resourcePolicyDocument=<json>, expectedResourceId='')

Три детали, на которых спотыкаются:

  1. Имя поля — именно resourcePolicyDocument, не policy и не document.
  2. expectedResourceId передаётся пустой строкой при создании политики. Семантика непустого значения в нашей практике не проверялась — уточнять в документации версии, а не угадывать.
  3. Пустой ответ {} — это успех, а не ошибка. Не ждите текста «OK».

Документ — JSON в языке политик AWS (иллюстрация структуры, не дословный шаблон вендора; готовый документ брать из документации версии или у поддержки):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::<ACCOUNT_ID>:user/<USER>"},
      "Action": ["s3:ListBucket", "s3:GetObject", "s3:PutObject"],
      "Resource": ["arn:aws:s3:::<BUCKET>", "arn:aws:s3:::<BUCKET>/*"]
    }
  ]
}

Проверять после выдачи надо не «настройку», а реальный доступ: листинг и round-trip объекта. Вендорский способ без установки клиента — cli.s3_connect('<ACCESS_KEY>') с validate=False, затем PUT и GET пробного объекта. Подробнее — CLI sherpa.

Чего не делать. Обходить политику системным ключом служебного бакета — это не ремонт: такой доступ не переживёт следующую проверку и ломает картину прав. Служебный бакет по своей политике разрешает только запись (PutObject); GET/DELETE к нему отвечают 403 штатно.


4. Условия и переменные политик (с 7.1)

С версии 7.1 в IAM появились conditions и переменные политик:

  • условия ограничивают доступ по IP-адресу источника, по времени, по тиру Glacier и по длительности Object Lock;
  • переменные работают как подстановки — например, префикс бакета может быть равен имени пользователя, и один шаблон политики обслуживает много пользователей.

Практический смысл: одна политика на бакет вместо ручной политики на каждого пользователя, и ограничение доступа «только из нужной сети» без отдельного шлюза.

Ограничения, которые надо учитывать:

  • Условия по IP работают только при FQDN-доступе; запрос «по IP» в IAM не проходит в принципе.
  • Условия по времени чувствительны к часам: расхождение времени на узле ломает и подпись, и оконные условия.
  • Тир Glacier в условиях имеет смысл только при настроенном холодном хранении и lifecycle.

Если политика с условиями «не срабатывает», проверять надо три вещи по порядку: домен (FQDN), время на узле и фактический Host в запросе. Это разные слои, и симптом у них один — 403.


5. Порядок выдачи доступа

Штатная последовательность для нового пользователя:

  1. CreateAccount — отдельный аккаунт (лимит ключей у системного аккаунта — 2, поэтому пользователей разносят по аккаунтам, а не лепят ключи на системный).
  2. CreateUser — пользователь в этом аккаунте.
  3. CreateAccessKey — пара ключей; секрет показывается один раз, хранить вне репозитория.
  4. PutResourcePolicy — политика на бакет с этим пользователем в Principal.

Через вендорский CLI прикладного уровня это выглядит короче: ascli s3 account access-key generate, ascli account quota show (квота аккаунта). ascli — отдельный инструмент, не замена sherpa: см. CLI sherpa.

Для проверки списка аккаунтов и пользователей в консоли:

from google.protobuf.json_format import MessageToDict
iam = cli._connection_factory.create_iamd_connection()
MessageToDict(iam.ListAccounts(marker='', onlyIfQuotaExceeded=False,
                               nameOrEmailRegex='', maxItems=100, allowDisabled=True))

ListUsers использует oneof: передаётся ровно одно из accountId / accountCanonicalId / accountEmail, остальные — пустыми. Ошибка про missing oneof arguments — подсказка, какие поля заполнить.


6. Проверка и профилактика

Признаки, что всё сделано правильно:

  • листинг бакета с клиентским ключом → 200 и список объектов;
  • PUT/GET пробного объекта → 200, тело совпадает;
  • S3-хост → 307, IAM-хост → 400 (разные домены, разные ответы);
  • в событиях нет IAMD_HEALTH_FAILURE в момент проверки.

Профилактика:

  1. Политика — часть приёмки бакета, а не «допишем потом». Новый бакет без resource policy выглядит как сломанный S3.
  2. Один пользователь — один аккаунт, если нужны независимые ключи и квоты.
  3. Ключи не переиспользовать между сервисами: отдельная пара на интеграцию упрощает отзыв.
  4. Домены S3 и IAM не смешивать; после любой правки карты доменов — перезапуск службы фронта и проверка обоих хостов.
  5. NTP держать в норме: 403 из-за времени выглядит как проблема прав, но лечится часами.
  6. MFA для администраторов (с 7.2) — для UI, CLI и REST; это не заменяет resource policy, но закрывает доступ к управлению.

7. Источники

  • Quantum Software Documentation Center (7.3) (2026-11-07)
  • Release Notes 7.3 — IAM conditions/variables (7.1), IAM на scaled-out нодах и удаление последнего IAM-домена (7.2), MFA (7.2)
  • ActiveScale Z200 datasheet — IAM, квоты аккаунтов
  • Наша практика (обезличенно): PutResourcePolicy через IAM gRPC, разделение S3/IAM-доменов и проверка round-trip на кластере из трёх нод, ActiveScale 7.3.1.2.
  • Обновлено: 2026-11-07.

Нужен инженер? Разберу вашу ситуацию и скажу, что делать — по шагам, с проверками.

Обсудить задачу