メーリングリストの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が下流で追加されると:
- Gmailは
reply-toを不在として署名した。 - KaiMailがメッセージに
Reply-Toを追加する。 - Gmailの検証者が、署名が「存在すべきでない」と示す
Reply-Toを発見する。 - 元の送信者のDKIMが失敗する。
- DMARCは
From:ドメイン(kafkai.com)に整合する合格DKIMを確認するが、見つからない。 - 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キーを設定している場合、転送パイプラインは以下のように動作する。
From:を書き換える:"Kamal Mustafa via discuss" <[email protected]>Reply-To:をリストアドレスに設定する。X-Original-SenderおよびX-Original-Fromに元の身元を保持する。- 顧客ドメインのDKIMキーで署名する。 書き換えた
From:ドメインと整合する。 - 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キーを設定する。
カマル・ムスタファ