メーリングリストのDMARC対応―DKIM認証失敗の解決

最近、メールリストのサポート機能をリリースしました。一見するとシンプルな機能でしたが、実装にあたってはDKIM署名メカニズムやDMARCとの整合性など、深く掘り下げる必要がありました。この記事では、その開発プロセス、発生した問題点、そして解決策について解説します。


KaiMailは、最近 [email protected][email protected] のようなアドレスを購読者に配信できるメーリングリスト機能をリリースした

メーリングリストは、3つのモードがある。

  • 公開型 — 誰でも送信できる。全メンバーが受信する。
  • メルマガ型 — オーナーのみが投稿できる(ポート587の認証済みSMTP経由)。メンバーが受信する。ニュースレターに適している。
  • メンバー限定型 — メンバーが投稿できる。全員が受信する。Discussion リストでもいう。

各モードにはそれぞれ独自のユーザーインターフェースが用意されており、各メンバーの登録メールアドレスに配信される。

その後、小さな改良を1つ入れてみた。「メンバー限定型」のリストで「返信」を押したら元の送信者に個別に届くのは困るので、リストアドレスに戻るようにReply-Toヘッダーを注入した。

5分で終わる話のはずだったが、そうはいかなかったよねー。

返信の問題

メンバー限定型のリストで「返信」を押すと、メッセージはリストに戻るべきだが、実際は元の送信者に届いてしまう。

なので、ARC/DKIM署名の前にReply-Toヘッダーを注入する。

Reply-To: [email protected]

簡単だと思った。

DMARCが失敗し始めた

Reply-Toを追加した直後、Gmailの認証結果を確認すると、DiscussionリストのメールにDMARC失敗が出ていることに気づいた。

dkim=pass [email protected]
dkim=fail [email protected]
dmarc=fail (p=REJECT sp=REJECT dis=QUARANTINE) header.from=kafkai.com

変更前は、同じ送信者からのメールは正常に通過していた。

dkim=pass [email protected]
dkim=pass [email protected]
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=kafkai.com

何も変えていないはずなのに、なぜDMARCが突然失敗するのか。Reply-Toのヘッダーを追加したから多分それだろう。ここから数日かかる調査が始まった。

ヘッダー追加がDKIMを破壊する理由

DKIM署名はh=フィールドで宣言された特定のヘッダーリストをカバーする。 Gmailは、Reply-Toが存在しない場合でも、意図的にそのリストに含める。これはヘッダーインジェクション攻撃への防御だ。

つまり、Reply-Toが存在しないメッセージに対しても、Gmailは「Reply-Toはない」とその不在を署名している。

ここでReply-Toが下流で追加されると:

  1. Gmailはreply-to不在として署名した。
  2. KaiMailがメッセージにReply-Toを追加する。
  3. Gmailの検証者が、署名が「存在すべきでない」と示すReply-Toを発見する。
  4. 元の送信者のDKIMが失敗する。
  5. DMARCはFrom:ドメイン(kafkai.com)に整合する合格DKIMを確認するが、見つからない。
  6. DMARCが失敗する。

問題の核心はここだ。KaiMailがヘッダーを追加したことで、元の送信者が出したDKIM署名の整合性が崩れた。でもこれは我々が管理できないものだ。どうしよう。

失敗した修正―ARC署名後への注入移動

最初に思いついたのは、Reply-Toの注入をKaiMailのARC署名のに移動することだった。ARC署名の前にヘッダーを追加していたから、ARCがそのReply-Toまで署名してしまい、チェーンが汚れていたのではないかと考えた。

この変更で、ARC-Message-Signatureは確かに修正された。Reply-Toを誤って署名しなくなった。

しかしDMARCは依然として失敗していた

なぜなら、最終的な配信メッセージにはReply-Toが含まれている。KaiMailがヘッダーを注入するタイミングに関わらず、元の送信者のDKIMは破損する。ARCを直したことと、DMARCは別の問題だったみたい。

実際に直せたのが、Fromヘッダーの書き換え

そう、Fromそのものが問題だった。

DMARCはFrom:ヘッダーと整合するドメインからの合格DKIM署名を必要とする。KaiMailは元の送信者のドメインの秘密鍵なしに署名できない(当然だけどね)。つまり、元の送信者のDKIMを修復する方法は存在しない。

唯一の解決策は、From:ドメインをKaiMailが管理するものに変更することだ。Google GroupsもMailman 3も同じやり方をしてる。できてるものを参考にすればよかったの話。

Google Groupsの例:

From: 'Sxxx Abxxxx' via Wagtail support <[email protected]>
Reply-To: [email protected]
X-Original-Sender: [email protected]
X-Original-From: Sxxx Abxxxx <[email protected]>
DKIM-Signature: d=googlegroups.com; ...

自分が管理するドメインにFrom:を書き換え、自分のDKIMキーで署名すれば、DMARCは整合する。これが正解だ。

KaiMailの実装

お客様がドメインのDKIMキーを設定している場合、転送パイプラインは以下のように動作する。

  1. From:を書き換える: "Kamal Mustafa via discuss" <[email protected]>
  2. Reply-To:をリストアドレスに設定する。
  3. X-Original-SenderおよびX-Original-Fromに元の身元を保持する。
  4. 顧客ドメインのDKIMキーで署名する。 書き換えたFrom:ドメインと整合する。
  5. ARC署名する。 KaiMailのキーで認証チェーンを保持する。

修正後のYahooでテストメールを受信した結果:

dkim=pass [email protected] header.s=kaimail
dkim=perm_fail [email protected]
dmarc=pass(p=REJECT) header.from=mydomain.com

お客様のドメインのDKIMが合格し、DMARCを満たす。元のkafkai.comのDKIMは依然として失敗するが、これは予期されたことだ。メッセージは改変されたので元のDKIMが破損して当然。ただし、もはやDMARCの判定には関係ない。

お客様のDKIMがない場合のフォールバック

お客様がドメインDKIMキーを設定していない場合、KaiMailはARC署名とReply-To注入にフォールバックとなる。

この場合、厳格な送信者に対してDMARCは依然として失敗する可能性がある。しかしARCチェーンは処置をREJECTからQUARANTINEに和らげる。メッセージは配信されるが、インボックスではなくスパムに落ちる可能性がある。

DKIMキーを設定していれば完璧に解決する。設定していなければ、配信はされるが最適ではない。

あと細かいこと―空のリスト名

この一連の修正を入れている最中に、気づかないうちに別のバグを埋め込んでいた。リスト名が空の場合、Fromの表示名に二重スペースが入っていた。

Kamal Mustafa via  <[email protected]>

名前が空白の場合、リストアドレスのローカルパートにフォールバックするように修正した。

Kamal Mustafa via discuss

小さなことだが、見た目が悪い。

重要な教訓

  • DiscussionリストはReply-Toをリストアドレスに設定しなければならない。そうでなければ返信は元の送信者に漏れる。
  • Reply-Toの追加は、元の送信者がReply-Toを不在として事前署名した場合(Gmailは送信メールすべてでこれを行う)、元の送信者のDKIMを破壊する。
  • 自分の署名後への注入移動は自分のARC署名を修正するが、DMARCは失敗する
  • 正しい直し方はFrom:ヘッダーの書き換えだ。 From:ドメインを自分が管理するものに移し、自分のDKIMで署名し、DMARCが自分のドメインに対して整合するようにする。

DiscussionリストでエンドツーエンドのDMARC合格を達成するには、KaiMail設定でドメインのDKIMキーを設定する