/var/lib/azumakuniyuki

Sisimaiとか技術的なことはこっちに書いてみようかという試み

大量誤送信撲滅委員会京都支部

この記事はSMTP Advent Calendar(そんなカレンダーは存在しないし作ってもいない)の22日目です。一昨日の夜に京都新聞でメールを六千人に誤送信って記事を見て「またか」「複数人のチェックしても起きる、人間が介在する限り永遠に起き続ける」「もうCc:ヘッダやめちまえ」と思い、だれかそういうCc:ヘッダをどうにかする記事を書いてるやろと思ったものの、ビシャっと当てはまる記事が見つからず、気まぐれで試して適度に意図した動作になったので記事にした次第です。

www.kyoto-np.co.jp

記事ではCc:ヘッダとか何も書いてないのですが、似たような事件が毎月のようにあるので、とりあえずシステム側の仕組みとして大量の誤送信を排除する方法として試した記録を書いておきます。

メール系のAdvent Calendarがなにもない

思い付きの気まぐれで実験してブログにするかと思い、せっかくやからメール系のAdvent Calendarに飛び入り参加しようと検索したものの何もなかったのでSMTP Advent Calendar 22日目を詐称しています。

根本的な問題と手早い解決方法

  • そもそもTo:Cc:に文字列として多くのメールアドレスや名前が入っているのがマズい。
    • 誤送信ならば受け取った人に漏洩する
  • 入れても良いメールアドレスの数を制限する
  • あるいはヘッダそのものをバサッと書き換える

前提条件とやること

  • Postfix 2.10.1が動いている
  • MTAとして動いていて外からのメールを25番ポートで受け付ける
  • MSA(Message Submission Agent)としても動いていて587番ポートで自組織からのメール投函も引き受ける
    • To:に入れてもよいメールアドレスの数を3個に制限する
    • Cc:は5個のメールアドレスまではOKってことにする
      • または制限はせずヘッダの中身からメールアドレスを消す(書き換え)
    • ただし587番ポート経由で投函されたメールに対してのみ行う
    • つまり外から25番ポート宛に来たメールに対しては上記制限も書き換えも行わない

手早い解決方法

設定内容はPostfix Built-in Content InspectionのConfiguring different header/body checks for MX service and submission serviceにある内容だけです。 それだけで完結しますしリンクと説明をTwitterに流せばわざわざブログにするまでもないのですが、気まぐれでたまには書くかってとこです。

MSAだけがヘッダのチェックをする

上述のとおり、外からくるメールに対しては制限も書き換えも実施せず、MSA(587番ポート経由のSubmission)のみがヘッダのチェックをするように設定します。

main.cf

Postfixではcleanup(8)がヘッダのチェック、つまりheader_checksを呼びますので、まずは/etc/postfix/main.cfに次のパラメータを書きます。

# /etc/postfix/main.cf
msa_cleanup_service_name = msa-cleanup # MSAだけが呼ぶcleanupを定義する
msa_header_checks = pcre:/etc/postfix/re_auditheader # このファイルにMSA専用のヘッダ検査用正規表現と動作を書く

上記の設定をどこか適当なところに書きます。なお、どちらも独自に定義するパラメータで、Postfixに用意されているパラメータではありません。 とはいえ誰かがたまたま同じ目的で同じ名前のパラメータを書いているかもしれないので、postconf | grep ^msa_でも実行して一致するものがないことを確認しておくと良いでしょう。

まだPostfixは再起動しません、他のファイルも編集するので。

master.cf

/etc/postfix/master.cfには次の内容(コメント部分以外)を書き加えます。場所はどこでも良いです。 -oから始まる行はcleanupの引数ですので、前の行から続いてますよ〜を示すために、先頭に空白を入れます。

# /etc/postfix/master.cf
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================
msa-cleanup unix n      -       n       -       0       cleanup
  -o header_checks=$msa_header_checks

serviceのとこに書いたmsa-cleanupは、さっきmain.cfで書いたmsa_cleanup_service_name = msa-cleanupの右辺値、 command + argsにあるcleanupは実際に呼び出すコマンド(/usr/libexec/postfix/cleanup)と-o以下の引数で、$msa_header_checks は同じくmain.cfに書いたmsa_header_checks = pcre:/etc/postfix/re_auditheaderを指します。

まだPostfixは再起動しません、他のファイルも編集するので。

具体的なヘッダに対する正規表現

re_auditheader

実際にどのヘッダをどう引っ掛けてどう処理するのかを記述するre_auditheaderを作ります。ファイル名はさっきのmain.cfで定義した名前と一致していれば何でもよいです。

# cd /etc/postfix
# vi ./re_auditheader

メールアドレスの個数を制限する

作ったファイルの中には「あんまりたくさん入れたらアカンで」的な制限を正規表現で記述します。

# /etc/postfix/re_auditheader
# /ヘッダに対する正規表現/ 正規表現に一致した場合の動作
/^To:(?:[^@]+@[^@]){4}/ REJECT 550 5.7.1 There are 4 or more email addresses in the To: header
/^Cc:(?:[^@]+@[^@]){6}/ REJECT 550 5.7.1 Too many email addresses in the Cc: header

To:に入れられるメールアドレスは3個まで=4個以上あるとREJECTされる、Cc:は6個以上あるとREJECTされる、という動作になります。 REJECTのうしろに何も書かない場合は550 5.7.1 message content rejectedってエラーメッセージが返りますが、何が悪いのか意味不明なので 「To:に4個以上のメールアドレスを入れたらダメですよ〜」とか、「Cc:にメールアドレスを入れすぎやねん」とかが適切(どうせエラーメッセージを読まない人は何を書いても読まないので適当なのでいいかも)です。

なお、ここで書ける正規表現main.cfで種別をpcre:/...と書いたとおりPerl Compatible Regular Expressionです。つまりPerl正規表現を書き慣れている人にとっては いともたやすく書かれる簡易な正規表現ってことになります、サラサラっと書けます。

とはいえ、上記の正規表現を見ても分かるとおり、大雑把です。単に@の個数を数えているに等しい正規表現で、表示される名前をあずま@京都のように半角の@を含んでいると、 それも制限値を数える対象になります、たぶん。実際にテストはしてないのですが、多分ひっかかります。それとローカルパートが"neko@nyaan@exampe.jp."@example.co.jpみたいなRFC的にはOKでも 人間的には正気かよって思えるようなローカルパートに@が入っているアドレスも同様ですが、滅多に見ないので気にしなくて良いです。

Postfix manual - header_checks(5)にある説明のとおり、REJECT以外にも隔離するHOLDとかいろいろ動作の指定が可能です。

Cc:の中身を書き換える

Cc:に列挙できるメールアドレスの数を制限すると、たとえば標準でBcc:ヘッダの入力欄が表示されていないMUAを使ってる人は不便に思うかもしれません。「いや、Cc:Bcc:ヘッダの意味と違いと用法と作法は現代の基礎的な情報リテラシーやろ」ってことですが、組織によってはなんらかの理由でCc:をフル活用してはるかもしれません。ってことでCc:の数は制限せずにヘッダの中身をバッサリ書き換える正規表現が次のREPLACEを使った書き方です。

# /etc/postfix/re_auditheader
/^To:(?:[^@]+@[^@]){4}/ REJECT 550 5.7.1 There are 4 or more email addresses in the To: header
/^Cc:.*$/ REPLACE Cc: undisclosed-recipients:;

undisclosed-recipients:;はたまーに見かけるグループアドレスってやつです、PostfixSendmailソースコードにもでてきます。

postfix-3.5.7(0) % find ./src -type f -exec grep undisclosed {} +
./src/cleanup/cleanup_milter.ref4:     1182 regular_text: To: undisclosed-recipients:;
./src/cleanup/cleanup_milter.ref5:      541 regular_text: To: undisclosed-recipients:;
./src/cleanup/bug1.ref:     1182 regular_text: To: undisclosed-recipients:;
./src/cleanup/bug1.text.ref:To: undisclosed-recipients:;
./src/cleanup/cleanup_milter.ref1:     1182 regular_text: To: undisclosed-recipients:;
./src/cleanup/cleanup.c:/* .IP "\fBundisclosed_recipients_header (see 'postconf -d' output)\fR"
./src/cleanup/cleanup_milter.ref11:      907 regular_text: To: undisclosed-recipients:;
./src/cleanup/cleanup_milter.ref13d:      907 regular_text: To: undisclosed-recipients:;
./src/global/mail_params.h:#define VAR_RCPT_WITHELD "undisclosed_recipients_header"
sendmail-8.17.1(0) % find ./sendmail -type f -exec grep undisclosed {} +
./sendmail/collect.c:           addheader("To", "undisclosed-recipients:;", 0, e, true);
./sendmail/sendmail.0:              undisclosed  adds  a  header  reading  `To:  undisclosed-recipi-
./sendmail/sendmail.h:#define NRA_ADD_TO_UNDISCLOSED    4   /* add To: undisclosed:; header */
./sendmail/sendmail.8:add-to-undisclosed
./sendmail/sendmail.8:`To: undisclosed-recipients:;'.
./sendmail/readcf.c:        else if (SM_STRCASEEQ(val, "add-to-undisclosed"))

Cc:は本来、メールアドレスを書くヘッダなので、殆どのMTAではドメインのないアドレスにはドメインを追記するって動作をします。 なので、/^Cc:.*$/ REPLACE Cc: redacted のように書くとヘッダ自体は書き換えられますが、通過するどこかのMTAでredacted@example.jp のような書き換えが発生し、意図しないメールアドレスが生まれます。

よく調べられてないのですがCc:ヘッダは rfc5322 では1個以上のメールアドレスが入る、と読める気がするので、空の値を許容するかどうか、 また空のCc:ヘッダの存在が配送経路のどこで何にどう影響するかよく分からない(たぶん大丈夫そうな気がする)ので/^Cc:.*$/ REPLACE Cc:と書くよりはundisclosed-recipients:;にしておくほうが 無難かなぁと思います。

Cc:を制限してなかったら大量に送られるやん」ってとこですが、そもそもMTAはヘッダの種類に関係なく宛先が1つでも2つでも25個でも2200個でも、それが誤送信であるかどうかを判断することはできないので、 「間違って大量に送ってしまいました」な事故においてメールアドレスを露出させない防波堤の役割となります。

Postfixに反映させる

main.cfmaster.cfre_auditheaderを編集し終わったら、最後にpostmap auditheader.dbを実行してPostfixが参照する形式のファイルを作り、Postfixを再起動します。

# cd /etc/postfix
# /usr/sbin/postmap ./re_auditheader.db
# /usr/sbin/postfix check
# systemctl restart postfix

動作試験

MTA

まずは外部からPostfixの動いているホストへメールを投げて、正常に受け取れるか(設定ミスが有るとさっきのヘッダに関係なくエラーになる)と、To:Cc:が制限対象になってないか、書き換えられてないかを確認します。

% telnet Postfixの動いているホスト 25
Trying 192.0.1.25...
Connected to smtps.example.jp.
Escape character is '^]'.
220 mbox.cubicroot.jp ESMTP MAIL SYSTEM
EHLO [127.0.0.1]
250-smtps.example.jp
250-PIPELINING
250-SIZE 33554432
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
MAIL FROM: <nekochan@example.com>
250 2.1.0 Ok
RCPT TO: <kijitora@example.jp>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: TEST #1
From: nekochan@example.com
To: kijitora@example.jp, mikeneko@example.jp, sabineko@example.jp, sabatora@example.jp
Cc: kuroneko@example.jp, sironeko@exampe.jp, michitsuna@example.jp, nekodono@example.jp, chatora@example.jp, mugiwara@example.jp
Date: Wed, 22 Dec 2021 06:00:00 +0900

Nyaan
.
250 2.0.0 Ok: queued as 4JJcZn4Mrhz1yv5t
quit
221 2.0.0 Bye
Connection closed by foreign host.

もしもTo:ヘッダのメールアドレス数が制限に引っかかったら、最後の.を入れたところでエラーとなりますが、大丈夫でした。Cc:も同様です。 Cc:を書き換え対象にした場合は、受け取ったメールのCc:書き換えられてないかを確認しておきます。なお、僕は手っ取り早くtelnetでやりましたが、 Gmailとかからヘッダを山盛りにしてメールを投げるほうが楽です、たぶん。

MSA

次のテストはPostfixのヘッダ検査で対象となるかどうかの試験です。同じくtelnetでやってますが、Submissionが可能なアカウントから適当なMUAでメールを投げるほうが楽です、たぶん。

To:ヘッダの個数制限

% telnet Postfixの動いているホスト 587
Trying 192.0.1.25...
Connected to smtps.example.jp.
Escape character is '^]'.
220 mbox.cubicroot.jp ESMTP MAIL SYSTEM
EHLO [127.0.0.1]
250-smtps.example.jp
250-PIPELINING
250-SIZE 33554432
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
AUTH PLAIN xP...(BASE64の長い文字列)...cw==
235 2.7.0 Authentication successful
MAIL FROM: <nekochan@example.com>
250 2.1.0 Ok
RCPT TO: <kijitora@example.jp>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: TEST #2
From: nekochan@example.com
To: kijitora@example.jp, mikeneko@example.jp, sabineko@example.jp, sabatora@example.jp
Cc: kuroneko@example.jp, sironeko@exampe.jp, michitsuna@example.jp, nekodono@example.jp, chatora@example.jp, mugiwara@example.jp
Date: Wed, 22 Dec 2021 06:00:00 +0900

Nyaan
.
550 5.7.1 550 5.7.1 There are 4 or more email addresses in the To: header
QUIT
221 2.0.0 Bye
Connection closed by foreign host.

AUTH PLAINで渡すSMTP認証の文字列は コマンドラインからSMTP認証の試験を行う - Qiita あたりが参考になります。

メール本文の入力終了を示す.の直後にさっきのre_auditheaderで書いたエラーメッセージが表示されたので意図した動作であると確認できました。 Cc:の個数制限または書き換えの試験は、同じように587番ポートに繋いで認証してTo:ヘッダをメールアドレス1個だけにして試験するとよいです。

f:id:azumakuniyuki:20211222134851p:plain

/var/log/maillog

あと、ログには次のような内容で出力されます。

Dec 22 06:00:00 smtps postfix/cleanup[6395]: 4JJGgX6BvMz1yv5t: reject: header To: kijitora@example.jp, mikeneko@example.jp, sabineko@example.jp, sabatora@example.jp from p2022-ipngnfx22kyoto.kyoto.ocn.ne.jp[192.0.2.26]; from=<nekochan@example.com> to=<kijitora@example.jp> proto=ESMTP helo=<localhost>: 5.7.1 550 5.7.1 There are 4 or more email addresses in the To: header

ヘッダを書き換える他の選択肢

Milterを使う

実験したのは手早くやる方法なので、メールアドレスの個数を数えるあたりの正規表現が大雑把です。たとえば、 - 正確にメールアドレスの個数を取りたい(真面目にやると結構たいへん) - 「Cc:に○○課長が入っているときはヘッダに残してほしい」「To:に入れて(分かりました)」 - 「メールアドレスの並び順で失礼があったらアカン」「並び順に失礼とか無い(なるほど確かに)」 - 「営業部長はCc:にたくさん入れはるから制限の例外にしたい」「沢山いれるな(部長は顔が広いですしね)」 って、実装したらいろいろ細かい要望が湧き出してくると思うので、細かい制御がしたいならMilter一択です。わりと手間がかかります。

ヘッダを書き換えない選択肢

メーリングリストを使う

誤送信でメールアドレスが露出しないようにするなら、これが妥当な選択肢の一つではあります。つい先日メーリングリストを作ったときは GNU Mailman選んだのですが、RHELでは非推奨になったそうで、Google Groups とかに流れるのかなぁと。

Sympaってのも試したのですが、ちょっとごつすぎてAnsibleのロールを書くのに時間がかかりそうなため、慣れたMailmanにしました。 現代ではDKIMの問題とかがあり、そこそこ新しいMLドライバであることが求められるので、どうなるのかなぁって。

困ること

ヘッダにメールアドレスを大量に列挙しないので、僕は特に困らない気がしますが、大きな企業さんとやり取りするときなんかは、Cc:に入るメールアドレスが次第に増えていく感じなので、 「全員に返信」で困りそう(数の制限にひっかかる、書き換えるとTo:の人にしか返信できない)な気がします。なので、Cc:に入れてもよいメールアドレスの上限を10個とかにしておけば、うっかり1000件のメールアドレスを入れて送ってしもた系の事故は防げるかなと思います。

しかしながら、このようなケースではWebUIを備えた今どきのメーリングリストを作るのが妥当な解決策です。この記事はあくまで大量の誤送信をシステム側の仕組みで阻止する手早い方法があるでって話なので、深堀りはしないです、各組織でうまいことやってください。

エンベロープとヘッダ

むかーし、別の目的で、Sendmailでメールアドレスのヘッダを書き換えたときに「To:とかCc:を書き換えてちゃんと宛先に届くの?」と聞かれたがあります。

例えば、親しいネコさんにAmazonで見つけた良さそうな爪研ぎをクリスマスの贈り物として届けるにあたっては次の二通りの方法があります。

  1. 宛名は人間(世帯主とか)にしてメッセージカードに「通綱(ネコの名前)様」と書く
  2. 宛名もメッセージカードも「姉小路道綱(世帯主の名字+ネコの名前)様」と書く

クロネコさんとかが見る宛名がエンベロープで、中のメッセージカードに書く宛名がヘッダです。語源のとおりに説明すると封筒に書く宛名(と住所)がEnvelopeで、中に入れた手紙の宛名がHeaderです。 SMTPコマンドではMAIL FROM: <エンベロープFromのアドレス>RCPT TO: <エンベロープToのアドレス>となります。ヘッダの書き換えはDATAコマンドの入力が.で終わったあとで実施されるので、 エンベロープには影響しません。よって、To:とかCc:を書き換えてもちゃーんと宛先に届きます。

以上のとおり、ヘッダには好きな値が書け詐称し放題なので、現代ではなりすまし(なりすまされ)防止を補完するDKIMやDMARCがありますが、それはまた別の話です。

二行ぐらいでまとめ

To:とかCc:に大量のメールアドレスが入ってる場合はPostfixのヘッダ検査で手軽に検出できるし漏洩防止にもなる。あと本気でやるなら商用で誤送信防止のプロダクトとかソリューションがあると思う、たぶん。