電気ひつじ牧場

技術メモ

AWS SESで信頼性の高いメール送信(SPF, DKIM, DMARC) with Terraform

メール認証の仕組みと、SESでのTerraformを使った設定方法について紹介します。

メール認証の種類

メールでは送信元のなりすましを検出するための認証の仕組みとして、主に以下の3つがあります。それぞれRFCで定められています。

  • SPF(Sender Policy Framework)
  • DKIM(DomainKeys Identified Mail)
  • DMARC(Domain-based Message Authentication, Reporting, and Conformance)

メールメッセージ

SMTPで送信されるメールオブジェクトの構造は次のようになっています。

  • envelope: 送信元アドレス(MAIL FROM)、受信者アドレスなどを含むセクション
  • content
    • header: 送受信者、cc, bccなどを含むセクション
    • body: メール本文を含むセクション

envelopeとcontentの2つのセクションがあり、さらにcontentはheaderとbodyに分けられます。ここで着目すべきなのは、envelopeとcontent headerの両方に送信元を表す情報があることです。

MAIL FROM

envelopeに記載される送信元の情報です。ここにはエラーレポートのために使われるメールボックスのアドレスが記載されます。 Return-Path, envelope fromなどの別名があります。

FROM

content headerに記載される送信元の情報です。ここには送信者のメールボックスのアドレスが記載されます。

SPF(Sender Policy Framework)

送信元ホストを認証するための仕組みです。RFC7208に記載があります。

ドメインの所有者は、どのホストからメールが送信できるかを表すSPFレコード(TXTレコード)をDNSサーバに登録します。メールを受信したメールサーバは、MAIL FROMのドメイン名を使ってSPFレコードを問い合わせ、メールが正規のサーバから送信されたものかをチェックします。

DKIM(DomainKeys Identified Mail)

送信元ホストの認証、メッセージの改竄検出をするための仕組みです。RFC6376に記載があります。 DKIMでは、送信側サーバがメールのハッシュ値に対して署名を付けます。検証鍵(公開鍵)はDNSのレコードとして公開し、受信側がその鍵を使ってメールの検証をすることで、ドメインのなりすましと改ざんを検出することができます。

DMARC

SPFDKIMで認証失敗したメールに対するハンドリングポリシーやレポーティングを提供する仕組みです。RFC7489に記載があります。

送信側のドメイン所有者は、SPFDKIMで認証失敗したメールに対するハンドリングポリシー、それらのレポーティングメールを送信する宛先メールアドレスをDNSのレコードとして公開します。受信側はドメインごとに認証失敗時の動作を決定することができて、送信側はSPFDKIMの効果を知ることができるといったメリットがあります。

SESの設定

SESで利用するドメイン認証

これら3つをTerraformで設定していきます。

それらを行う前に、まずSESで使う送信元メールアドレスのドメインを認証する必要があります。これを行わない限りSESでメールを送信することはできません。 個別で送信元メールアドレスを認証することもできますが、ドメインレベルで認証することで、そのドメインを持つすべてのメールアドレスから送信が可能になります。

送信元ドメインには別途取得したblacksheep.linkを利用しています。

resource "aws_ses_domain_identity" "this" {
  domain = var.domain_name
}

resource "aws_route53_record" "verify" {
  zone_id = var.zone_id
  name    = "_amazonses.${var.domain_name}"
  type    = "TXT"
  ttl     = "600"
  records = [aws_ses_domain_identity.this.verification_token]
}

実はこの状態で既にSPF, DKIMに合格します。試しにAWSコンソールにあるSend test emailからメールを送信してみます。(SESがSandbox modeの場合はこの時の送信先メールアドレスは認証されている必要があります)

f:id:cha-shu00:20220306160034p:plain

画像はGmailに送信したメールのソースを表示してみた結果です。SPF, DKIM共にPASSとなっています。これは、SESが気を利かせてamazonses.comのDNSレコードに必要な情報を登録しているからだと推測できます。

認証結果を表すヘッダは次のようになっていました。自ドメインに対するDKIMは鍵を登録していないため失敗し、amazonses.comに対するDKIMは成功しています。SPFap-northeast-1.amazonses.comドメインにおいて成功しています。

Authentication-Results: mx.google.com;
    dkim=temperror (no key for signature) header.i=@blacksheep.link header.s=ifivvwdq3hqh3nh5uxmqyauex25puggo header.b=XECBGecj;
    dkim=pass header.i=@amazonses.com header.s=wf7ez2pjvcsodozkoqksj277kza7wu47 header.b=NcJ3gCOn;
    spf=pass (google.com: domain of 0106017f5e045354-a5a80a0d-8064-4d3b-b44c-245d73ef7c3b-000000@ap-northeast-1.amazonses.com designates 23.251.234.8 as permitted sender) smtp.mailfrom=0106017f5e045354-a5a80a0d-8064-4d3b-b44c-245d73ef7c3b-000000@ap-northeast-1.amazonses.com

ただし、このままではDMARCを自ドメインに対して設定することはできません。

DKIM設定

ドメインに対してDKIMの設定をします。DKIMの設定もidentityとそれに対応するDNSのレコードになります。 dkim_tokensがapply後にしか取得できない値のため、まずはaws_ses_domain_dkimだけapplyし、その後aws_route53_recordコメントアウトを外して実行する必要があります。

resource "aws_ses_domain_dkim" "this" {
  domain = aws_ses_domain_identity.this.domain
}

# resource "aws_route53_record" "dkim" {
#   for_each = toset(aws_ses_domain_dkim.this.dkim_tokens)

#   zone_id = var.zone_id
#   name    = "${each.value}._domainkey.${var.domain_name}"
#   type    = "CNAME"
#   ttl     = "600"
#   records = ["${each.value}.dkim.amazonses.com"]
# }

この状態でメールを再度送信すると、DKIMドメインblacksheep.linkとなっているのが分かります。

f:id:cha-shu00:20220306163153p:plain

DMARC with DKIM

DMARCはDKIM, SPF, またはその両方の情報を使って設定することができます。最低限DKIMSPFのどちらか片方の設定が自ドメインに対して適切に行われていないと準拠することはできません。先ほどDKIMを自ドメインに対して設定したので、このセクションではDKIMを使ったDMARCが有効になります。

DMARCの設定は_dmarc.blacksheep.linkに対してTXTレコードを1つ追加するだけです。 レコードには;で区切られた情報が入っており、p=で認証失敗時のハンドリング方法(none=何もしない, quarantine=スパムフォルダ行き, reject=受信拒否)でruaでレポートの送信先アドレスを指定します。他にもpct=で DMARCポリシーを適応する割合を指定できたりします。

resource "aws_route53_record" "dmarc" {
  zone_id = var.zone_id
  name    = "_dmarc.${aws_ses_domain_identity.this.domain}"
  type    = "TXT"
  ttl     = "60"
  records = ["v=DMARC1;p=none;rua=mailto:dmarc-reports@${aws_ses_domain_identity.this.domain}"]
}

この状態でメールを送信してみると、DMARCがPASSになっているのが分かります(DNSの設定から数分待たないと有効にならない時があるようです)。

f:id:cha-shu00:20220306174117p:plain

DMARC with SPF

さて、ここまででSPF, DKIM, DMARCを有効にできました。このままでも問題はないのですが、SPFを使ったDMARCの準拠も紹介しておきます。

まずはSPFチェックが自ドメインに対して通るように設定します。SPFではMAIL FROMを利用して送信元の検証を行うため、カスタムのMAIL FROMを設定します。検証されたドメインaws_ses_domain_identityで設定したドメイン)のサブドメインである必要があります。

resource "aws_ses_domain_mail_from" "this" {
  domain           = aws_ses_domain_identity.this.domain
  mail_from_domain = "bounce.${aws_ses_domain_identity.this.domain}"
}

resource "aws_route53_record" "mail_from_mx" {
  zone_id = var.zone_id
  name    = aws_ses_domain_mail_from.this.mail_from_domain
  type    = "MX"
  ttl     = "600"
  records = ["10 feedback-smtp.ap-northeast-1.amazonses.com"]
}

続いてカスタムのMAIL FROMドメインに対してSPFを設定します。1つ注意点として、docomoauといったキャリアは独自のなりすまし対策としてMAIL FROMと共にFROMのドメインをチェックするようです。そのため、FROMに設定しているaws_ses_domain_identity.this.domainに対してもSPFのTXTレコードを登録しています。実際に送信元メールサーバのIPアドレス情報を持っているのはamazonses.comのゾーンにあるため、いずれもincludeを使って解決を委譲しています。

resource "aws_route53_record" "spf" {
  zone_id = var.zone_id
  name    = aws_ses_domain_mail_from.this.mail_from_domain
  type    = "TXT"
  ttl     = "600"
  records = ["v=spf1 include:amazonses.com ~all"]
}

# ヘッダFROMのドメインもSPFレコードに登録するのは、キャリアメールが届くようにするため
# https://qiita.com/shouta-dev/items/a33c55e0df154012c557
resource "aws_route53_record" "spf_career" {
  zone_id = var.zone_id
  name    = aws_ses_domain_identity.this.domain
  type    = "TXT"
  ttl     = "600"
  records = ["v=spf1 include:amazonses.com ~all"]
}

これでSPFを使ったDMARCが有効になりました。SPF, DKIM, DMARCをすべてパスしたメールのAuthentication-Resultsは次のようになります。

Authentication-Results: mx.google.com;
    dkim=pass header.i=@blacksheep.link header.s=ifivvwdq3hqh3nh5uxmqyauex25puggo header.b=XC7mjg2c;
    dkim=pass header.i=@amazonses.com header.s=wf7ez2pjvcsodozkoqksj277kza7wu47 header.b=QHVEVzHZ;
    spf=pass (google.com: domain of 0106017f5e4de53b-0911b2ea-3b0f-49c9-988b-636be36684cd-000000@bounce.blacksheep.link designates 23.251.234.9 as permitted sender) smtp.mailfrom=0106017f5e4de53b-0911b2ea-3b0f-49c9-988b-636be36684cd-000000@bounce.blacksheep.link;
    dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=blacksheep.link

参考