1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SPF/DKIM/DMARCが受信側でどのように判定されているか調べてみる

Posted at

「送信したメールが迷惑メールに入ってしまう」「相手に届かない」——そんなときに頼りになるのが Authentication-Results ヘッダです。これは受信側のメールサーバーが付与する"判定ログ"であり、SPF/DKIM/DMARCのどこで失敗したかを最短で特定できます。

本記事では、Authentication-Resultsの読み方を「型」として整理し、結果から修正アクションへ落とし込むまでの流れをコマンドと判定表でまとめます。

ゴールと前提

ゴール

ヘッダ1枚から原因箇所を特定し、修正アクションに落とす

メール認証のトラブルシューティングでは、問題の切り分けに時間がかかりがちです。本記事の手順に沿えば、Authentication-Resultsを見た瞬間に「どこを直せばいいか」が分かるようになります。

送信者がAuthentication-Resultsを確認する意味

blastengineなどのメール配信サービスを利用している場合、気になるのは「自分が送ったメールが相手にどう判定されているか」です。Authentication-Resultsは受信側が付与するヘッダなので、自分宛てにテストメールを送信し、そのヘッダを確認することで、本番配信前に認証状態をチェックできます。

到達率が低い・迷惑メールに入るといった問題の多くは、SPF/DKIM/DMARCの設定不備が原因です。テストメールのAuthentication-Resultsを確認すれば、DNS設定やalignmentの問題を事前に発見できます。

前提

  • Authentication-Resultsは受信側のメールサーバーが付与します
  • 同じメールでも、Gmail・Microsoft 365・Yahoo!メールなど受信サービスによって書式が微妙に異なります
  • 本記事ではRFC 8601に準拠した一般的な書式を基準に解説します

まずはヘッダを取得する

トラブルシューティングの第一歩は「生ヘッダ」の取得です。メール本文ではなく、ヘッダ全文が必要になります。

テストメールを送信する

送信側として認証状態を確認するには、まず自分宛てにテストメールを送ります。

  1. 本番と同じ送信経路でメールを送信(blastengineのAPIやSMTPリレー経由)
  2. 複数の受信サービスで確認するのがベスト(Gmail、Microsoft 365など)
  3. 受信したメールの生ヘッダを取得

受信サービスによって判定が異なる場合があるため、主要な宛先(顧客が多く使っているサービス)でテストしておくと安心です。

なぜ生ヘッダが必要か

通常のメールクライアントでは、From・To・Subjectなど一部のヘッダしか表示されません。しかし、認証結果を確認するには以下のヘッダが必要です。

  • Authentication-Results: 認証判定の結果
  • Received: メールの配送経路
  • DKIM-Signature: DKIM署名の詳細
  • Return-Path: エンベロープFrom

主要サービスでの取得方法

サービス 操作手順
Gmail メールを開く → 右上の三点メニュー → 「原文を表示」
Outlook (Web版) メールを開く → 三点メニュー → 「表示」→「メッセージの詳細を表示」
Outlook (Mac版) メールを右クリック → 「ソースの表示」
Yahoo!メール メールを開く → 上部メニュー→その他→「詳細ヘッダー」をクリック
Apple Mail メールを選択 → 「表示」→「メッセージ」→「すべてのヘッダ」

取得したヘッダの中から Authentication-Results: で始まる行を探します。

Authentication-Resultsの読み方

Authentication-Resultsは情報量が多いですが、読む順番を固定すれば迷いません。以下の「型」で読みましょう。

読む順番(型)

Authentication-Results: mx.google.com;        ← ① authserv-id
       dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com;  ← ② dmarc
       dkim=pass header.i=@example.com header.s=selector1 header.b=xxxxx;  ← ③ dkim
       spf=pass (google.com: ... ) smtp.mailfrom=bounce@example.com        ← ④ spf
順番 フィールド 確認ポイント
authserv-id どのサーバーが判定したか(mx.google.com など)
dmarc= 最終結論。pass/fail/none のいずれか
dkim= 署名検証の結果。header.d(署名ドメイン)と header.s(セレクタ)も確認
spf= 送信元IPの正当性。smtp.mailfrom(エンベロープFrom)も確認

判定結果の種類

結果 意味
pass 認証成功
fail 認証失敗(明示的に拒否される可能性あり)
softfail 認証失敗だが、受信側の判断に委ねる(SPFで多用)
neutral 認証情報なし、または判定を明言しない
none レコードが存在しない
temperror 一時的なエラー(DNS応答タイムアウトなど)
permerror 永続的なエラー(レコードの文法エラーなど)

実例1: オールpass(正常系の基準)

まず正常なAuthentication-Resultsを確認し、以降の異常例と比較する基準にします。

Authentication-Results: mx.google.com;
       dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com;
       dkim=pass header.i=@example.com header.s=selector1 header.b=aBcDe123;
       spf=pass (google.com: domain of info@example.com designates 203.0.113.10 as permitted sender)
           smtp.mailfrom=info@example.com

チェックポイント

項目 判断
dmarc pass SPFまたはDKIMがpassし、かつalignment(ドメイン一致)もOK
dkim pass 署名が有効。header.d=example.com がFromドメインと一致
spf pass 送信元IP 203.0.113.10 がSPFレコードで許可されている
header.from example.com メールのFromヘッダのドメイン
smtp.mailfrom info@example.com エンベロープFromのドメイン(example.com)

すべてpassかつドメインが揃っている——これが正常な状態です。

実例2: DKIM failのときにやること

Authentication-Results: mx.google.com;
       dmarc=fail (p=NONE sp=NONE dis=NONE) header.from=example.com;
       dkim=fail (signature verification failed) header.i=@example.com header.s=selector1;
       spf=pass smtp.mailfrom=info@example.com

dkim=fail が発生しています。原因を特定していきましょう。

Step 1: DKIM-Signatureヘッダを確認

同じメールの DKIM-Signature ヘッダを探します。

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=example.com; s=selector1;
        h=from:to:subject:date:message-id;
        bh=xxxxxx; b=yyyyyy

確認すべき値は以下の通りです。

  • d=: 署名ドメイン(example.com)
  • s=: セレクタ(selector1)
  • h=: 署名対象ヘッダ

Step 2: DNSでDKIMレコードを確認

dig selector1._domainkey.example.com TXT +short

期待される出力は以下の通りです。

"v=DKIM1; k=rsa; p=MIIBIjANBgkq..."

よくある問題

  • レコードが存在しない → セレクタ名の設定ミス
  • p= が空 → 鍵が無効化されている
  • 公開鍵の値が送信サーバーの秘密鍵とペアになっていない → 鍵の再生成が必要

Step 3: 署名対象ヘッダの改変を疑う

DNSレコードが正しいのにfailする場合、メール中継時にヘッダが改変された可能性があります。

改変されやすい箇所 原因例
Subject メーリングリストの接頭辞付与 [ML]
From 転送時の書き換え
本文(body hash) フッタ・免責事項の自動追加
Content-Type エンコード変換

対策

  • 中継サーバーでのヘッダ・本文改変を確認
  • 改変が避けられない場合は、中継後に再署名する設定を検討

実例3: SPF failのときにやること

Authentication-Results: mx.google.com;
       dmarc=fail (p=QUARANTINE sp=QUARANTINE dis=QUARANTINE) header.from=example.com;
       dkim=pass header.i=@example.com header.s=selector1;
       spf=fail (google.com: domain of info@example.com does not designate 198.51.100.25 as permitted sender)
           smtp.mailfrom=info@example.com

SPFがfailし、DMARCもfailしています(DKIMはpassしていますが、alignmentの問題がある可能性)。

Step 1: smtp.mailfromを確認

エンベロープFrom(Return-Path)は info@example.com です。SPFはこのドメインのレコードで判定されます。

Step 2: 送信元IPを特定

Authentication-Resultsの記述から 198.51.100.25 が送信元IPと分かります。Receivedヘッダでも確認できます。

Received: from mail.sender.example (unknown [198.51.100.25])
        by mx.google.com with ESMTPS id ...

Step 3: SPFレコードを確認

dig example.com TXT +short | grep spf

出力例

"v=spf1 include:_spf.google.com include:sendgrid.net ~all"

問題の特定

198.51.100.25 がこのSPFレコードで許可されているか確認します。

# includeされているドメインも展開して確認
dig _spf.google.com TXT +short
dig sendgrid.net TXT +short

よくある原因

原因 対策
新しい送信サーバーのIPが未登録 SPFレコードに ip4:198.51.100.25 を追加
外部メール配信サービスのinclude漏れ include:spf.vendor.com を追加
転送サーバー経由で元IPが変わった SRS(Sender Rewriting Scheme)の導入を検討
DNSルックアップ回数が10回超過 SPFレコードの簡素化(後述のpermerror参照)

実例4: DMARC failのときにやること(alignment不一致)

Authentication-Results: mx.google.com;
       dmarc=fail (p=REJECT sp=REJECT dis=REJECT) header.from=example.com;
       dkim=pass header.i=@mail.example.com header.s=selector1;
       spf=pass smtp.mailfrom=bounce@mail.example.com

SPFもDKIMもpassしているのに、DMARCがfail——これは典型的なalignment(アライメント)不一致です。

alignmentとは

DMARCは「SPFまたはDKIMがpassかつドメインが一致」していることを要求します。

認証 対象ドメイン Fromドメインとの比較
SPF smtp.mailfrom(エンベロープFrom) 一致が必要
DKIM header.d(署名ドメイン) 一致が必要

この例での問題

項目 Fromドメインとの一致
header.from example.com (基準)
smtp.mailfrom bounce@mail.example.com 不一致(サブドメイン)
dkim header.d mail.example.com 不一致(サブドメイン)

DMARCポリシーの aspfadkim がデフォルト(strict)の場合、サブドメインは一致とみなされません。

修正の選択肢

選択肢 内容 メリット/デメリット
A. DMARCポリシーを relaxed に aspf=r; adkim=r を追加 簡単だがセキュリティは若干低下
B. DKIMの署名ドメインをFromに合わせる d=example.com で署名 推奨。From詐称への耐性維持
C. エンベロープFromをFromに合わせる Return-Pathを @example.com バウンス管理の設計変更が必要
D. Fromヘッダを署名ドメインに合わせる From: を @mail.example.com ブランドイメージへの影響を検討

推奨 選択肢B(DKIMの署名ドメインをFromドメインに合わせる)が最もバランスが良いです。

結果→修正アクション対応表

判定結果から修正箇所を素早く特定するための対応表です。

DKIM関連

結果 原因 修正アクション
dkim=fail 署名検証失敗 DNSの公開鍵確認 / 中継時の改変確認
dkim=permerror DNS/セレクタ設定エラー s=セレクタのDNSレコード確認、文法チェック
dkim=temperror DNS一時障害 時間をおいて再確認 / DNSサーバーの状態確認
dkim=none 署名なし 送信サーバーでDKIM署名を有効化
dkim=neutral 署名はあるが判定保留 署名設定の見直し

SPF関連

結果 原因 修正アクション
spf=fail 送信元IPが未許可 SPFレコードにIP/includeを追加
spf=softfail 未許可だが拒否はしない設定 ~all を意図的に使っているか確認
spf=permerror SPF文法エラー / ルックアップ10回超過 レコード文法確認 / include展開数の削減
spf=temperror DNS一時障害 時間をおいて再確認
spf=none SPFレコードなし SPFレコードを作成

DMARC関連

結果 原因 修正アクション
dmarc=fail SPF/DKIMともにfail、またはalignment不一致 SPF/DKIMの個別結果を確認 → alignment調整
dmarc=none DMARCレコードなし DMARCレコードを作成
dmarc=permerror DMARCレコード文法エラー _dmarc.example.com のTXTレコード確認

確認コマンド(dig/openssl)

トラブルシューティングで使用する最小限のコマンド集です。

DNS確認

# SPFレコード確認
dig example.com TXT +short | grep -i spf

# DKIMレコード確認(セレクタ名は実際の値に置換)
dig selector1._domainkey.example.com TXT +short

# DMARCレコード確認
dig _dmarc.example.com TXT +short

# MXレコード確認(参考)
dig example.com MX +short

SPFレコードの展開確認

# includeされているドメインを再帰的に確認
dig _spf.google.com TXT +short
dig spf.protection.outlook.com TXT +short

# IPアドレスの逆引き確認(送信元特定に有用)
dig -x 203.0.113.10 +short

DKIM公開鍵の詳細確認

# 公開鍵を整形して確認
dig selector1._domainkey.example.com TXT +short | tr -d '"' | fold -w 64

継続監視: DMARCレポートを集計する

テストメールでの確認は単発のチェックに有効ですが、本番配信の認証状況を継続的に把握するにはDMARCレポートを活用します。DMARCレポートは、受信側(Gmail、Microsoft 365など)が送信側に送ってくれる認証結果の集計データです。

DMARCレポートとは

DMARCレコードに rua= タグを設定すると、受信側から定期的に集計レポート(Aggregate Report) が送られてきます。

_dmarc.example.com.  IN TXT  "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com"
タグ 意味
rua= 集計レポートの送信先(mailto: または https:)
ruf= 失敗時の詳細レポート送信先(対応していない受信者も多い)

レポートの中身

レポートはXML形式(gzip圧縮)で届きます。主要な情報は以下の通りです。

<record>
  <row>
    <source_ip>203.0.113.10</source_ip>
    <count>1500</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>pass</dkim>
      <spf>pass</spf>
    </policy_evaluated>
  </row>
  <identifiers>
    <header_from>example.com</header_from>
  </identifiers>
  <auth_results>
    <dkim>
      <domain>example.com</domain>
      <result>pass</result>
      <selector>selector1</selector>
    </dkim>
    <spf>
      <domain>example.com</domain>
      <result>pass</result>
    </spf>
  </auth_results>
</record>
フィールド 内容
source_ip 送信元IP
count そのIPからの送信数
dkim/spf 認証結果(pass/fail)
disposition 受信側が適用したポリシー(none/quarantine/reject)

Pythonでのパース例

import gzip
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path

@dataclass
class DMARCRecord:
    source_ip: str
    count: int
    header_from: str
    dkim_result: str
    spf_result: str
    disposition: str

def parse_dmarc_report(file_path: str) -> list[DMARCRecord]:
    """DMARCレポート(gzip圧縮XMLまたは生XML)をパースする"""
    path = Path(file_path)

    # gzip圧縮か生XMLかを判定
    if path.suffix == '.gz':
        with gzip.open(file_path, 'rt', encoding='utf-8') as f:
            tree = ET.parse(f)
    else:
        tree = ET.parse(file_path)

    records = []
    for record in tree.findall('.//record'):
        row = record.find('row')
        policy = row.find('policy_evaluated')
        identifiers = record.find('identifiers')

        records.append(DMARCRecord(
            source_ip=row.findtext('source_ip', ''),
            count=int(row.findtext('count', '0')),
            header_from=identifiers.findtext('header_from', ''),
            dkim_result=policy.findtext('dkim', ''),
            spf_result=policy.findtext('spf', ''),
            disposition=policy.findtext('disposition', ''),
        ))

    return records

# 使用例
if __name__ == "__main__":
    records = parse_dmarc_report('mail.ru!example.com!1759449600!1759536000.xml')
    for r in records:
        print(f"{r.source_ip}: DKIM={r.dkim_result}, SPF={r.spf_result}, count={r.count}")

集計・可視化

パースしたデータは以下のように活用できます。

  • 送信元IPごとの認証成功率: 意図しないIPからの送信(なりすまし)を検出
  • fail率の推移: 設定変更後の影響を確認
  • ダッシュボード化: Grafana等で可視化し、閾値超過時にアラート
# 簡易集計の例
from collections import defaultdict

def summarize_records(records: list[DMARCRecord]) -> dict:
    stats = defaultdict(lambda: {'pass': 0, 'fail': 0})
    for r in records:
        stats['dkim'][r.dkim_result] += r.count
        stats['spf'][r.spf_result] += r.count
    return dict(stats)

まとめ

Authentication-Resultsの読み方を「型」として整理しました。

読む順番(型)

  1. authserv-id: 誰が判定したか
  2. dmarc=: 最終結論
  3. dkim=: 署名検証 + header.d / header.s
  4. spf=: 送信元IP検証 + smtp.mailfrom

トラブルシューティングの流れ

  1. メールの生ヘッダを取得
  2. Authentication-Resultsを「型」に沿って読む
  3. fail/permerrorがあれば、対応表から修正箇所を特定
  4. dig等でDNSレコードを確認・修正

この手順を身につければ、メールの到達率低下や迷惑メール判定の調査時間を大幅に短縮できます。問題が発生したら、まずAuthentication-Resultsを確認する習慣をつけましょう。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?