メール認証の仕組みと、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
SPFやDKIMで認証失敗したメールに対するハンドリングポリシーやレポーティングを提供する仕組みです。RFC7489に記載があります。
送信側のドメイン所有者は、SPFやDKIMで認証失敗したメールに対するハンドリングポリシー、それらのレポーティングメールを送信する宛先メールアドレスをDNSのレコードとして公開します。受信側はドメインごとに認証失敗時の動作を決定することができて、送信側はSPFやDKIMの効果を知ることができるといったメリットがあります。
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の場合はこの時の送信先メールアドレスは認証されている必要があります)
画像はGmailに送信したメールのソースを表示してみた結果です。SPF, DKIM共にPASSとなっています。これは、SESが気を利かせてamazonses.comのDNSレコードに必要な情報を登録しているからだと推測できます。
認証結果を表すヘッダは次のようになっていました。自ドメインに対するDKIMは鍵を登録していないため失敗し、amazonses.com
に対するDKIMは成功しています。SPFもap-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
となっているのが分かります。
DMARC with DKIM
DMARCはDKIM, SPF, またはその両方の情報を使って設定することができます。最低限DKIMかSPFのどちらか片方の設定が自ドメインに対して適切に行われていないと準拠することはできません。先ほど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の設定から数分待たないと有効にならない時があるようです)。
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つ注意点として、docomoやauといったキャリアは独自のなりすまし対策として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