はじめに
dkimpy-milterを使用してしばらく経ちますが、上手く設定できなていないことがあり、その問題解決についてメモを残します。
よって dkimpy-milter に関する全体的な説明はありませんので、他の参考文献を参考に、セットアップしてみてください。重要なので二度言いますが、ここでは全体に触れません。
どういう問題かというと、署名モード(Mode s)にしていると、外部からの自分宛メール「も」署名されてしまう、という現象です。
具体的にはFromアドレスとToを自分宛とするメールに対してDKIM署名してしまうという問題です。この現象を理解するには、あまりにもドキュメントは不親切でした。
本メモについて
-
dkimpy-milterの特定の状況に対するメモです - OSは問いませんが、
sendmail+dkimpy-milterの組み合わせです - 特にバージョンは問いません(ほぼ最新のものである前提ですが)
-
postfix+dkimpy-milterの組み合わせでも、設定の理解に役に立つとは思いますが、その点についてはフォローしません - インストール環境を問わないので、分かってる前提で色々と省略しています(設定ファイルのパスなど)
原因と対策
本質的な原因は MacroListVerify 設定(ミス)にありました。また sendmail 側の設定ミス(自分の過去の実験の名残)が重なっていました。両者の設定をキチンと合わせましょうというのが結論になります。
sendmail 側の問題
具体的には Milter.macros.connect という設定です。通常は以下のように設定されています。
O Milter.macros.connect=j, _, {daemon_name}, {if_name}, {if_addr}
m4的には confMILTER_MACROS_CONNECT() 設定となります。
define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {if_name}, {if_addr}'')
この値が j, _, {daemon_name}, {if_name}, {if_addr} となっていること確認します。より具体的に注意しないといけないのは {daemon_name} という設定があることが前提となります。
これ以外の設定は(dkimpy-milter としては)ノイズです。参照しないので、あっても無くても問題ありません。
次に DaemonPortOptions の設定状況を確認する必要があります。デフォルトでは下記のような状況となっています。
O DaemonPortOptions=Name=MTA
O DaemonPortOptions=Port=587, Name=MSA, M=E
m4的には下記の状態で、書かれていない場合のデフォルトになります。
DAEMON_OPTIONS(`Name=MTA')
DAEMON_OPTIONS(`Port=587, Name=MSA, M=E')
この設定の具体的に何を見るか、については「SMTP」(ないしはSMTPS)としてサービスしているか、を確認します。具体的にはポート25ないしはポート465になります。
ポート587は submission サービスなので、送信を受け付けるためのサービス(ポート)なので今回は対象外となります(設定してはいけない)。
また Name パラメータが重要となります。今回は MTA という文字列になります。何が言いたいかと言うと、{daemon_name} というマクロで渡される情報が DAEMON_OPTIONS() で指定された Name パラメータであるということです。
つまり MTA はSMTPサービスなので、外部からのメールが届くサービス(ポート)である、ということを意味します。
これが dkimpy-milter が「外部ソースである」と決定するための情報になります。
dkimpy-milter 側の問題
Mode が s(igner) と v(erifier) がで関連する設定が変わります。
-
s:MacroListVerifyの設定により抑制 -
v:MacroListの設定により抑制
今回の、署名させたい(s)時に、外部からのメールを署名して欲しくない場合は MacroListVerify により外部からのメールであるという設定をすることになります。
MacroListVerify daemon_name|MTA
dkimpy-milter は sendmail から {daemon_name} というマクロの内容(本例では MTA と MSA)が送られてきます。本設定により {daemon_name} の内容が MTA の時は外部メールであるというヒントを与えます。
Mode s なので署名して欲しいけど、検証だけして欲しいという時に指定する設定となります。そして Mode sv ではないので、検証できない=署名しないという働きをします。
参考までに Mode v なので検証して欲しいけど、署名だけして欲しいという時に指定する設定は MacroList になります。Mode sv ではないので、署名できない=検証しないという働きをします。
このように対の関係になっています。
dkimpy-milter のここが納得できない!
意図が理解できないことは無いにせよ、すごく直感に反する振る舞いです。マニュアルをよく読んでも直感に反する書き方となっています。
MacroListVerify (データセット)
Defines a set of MTA-provided macros that should be checked to
see if the sender has been determined to be an external source
and therefore whether or not the message should be signed.
送信者が外部ソースであり、よってメッセージが署名されるか否かを決定す
るために確認される、MTAが提供するマクロのセットを定義します。
Entries in this data set follow the same form as those of the
MacroList option above. [this option is not inhereted from
OpenDKIM]
このデータセットのエントリ(記述内容)は、上記の MacroList オプショ
ンと同じ形式に従います(このオプションは OpenDKIM から継承されたもの
ではありません)。
Mail sent via connections where macros that are in
MacroListVerify are provided will be not DKIM signed. If the
Mode is 's', then no actions will be performed.
MacroListVerify により提供されたマクロにある接続を経由して送信され
たメールにはDKIM署名は行われません。モードが「s」の場合、何も実
行されません。
そこでGeminiに聞いてみた
ある種の、誘導している…という話は否定しないけど、この気持ち悪さを良い感じに言語化してくれたので参考までに。
gemini-cli を使用していますが、こういう時、ソースコードとドキュメントの照らし合わせを実施して回答させるというメリットがありますね。
極めて分析がはかどりました。
man/dkimpy-milter.conf.5 というマニュアルについて、特に MacroListVerify の説明と、
プログラムコードとの関係について、適切な説明となっているのか、監査してください。
✦ man/dkimpy-milter.conf.5 の MacroListVerify に関する記述と、実際のプログ
ラムコード (dkimpy_milter/__init__.py) の動作を照合・監査しました。
結論として、マニュアルの説明は技術的に正確であり、実装された動作と矛盾しませ
んが、英語の誤字と文法的に不自然な箇所が認められました。
以下に詳細な監査結果を報告します。
1. 技術的正確性の監査
マニュアルの記述とコードの実装は一致しています。
* マニュアルの記述:
> Defines a set of MTA-provided macros that should be checked
to see if the sender has been determined to be an external
source ... Mail sent via connections where macros that are
in MacroListVerify are provided will be not DKIM signed.
> (送信者が外部ソースであると判断するためにチェックすべきMTA提供の
マクロセットを定義する...MacroListVerifyにあるマクロが提供され
た接続経由で送信されたメールは、DKIM署名されない)
* コードの実装 (`dkimpy_milter/__init__.py`):
* 接続時 (`connect` メソッド): 設定された MacroListVerify のマ
クロをチェックし、マッチした場合にフラグ
self.external_connection = True をセットします。
1 if self.conf.get('MacroListVerify'):
2 # ... (マクロ確認ループ) ...
3 if (...):
4 self.external_connection = True
* メッセージ処理終了時 (`eom` メソッド): 署名を行うかどうかの判定
部分で not self.external_connection を確認しており、Trueであ
れば署名処理(self.sign_dkim) がスキップされます。
1 if ((self.fdomain in self.domain) and not self.conf.get('Mode') == 'v'
2 and not self.external_connection): # <--- ここで署名を抑制
3 # ...
4 self.sign_dkim(txt)
* マニュアルの記述:
> If the Mode is 's', then no actions will be performed.
> (Modeが 's' の場合、何も実行されない)
* コードの実装:
* Modeが 's' (署名のみ) の場合、コードの検証ロジック (check_dkim) は
実行されません。
* さらに MacroListVerify にマッチすると署名ロジック (sign_dkim) も実
行されません。
* したがって、両方の条件が揃うと「署名も検証もしない(何もしない)」と
いう動作になり、記述は正確です。
2. 誤字・文法の指摘
説明自体は正しいですが、以下の修正が推奨されます。
* 誤字 (317行目):
* 原文: [this option is not inhereted from OpenDKIM]
* 修正案: [this option is not inherited from OpenDKIM]
* 理由: inhereted はスペルミスです。
* 文法 (320行目):
* 原文: will be not DKIM signed.
* 修正案: will not be DKIM signed.
* 理由: "will be not" は英語として不自然な語順です。
まとめ
説明内容自体に嘘や誤解を招く重大な欠陥はありません。MacroListVerify が
「署名を抑制するための機能である(検証機能には影響しない)」という点につ
いても、コードの挙動と合致しています。上記の誤字・文法修正を行えば、ドキュ
メントとして完成度が高まります。
Mode s の時に MacroListVerify という設定が「署名しないこと」を制御する
(より具体的には抑制する)という振る舞いは、直感に反する動きに感じます。
もっと素直に Mode s でかつ署名条件を満たす時に…として MacroListSigning
のような仕組みであれば理解しやすいように考えているのですが、その見解に問
題ありますでしょうか?
✦ ご指摘の通りです。ユーザー様の見解は非常に論理的で、直感的な設計として
より優れていると言えます。現状の MacroListVerify という命名と挙動が
「直感に反する」と感じられるのは、このソフトウェアの設計経緯やデフォル
トの挙動に起因する「ねじれ」があるためです。
以下に、なぜそのような「直感に反する実装」になっているのか、コードと仕
様の観点から分析した結果を解説します。
1. 現状の設計構造:「除外」の論理
ユーザー様が提案された「署名条件を満たす時に署名する(ホワイトリスト方
式)」に対し、現在の dkimpy-milterは、「基本的には署名しようとするが、
特定の条件では署名を止める(ブラックリスト方式)」 という動きをしてい
ます。
* デフォルトの動作:
送信元のメールアドレスのドメインが、設定ファイル(Domain や
SigningTable)で管理対象として定義されていれば、接続元がどこであ
ろうと署名を試みます(コード内の sign_dkim 呼び出し条件に
internal_connection の必須チェックがありません)。
* `MacroListVerify` の役割:
この「何でも署名してしまう」挙動を止めるために、「これは検証専用
(外部からの)接続だから署名してはいけない」と明示的に除外するた
めに後付けされた機能が MacroListVerify です。
そのため、「Verify(検証)モードにする」→「結果として署名が止まる」と
いう回りくどい論理になっています。
2. なぜ MacroListSigning ではないのか
これは既存の MacroList という設定項目との兼ね合いが大きいです。
* `MacroList` (既存・OpenDKIM互換):
これは実質的に `MacroListSigning` と同等の意味を持っています。こ
れにマッチすると「内部接続(Trusted)」とみなされ、検証がスキップ
され、署名プロセスに進みやすくなります。
* 名前の対比:
既に「署名寄り(内部)」にする MacroList が存在するため、その対義語
として「検証寄り(外部)」にする設定が必要になり、MacroListVerify
と命名されたと推測されます。
3. ユーザー様の見解の妥当性
ユーザー様が仰るように、セキュリティや設定の明快さを考えれば、以下のよう
な設計の方がはるかに安全で直感的です。
1. デフォルトは署名しない(安全側に倒す)。
2. 署名許可条件 (`MacroListSigning` 等) にマッチした場合のみ署名する。
現状の仕様では、うっかり設定を間違えると「外部から自社ドメインを騙ったな
りすましメール」に対しても、Milter が正規の署名を付与してしまうリスク
(オープンリレーに近い状態)が生じやすくなっています。MacroListVerify
を設定し忘れると署名されてしまう可能性があるからです。
結論
ユーザー様の見解には全く問題ありません。むしろ、ソフトウェアの設計として
はユーザー様の考え方(ホワイトリスト形式での許可)の方が安全設計の原則に
即しています。
現状の仕様は、歴史的経緯(OpenDKIMの仕様)や実装の都合による「後付けの
除外設定」であるため、直感に反する名前と挙動になっていると理解して間違い
ありません。設定を行う際は、「この設定は『署名禁止リスト』である」 と読
み替えて設定するのが最も事故が少ない解釈となります。
ということで誤解を招く仕組みであることは確かのようです。ただし現状こういう状況ではあるので仕方なく受け入れて運用するしか無いようです。
まとめ
sendmail 側
sendmail.cf の内容を確認します。
-
O Milter.macros.connect=の行中に{daemon_name}があること。 -
O DaemonPortOptions=の行中にメールを受信(送信のための受信ではなく)サービスである名前(Name=)を確認すること。
dkimpy-milter 側
dkimpy-milter.conf の内容を確認します。
-
MacroListVerifyの行中にdaemon_name|MTAと記載されていること(MTAの部分は先に確認したName=で指定された内容)。 - 複数ある場合は下記のように記載する。
MacroListVerify daemon_name|MTA,daemon_name|MTA6
よくある質問とその答え
Q.daemon_name、{daemon_name} どっちが正しいの?
A.{daemon_name} が正です。ただし MacroListVerify (MacroList)では指定されたマクロ名の前後に { と } を付与してマクロを参照します。よって sendmail が定義する _ や j というマクロは dkimpy-milter 側では参照できません。dkimpy-milter は {_} や {j} としてマクロを参照しようとしてしまうからです。
参考文献
- dkimpy-milter
- ARC(RFC8617)とEd25519 SHA-256のDKIM(RFC8463)にさくっと対応させてみる
- Bug#981156: dkimpy-milter: issues in sign-vs-verify logic (and documentation) 世の中似た様な問題を抱えてる人がいるらしい
そもそも sendmail と組み合わせてる例が見当たらなかったw。いや Postfix との組み合わせしか勝たん…。。 orz