
こんにちは。インフラエンジニアの小林です。
本日は、AWS SES(Simple Email Service)でアカウントをまたいだメールを送信した時に発生したハマりごとについて共有いたします。
はじめに
AWS SES(Simple Email Service)でクロスアカウントメール送信を構築した際、2つの AccessDenied エラーに遭遇しました。どちらもドキュメントを読んだだけでは気づきにくく、解決に大変時間が掛かる問題でした。
本記事では、以下の構成でクロスアカウント SES 送信を実装する際に遭遇した問題と解決策を共有します。
- IAM ポリシーの Resource 指定の不一致 — SES が IAM 評価時に組み立てるリソース ARN が、直感に反する形式になる問題
- Sandbox モードの適用範囲の誤解 — SES ドメインを管理するアカウントではなく、API 呼び出し元アカウントに Sandbox 制限が適用される問題
対象読者:
- クロスアカウントで SES メール送信を構築するエンジニア
- SES の Sending Authorization を初めて使う方
- IAM ポリシーで AccessDenied に悩んでいる方
構成の概要

- アカウント A(ECS): ECS 上のアプリケーションがメールを送信する
- アカウント B(SES): SES でドメイン identity(
example.com)を管理している - アプリケーションは
SendRawEmailAPI のsource_arnパラメータでアカウント B(SES)の identity を指定 - アカウント B(SES)の SES Identity Policy(Sending Authorization)でクロスアカウント送信を許可済み
落とし穴 1: IAM ポリシーの Resource が一致しない
発生したエラー
IAM ポリシーと SES Identity Policy を設定してメール送信をテストしたところ、以下のエラーが返されました。
User `arn:aws:sts::111111111111:assumed-role/my-ecs-task-role/...' is not authorized to perform `ses:SendRawEmail' on resource `arn:aws:ses:ap-northeast-1:111111111111:identity/noreply@example.com'
ここで注目すべきは、エラーメッセージに含まれるリソース ARN です。
source_arnで指定したのは:arn:aws:ses:ap-northeast-1:222222222222:identity/example.com(アカウント B(SES)のドメイン)- エラーに表示されたのは:
arn:aws:ses:ap-northeast-1:111111111111:identity/noreply@example.com(アカウント A(ECS)のメールアドレス)
アカウント ID も identity の種類も全く異なっています。
原因
SES は IAM ポリシーの権限チェック時、独自のルールでリソース ARN を組み立てます。
arn:aws:ses:{REGION}:{呼び出し元アカウントID}:identity/{Fromメールアドレス}
つまり:
source_arnパラメータは SES Identity Policy(Sending Authorization)の評価にのみ使われる- IAM ポリシーの評価には
source_arnは使われない - IAM 評価では、SES が自動的に呼び出し元アカウント ID と From メールアドレスからリソース ARN を組み立てる
これにより、IAM ポリシーの Resource に設定していた値と、SES が IAM に渡す実際のリソースが以下のように不一致になっていました。
IAM ポリシーの Resource(設定値):
arn:aws:ses:ap-northeast-1:222222222222:identity/example.com
^^^^^^^^^^^^ ^^^^^^^^^^^
アカウント B(SES) ドメイン identity
SES が IAM に渡す実際のリソース:
arn:aws:ses:ap-northeast-1:111111111111:identity/noreply@example.com
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
アカウント A(ECS) メールアドレス identity
アカウント ID、identity 種別(ドメイン vs メールアドレス)、identity 名のすべてが不一致でした。
裏付け
この挙動は AWS ドキュメントからも確認できます。
- AWS ドキュメント「Identity and access management in Amazon SES」の IAM ポリシー例はすべて
"Resource": "*"を使用しています。これは、SES が IAM 評価時に組み立てるリソース ARN が直感的でないため、ワイルドカードを使わざるを得ないことを示唆しています。 source_arnの API リファレンスには "This parameter is used only for sending authorization"(このパラメータは Sending Authorization にのみ使用される)と明記されており、IAM 評価とは無関係であることが裏付けられます。
解決策
IAM ポリシーの Resource に、呼び出し元アカウント(アカウント A(ECS))の identity をワイルドカードで指定します。
Terraform での修正例
# ECS タスクロールの IAM ポリシー resource "aws_iam_role_policy" "ecs_ses_send" { name = "ses-send-email" role = aws_iam_role.ecs_task_role.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "ses:SendEmail", "ses:SendRawEmail" ] # SES が IAM 評価時に使用するリソース ARN は # 呼び出し元アカウントの identity になるため、 # アカウント A(ECS)の identity をワイルドカードで指定 Resource = [ "arn:aws:ses:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:identity/*" ] # セキュリティは Condition で担保する Condition = { # 注意: ForAllValues は対象キーが空(Recipients なし)の場合 true を返すため、 # SES の API 仕様上 Recipients が必ず存在する前提で使用しています。 # より厳密にしたい場合は StringLike に変更してください。 "ForAllValues:StringLike" = { "ses:Recipients" = [ "*@example.com", "*@corp.example.com" ] } } } ] }) }
ポイント:
- Resource はアカウント A(ECS)の identity をワイルドカードで指定(identity/*)
- セキュリティは Condition ブロックの ses:Recipients で宛先を制限することで担保
- AWS ドキュメントの例に合わせて Resource: "*" とするパターンも可能(Condition で十分に制限されていれば同等のセキュリティ)
落とし穴 2: Sandbox モードが呼び出し元にも適用される
発生したエラー
IAM ポリシーを修正して AccessDenied は解消されましたが、今度は別のエラーが発生しました。
Email address is not verified. The following identities failed the check in region AP-NORTHEAST-1: noreply@example.com
アカウント B(SES)では SES ドメイン identity が検証済みで、本番モード(Sandbox 解除済み)です。にもかかわらず、「メールアドレスが検証されていない」というエラーが出ました。
原因
SES の Sandbox 制限は API 呼び出し元アカウントに適用されます。
アカウント A(ECS)はこれまで SES を直接使っていなかったため、SES が Sandbox モードのままでした。Sandbox モードでは、FROM アドレスと TO アドレスの両方が呼び出し元アカウントで検証された identity である必要があります。

アカウント B(SES)が本番モードかどうかは関係なく、API を呼び出すアカウント A(ECS)側の Sandbox 制限が適用されるのです。
解決策
2つのアプローチがあります。今回は stage 環境での事象だったため、Sandbox 解除の承認を待たずに即座に対応できるアプローチ 2 を採用しました。
アプローチ 1: アカウント A(ECS)で SES の本番モードをリクエスト
AWS コンソールまたは API でアカウント A(ECS)の SES Sandbox 解除をリクエストします。承認されれば Sandbox 制限がなくなり、任意のアドレスにメール送信可能になります。本番環境ではこちらが推奨です。
アプローチ 2: アカウント A(ECS)でもドメイン identity を作成・検証(今回採用)
stage 環境では Sandbox 解除の承認プロセスを待つよりも、呼び出し元アカウントにドメイン identity を作成・検証する方が迅速に対応できます。アカウント A(ECS)にもドメイン identity を作成して検証します。
# アカウント A(ECS)で SES ドメイン identity を作成 resource "aws_ses_domain_identity" "sender" { domain = "example.com" } # DKIM 設定 resource "aws_ses_domain_dkim" "sender" { domain = aws_ses_domain_identity.sender.domain } # ドメイン検証用 DNS レコード resource "aws_route53_record" "ses_verification" { zone_id = data.aws_route53_zone.main.zone_id name = "_amazonses.example.com" type = "TXT" ttl = 600 records = [aws_ses_domain_identity.sender.verification_token] } # DKIM 用 DNS レコード resource "aws_route53_record" "ses_dkim" { count = length(aws_ses_domain_dkim.sender.dkim_tokens) zone_id = data.aws_route53_zone.main.zone_id name = "${aws_ses_domain_dkim.sender.dkim_tokens[count.index]}._domainkey.example.com" type = "CNAME" ttl = 600 records = ["${aws_ses_domain_dkim.sender.dkim_tokens[count.index]}.dkim.amazonses.com"] }
DNS レコードの共存について:
アカウント A(ECS)とアカウント B(SES)でそれぞれ SES ドメイン identity を作成すると、_amazonses.example.com の TXT レコードに2つの検証トークンが必要になります。Route 53 の TXT レコードは複数値を設定できるため、両方のトークンを含めれば共存可能です。
resource "aws_route53_record" "ses_verification" { zone_id = data.aws_route53_zone.main.zone_id name = "_amazonses.example.com" type = "TXT" ttl = 600 records = [ aws_ses_domain_identity.account_a.verification_token, # アカウント A(ECS)用 "existing-token-from-account-b" # アカウント B(SES)用(既存) ] }
クロスアカウント SES の権限レイヤーまとめ
クロスアカウント SES 送信では、以下の 3つの権限レイヤーをすべて通過する必要があります。

| レイヤー | 評価されるアカウント | source_arn の扱い |
|---|---|---|
| IAM ポリシー | アカウント A(ECS) | 使われない |
| SES Identity Policy | アカウント B(SES) | 評価に使用 |
| SES Sandbox | アカウント A(ECS) | 関係なし |
おわりに
教訓
- SES の IAM 評価は
source_arnを無視する — IAM ポリシーの Resource にはアカウント B(SES)の ARN を書いても意味がなく、SES が自動組立するアカウント A(ECS)の identity ARN にマッチさせる必要がある - Sandbox 制限は API 呼び出し元アカウントに適用される — SES ドメインを管理するアカウント B(SES)が本番モードでも、呼び出し元のアカウント A(ECS)が Sandbox なら制限を受ける
- エラーメッセージをよく読む — エラーに含まれる ARN が
source_arnと異なることに気づけば、原因の手がかりになる
参考リンク
- Identity and access management in Amazon SES — IAM ポリシーの例がすべて
Resource: "*"を使っている理由がわかる - Sending authorization overview — Sending Authorization(Identity Policy)の仕組み
- Moving out of the Amazon SES sandbox — Sandbox 解除の手順
- SendRawEmail API Reference —
source_arnパラメータの説明