AWS SES サンドボックス解除とドメイン認証(DMARC)2026年版

|
AWS SES DKIM SPF DMARC メール Route53 Terraform

AWS SESサンドボックス解除からDKIM・SPF・DMARC設定まで、2026年版の完全手順。Terraformによる自動化とRoute53でのDNS設定を含む実践的なメール認証ガイド。

はじめに:2024年以降、メール送信の「前提条件」が変わった

2024年2月、Google と Yahoo が送信者ガイドラインを改定した。1日500通以上を送るドメインには DMARC レコードの設定が必須となり、未対応ドメインからのメールは迷惑メールフォルダへ振り分けられるか、受信拒否される。

問題は、これが「設定を追加すれば良い」だけの話に見えて、実態はそれなりに複雑な点だ。

メール認証の3層構造:

  SPF(Sender Policy Framework)
  └─ 「このドメインからの送信を許可するIPアドレス」をDNSに宣言
     → 偽装元ドメインからの直接送信を防ぐ

  DKIM(DomainKeys Identified Mail)
  └─ メール本文と送信元をデジタル署名で保証
     → 転送時のなりすましや改ざんを検出する

  DMARC(Domain-based Message Authentication)
  └─ SPF/DKIM が両方失敗したときの「処理ポリシー」をDNSに宣言
     → none(監視のみ)/ quarantine(隔離)/ reject(拒否)

  ┌────────────┐   署名検証    ┌─────────────┐
  │ SES 送信   │ ────────────→ │  受信サーバー │
  │ (DKIM付き) │              │ SPF/DKIM確認 │
  └────────────┘              │ DMARC判定   │
                              └─────────────┘

AWS SES でメールを送信するには、この3層をすべて設定した上でサンドボックスを解除する必要がある。この記事では、Terraform による自動化を前提とした完全手順を解説する。


Part 1:AWS SES のサンドボックスとは何か

SES を新規に有効化すると、デフォルトで「サンドボックスモード」になる。

サンドボックスモードの制限:

  ✗ 送信先: 検証済みメールアドレス宛のみ(任意の宛先に送れない)
  ✗ 送信量: 1日200通、1秒1通まで
  ✗ 用途:   開発・テスト専用

本番モード(サンドボックス解除後):

  ✓ 送信先: 任意のメールアドレス宛に送信可能
  ✓ 送信量: デフォルト1日50,000通(上限申請可能)
  ✓ 用途:   本番メール送信(トランザクションメール、通知メール等)

サンドボックス解除は AWS への申請が必要だが、申請前に SPF・DKIM・DMARC を設定しておかないと審査が通りにくい。順番が重要だ。

推奨手順:

  1. ドメインを SES に登録(DKIM 自動生成)
  2. Route53 に SPF・DKIM・DMARC レコードを設定
  3. SES コンソールで検証完了を確認
  4. サンドボックス解除申請
  5. 申請通過後、テスト送信で動作確認

Part 2:Terraform によるリソース構築

手動でのコンソール操作は、DNS レコードの設定漏れや環境間の差異を生む。Terraform で全リソースをコード管理する。

ディレクトリ構成

ses-setup/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

variables.tf

variable "domain_name" {
  description = "SESに登録するドメイン名(例: example.com)"
  type        = string
}

variable "aws_region" {
  description = "SESを設定するAWSリージョン"
  type        = string
  default     = "ap-northeast-1"
}

variable "dmarc_policy" {
  description = "DMARCポリシー(none / quarantine / reject)"
  type        = string
  default     = "quarantine"

  validation {
    condition     = contains(["none", "quarantine", "reject"], var.dmarc_policy)
    error_message = "dmarc_policy は none, quarantine, reject のいずれかを指定してください。"
  }
}

variable "dmarc_rua" {
  description = "DMARCレポートの送信先メールアドレス"
  type        = string
  # 例: "mailto:dmarc-reports@example.com"
}

main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# ── Route53 ホストゾーン(既存を参照)────────────────
data "aws_route53_zone" "main" {
  name         = var.domain_name
  private_zone = false
}

# ── SES ドメイン ID(DKIM自動生成)─────────────────
resource "aws_sesv2_email_identity" "domain" {
  email_identity = var.domain_name

  dkim_signing_attributes {
    next_signing_key_length = "RSA_2048_BIT"
  }
}

# ── DKIM レコード(SESが生成した3件をRoute53に登録)──
resource "aws_route53_record" "dkim" {
  count = 3

  zone_id = data.aws_route53_zone.main.zone_id
  name    = "${aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens[count.index]}._domainkey.${var.domain_name}"
  type    = "CNAME"
  ttl     = 300
  records = ["${aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens[count.index]}.dkim.amazonses.com"]
}

# ── SPF レコード ──────────────────────────────────
resource "aws_route53_record" "spf" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = var.domain_name
  type    = "TXT"
  ttl     = 300

  records = [
    # SES の送信IPを許可、その他は ~all(ソフトフェイル)
    # 本番で厳格にするなら -all(ハードフェイル)に変更
    "v=spf1 include:amazonses.com ~all"
  ]
}

# ── DMARC レコード ────────────────────────────────
resource "aws_route53_record" "dmarc" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "_dmarc.${var.domain_name}"
  type    = "TXT"
  ttl     = 300

  records = [
    # pct=100: 全メールにポリシーを適用
    # rua: 集約レポートの送信先(週次で届く)
    "v=DMARC1; p=${var.dmarc_policy}; pct=100; rua=${var.dmarc_rua}; adkim=s; aspf=s"
  ]
}

# ── MAIL FROM ドメイン(バウンス処理用)─────────────
# 独自の MAIL FROM を設定することで、SPF アライメントが向上する
resource "aws_sesv2_email_identity_mail_from_attributes" "main" {
  email_identity         = aws_sesv2_email_identity.domain.email_identity
  mail_from_domain       = "mail.${var.domain_name}"
  behavior_on_mx_failure = "USE_DEFAULT_VALUE"
}

# MAIL FROM ドメインの MX レコード
resource "aws_route53_record" "mail_from_mx" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "mail.${var.domain_name}"
  type    = "MX"
  ttl     = 300
  records = ["10 feedback-smtp.${var.aws_region}.amazonses.com"]
}

# MAIL FROM ドメインの SPF レコード
resource "aws_route53_record" "mail_from_spf" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "mail.${var.domain_name}"
  type    = "TXT"
  ttl     = 300
  records = ["v=spf1 include:amazonses.com ~all"]
}

outputs.tf

output "ses_identity_arn" {
  description = "SES Email Identity の ARN(IAMポリシーで使用)"
  value       = aws_sesv2_email_identity.domain.arn
}

output "dkim_tokens" {
  description = "DKIMトークン(Route53への登録確認用)"
  value       = aws_sesv2_email_identity.domain.dkim_signing_attributes[0].tokens
}

output "dmarc_record" {
  description = "設定されたDMARCレコードの内容"
  value       = "v=DMARC1; p=${var.dmarc_policy}; pct=100; rua=${var.dmarc_rua}; adkim=s; aspf=s"
}

terraform.tfvars

domain_name  = "example.com"
aws_region   = "ap-northeast-1"
dmarc_policy = "quarantine"
dmarc_rua    = "mailto:dmarc-reports@example.com"

Part 3:DMARC ポリシーの段階的な適用戦略

DMARC を最初から reject に設定するのはリスクがある。送信設定に不備があると正規のメールまで拒否される。段階的なロールアウトが実務上の推奨だ。

DMARCポリシーの3段階ロールアウト:

  フェーズ1:監視(none)
  ─────────────────────────────────────
  設定: p=none; pct=100; rua=mailto:...
  期間: 2〜4週間
  目的: レポートを収集し、正規のメール送信経路を把握
  確認: rua に届く集約レポートで SPF/DKIM の通過率を確認

  フェーズ2:隔離(quarantine)
  ─────────────────────────────────────
  設定: p=quarantine; pct=10; rua=mailto:...
  期間: 2〜4週間
  目的: 10% のみ quarantine に適用し影響を確認
  確認: 正規メールが迷惑メールに入っていないか確認後、pct=100に上げる

  フェーズ3:拒否(reject)
  ─────────────────────────────────────
  設定: p=reject; pct=100; rua=mailto:...
  期間: 恒久運用
  目的: なりすましメールを完全拒否
  注意: 正規の送信経路が全て SPF/DKIM を通過していることを事前確認

Terraform では var.dmarc_policy を変えるだけでポリシーを切り替えられる。フェーズ移行のたびにコンソールを操作する必要はない。

# フェーズ2(quarantine)へ移行
# terraform.tfvars を編集
dmarc_policy = "quarantine"

terraform plan   # 変更差分を確認(TXTレコードが1件 change)
terraform apply  # 適用

Part 4:SES の送信設定と IAM ポリシー

Lambda / アプリケーションからの送信に必要な IAM

# SES 送信専用のIAMポリシー(最小権限)
resource "aws_iam_policy" "ses_send" {
  name        = "ses-send-email-policy"
  description = "Allow sending emails via SES for specific identity"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ses:SendEmail",
          "ses:SendRawEmail"
        ]
        Resource = aws_sesv2_email_identity.domain.arn

        Condition = {
          StringLike = {
            # 送信元アドレスをドメイン単位で制限
            "ses:FromAddress" = "*@${var.domain_name}"
          }
        }
      }
    ]
  })
}

Node.js / TypeScript からの送信例(AWS SDK v3)

import {
  SESv2Client,
  SendEmailCommand,
} from '@aws-sdk/client-sesv2'

const sesClient = new SESv2Client({ region: 'ap-northeast-1' })

interface SendMailOptions {
  to:      string
  subject: string
  html:    string
  text:    string
}

export async function sendMail(opts: SendMailOptions): Promise<void> {
  const command = new SendEmailCommand({
    FromEmailAddress: `noreply@${process.env.DOMAIN_NAME}`,
    Destination: {
      ToAddresses: [opts.to],
    },
    Content: {
      Simple: {
        Subject: {
          Data:    opts.subject,
          Charset: 'UTF-8',
        },
        Body: {
          Html: { Data: opts.html, Charset: 'UTF-8' },
          Text: { Data: opts.text, Charset: 'UTF-8' },
        },
      },
    },
  })

  await sesClient.send(command)
}

Cloudflare Workers / Hono からの送信

Cloudflare Workers 環境では AWS SDK は使えない。代わりに SES の HTTP エンドポイントを直接呼ぶか、AWS Signature Version 4 で署名したリクエストを送る。

// Hono ルート内での SES 送信(Cloudflare Workers 対応)
// AWS SDK の代わりに @aws-sdk/signature-v4 ベースの署名を使う
import { AwsClient } from 'aws4fetch'  // npm: aws4fetch

const aws = new AwsClient({
  accessKeyId:     env.AWS_ACCESS_KEY_ID,
  secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
  region:          'ap-northeast-1',
  service:         'ses',
})

async function sendMailFromEdge(
  to: string,
  subject: string,
  body: string
): Promise<void> {
  const payload = {
    Content: {
      Simple: {
        Subject: { Data: subject, Charset: 'UTF-8' },
        Body:    { Text: { Data: body, Charset: 'UTF-8' } },
      },
    },
    Destination:      { ToAddresses: [to] },
    FromEmailAddress: 'noreply@example.com',
  }

  const response = await aws.fetch(
    'https://email.ap-northeast-1.amazonaws.com/v2/email/outbound-emails',
    {
      method:  'POST',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify(payload),
    }
  )

  if (!response.ok) {
    const error = await response.text()
    throw new Error(`SES send failed: ${error}`)
  }
}

Part 5:サンドボックス解除申請

DNS 設定が完了したら、AWS コンソールから申請する。

申請の流れ:

  1. SES コンソール → Account Dashboard
  2. "Request production access" をクリック
  3. 以下を入力:
     ─ Mail type: Transactional(トランザクション)or Marketing
     ─ Website URL: サービスのURL
     ─ Use case description: 送信目的の説明(詳細に書くほど通りやすい)
     ─ Additional contacts: 連絡先

  申請の記載例(通過率が上がるポイント):
  ─────────────────────────────────────
  "We send transactional emails (contact form confirmations, account
  notifications) for [service name]. All recipients have explicitly
  requested our emails. We have implemented SPF, DKIM, and DMARC
  with p=quarantine policy. Unsubscribe links are included in all
  marketing emails. Bounce and complaint handling is configured via
  SNS notifications."

審査期間は通常 24〜72時間。申請が却下された場合は理由が通知され、再申請できる。

バウンス・苦情の自動処理(必須)

サンドボックス解除後も、バウンス率と苦情率が高いと SES アカウントが停止される。SNS 経由で通知を受け取り、自動処理する仕組みが必要だ。

# バウンス・苦情通知の SNS トピック
resource "aws_sns_topic" "ses_notifications" {
  name = "ses-bounce-complaint-notifications"
}

# SES → SNS の通知設定(バウンス)
resource "aws_sesv2_configuration_set" "main" {
  configuration_set_name = "default"
}

resource "aws_sesv2_configuration_set_event_destination" "bounces" {
  configuration_set_name = aws_sesv2_configuration_set.main.configuration_set_name
  event_destination_name = "bounces-and-complaints"

  event_destination {
    sns_destination {
      topic_arn = aws_sns_topic.ses_notifications.arn
    }

    matching_event_types = [
      "BOUNCE",
      "COMPLAINT",
      "DELIVERY_DELAY",
    ]

    enabled = true
  }
}

Part 6:検証コマンドと運用チェック

DNS レコードの確認

# SPF レコードの確認
dig TXT example.com +short
# → "v=spf1 include:amazonses.com ~all"

# DKIM レコードの確認(SESが生成したトークンを使う)
DKIM_TOKEN="abcdef1234567890abcdef1234567890abcdef12"
dig CNAME "${DKIM_TOKEN}._domainkey.example.com" +short
# → "abcdef1234567890abcdef1234567890abcdef12.dkim.amazonses.com."

# DMARC レコードの確認
dig TXT _dmarc.example.com +short
# → "v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc-reports@example.com; adkim=s; aspf=s"

# MAIL FROM MX レコードの確認
dig MX mail.example.com +short
# → "10 feedback-smtp.ap-northeast-1.amazonses.com."

SES コンソールでの検証ステータス確認

# AWS CLI でドメイン検証ステータスを確認
aws sesv2 get-email-identity \
  --email-identity example.com \
  --query '{
    VerifiedForSendingStatus: VerifiedForSendingStatus,
    DkimStatus: DkimAttributes.Status,
    MailFromStatus: MailFromAttributes.MailFromDomainStatus
  }' \
  --output json

# 期待する出力:
# {
#   "VerifiedForSendingStatus": true,
#   "DkimStatus": "SUCCESS",
#   "MailFromStatus": "SUCCESS"
# }

テスト送信と配信確認

# CLI からテストメール送信
aws sesv2 send-email \
  --from-email-address "noreply@example.com" \
  --destination '{"ToAddresses":["your-test@gmail.com"]}' \
  --content '{
    "Simple": {
      "Subject": {"Data": "SES テスト送信"},
      "Body": {"Text": {"Data": "テスト送信です。DKIM・SPF・DMARC の確認用。"}}
    }
  }'

# 送信統計の確認(過去14日間)
aws sesv2 get-account \
  --query 'SendQuota' \
  --output json

Conclusion:「設定」ではなく「設計」としてメール認証を捉える

SPF・DKIM・DMARC の設定は一度やれば終わりに見えるが、実際には継続的な管理が必要だ。

項目 設定のみの管理 設計(Terraform)による管理
環境の再現性 コンソール操作の記録が残らない terraform plan で差分が見える
ポリシー変更 DNS コンソールを手動編集 var.dmarc_policy を変えて apply
バウンス処理 手動確認・手動削除 SNS → Lambda で自動処理
審査への説明 設定根拠が残らない コード+コメントが証跡になる

2026年時点でのメール送信は、認証なし = 届かない という前提で設計する必要がある。SES のサンドボックス解除は手段であって、その先にある DMARC reject への段階的移行が本当のゴールだ。

DNS レコードを Terraform で管理することで、設定変更の意図と経緯が git log に残る。それはセキュリティ設定における「変更の説明責任」を担保する最も実用的な方法だ。

この記事をシェア

Twitter / X LinkedIn
記事一覧に戻る
🤖
Cloud Assistant
IBM Watson powered

こんにちは!クラウドエンジニアのポートフォリオサイトへようこそ。AWS構成・副業サービス・お仕事のご相談など、何でも聞いてください 👋