Linux で動く KDC に Windows クライアントを認証

UNIX 系 OS で構築された Kerberos の環境に、Windows クライアントを加える話は、Windows 2000Active Directory が登場したころには、ところどころで聞くことがありました。有名なのは、monyo こと、たかはしもとのぶさんの書いた記事でしょう。

Linux の認証を Active Directory で行なう方法
Windows 2000 マシンの認証を Linux 上の Kerberos サーバで行なう方法

ただ、当時は認証できても、肝心のアプリケーションで対応している物が少なく、「こんな風にしたら、認証通るよ」といった感じでした。

しかし、ふと気がつくと、HTTP や、IMAPPOP3SMTP といったメジャーなプロトコルで Kerberos を生かせる環境が増えてきて、実際に Kerberos 認証をサポートするソフトも、特に海外発祥のマルチ OS 対応ソフトを中心に増えているので、今だったら、シングルサインオン環境の良さを実感出来るのでは、と思って試しました。

Windows での Kerberos 対応

Windows クライアントを Kerberos に対応させる方法として、下記の2つあります。

前者は、究極的には Active Directory を構築し、参加する方法です。後者は、Kerberos 発祥のマサチューセッツ工科大学(MIT)がリリースしているものです。

もし、お金持ちなら Windows Server を買ってきて、Active Directory を構築すれば、参考になる資料もたくさんあるし、簡単です。お金が無いけど時間と知識があるなら、まだアルファ版扱いですが、Samba Ver.4 を使うと、Active Directory 環境は構築できます。

ただ、Active Directory 環境は、単なるシングルサインオンではありません。一番大きいポイントは、各クライアントの各種設定等をシステム管理者が強制できる、グループポリシーの仕組です。Active Directory に参加せずに PC を使っていて、後から Active Directory に参加すると、それまでと同じユーザ名であっても、全く別ユーザ扱いになることに戸惑う人は少なくないと思います。Active Directory によるシングルサインオンの利便性を得るなら、その Active Directory を管理する者に従う必要があります。

元々の Kerberos には、Active Directory のような、強力なポリシー適用の仕組はありません*1シングルサインオンだけの事を考えると、ちょっと大げさな感じがあります。で、純粋に Kerberos のシングルサインオンを利用する仕組も用意されています。その仕組に関しては、後述します。

MIT Kerberos for Windows(以下、KfW)は、UNIX 系 OS で使われる Kerberos の使い方を、Windows で使えるようにしたもので、実際に試してみると、前者の「Windows に元々備わっている機構」を使うより手軽に利用できます。但し、日本語版はありません...。

ksetup vs KfW

Windows ネイティブの Kerberos 対応の設定に使う ksetup というコマンドがあります。Windows の種類やサービスパックの状況等で、最初から入っている場合や、Support Pack をインストールしなきゃいけない場合がありますが、このコマンドを使って、Active Directory じゃない Kerberos 環境を利用できるようになります。

ところが、この ksetup を使った場合と KfW を使った場合で、KDC の設定・管理方法、及び、対応アプリケーションに違いがあります。

アプリケーション側の都合

対応アプリケーションでの違いは、端的に対応アプリケーションが使う API の違いになります。Windows ネイティブの場合は SSPI という API を使う事になります。一方、KfW の場合には、KfW に付属するライブラリを利用する事になります。つまり、アプリケーションが「どっちに対応しているか」がポイントになります。UNIX 系 OS 向けが発祥だったりする場合には、KfW の API を使うほうが実装しやすいし、メンテナンスもしやすい事になります。

しかし、元々 Windows 向けのアプリケーションだと、KfW がインストールされていないと使えないライブラリをあてにするよりも、Windows に最初から用意されている API を使う方が良い、という事になります。また、KfW を使うと、Windows へのログオン後に、改めて KfW を使って Kerberos での認証を受ける必要があります*2

サーバ側の都合

KDC を UNIX 系 OS で作って、UNIX 系 OS 間で Kerberos 認証を使っていると、KfW の方が、UNIX 系 OS 向けと同じ感覚で運用できます。元々、UNIX 系 OS の場合、OS へのログインとは別に、kinit、kdestory というコマンドで、Kerberos へのログイン、ログアウト*3が可能です*4。なので、「Kerberos が使えた!」という実感を手っ取り早く得るには KfW の方が便利です。私が最初に試したのは putty で、パスワードやパスフレーズの入力なしにログインできた時の感動は忘れられません。Thunderbird も設定エディタから Network.auth.use-sspi を false にしてやると、KfW を使う事が可能です。

それ以上に大きいのが、Windows ネイティブの認証を可能にするための条件です。「こうすれば Linux の KDC に ksetup を使って認証できるようになったよ」という記事は多いのですが、その手順の中のどれが必須で、それがオプションなのか、はっきりと書かれている記事を見つける事が出来ませんでした。

ということで、実際にあれこれ試してみました。

Windows ネイティブで Kerberos に対応させるための条件

ホストプリンシパルと名前解決

プリンシパル、という言葉は Kerberos 特有ですが、Active Directory のオブジェクトに近いかもしれません。Kerberos の認証の対象となる物全般を指す言葉で、ユーザだったり、コンピュータだったり、サービスだったりします。

KfW を使う場合、クライアント PC の情報を Kerberos 側に持たせる必要はありません。基本的には、ユーザのプリンシパルと、実際に使おうとしているサービスのプリンシパルがあれば OK です。

ところが、Windows ネイティブの場合、そもそも、そのユーザがその PC を使って良いのかが問われます。実際に KDC が吐き出すログを見ていると、認証要求の後に必ず、クライアント PC のホストプリンシパルに対するチケット要求があるのが分かります。KfW の場合、Windows へのログオンが終わったあとに Kerberos の認証を受けるので、「そのユーザがその PC を使って良いのか」は問われません。UNIX 系 OS をクライアントとして使う場合も同様です。

このため、クライアント PC に対するホストプリンシパルが KDC に登録されている必要があります。で、正式には、ホストプリンシパル名に含まれる FQDN がユニークである事を保証するために、Dynamic DNS でも使ってやるのが良いのだと思いますが、FQDN がきちんと名前解決できる必要は無さそうです。但し、Windows 側の「フルコンピュータ名」からホストプリンシパル名が決まるので、この「フルコンピュータ名」と、KDC に登録されているホストプリンシパルFQDN が合致している必要はあります。

暗号

チケットの暗号化に使える暗号方式に違いがあります。Windows 2000 が登場した当初は、アメリカの暗号輸出規制の影響などもあって、RC4 が使われましたが、XP では DES が、Vista 以降では AES が使えるようです*5

で、この対応している暗号方式の関係で、KDC に保存される鍵が、どの暗号方式の鍵かが問題になります。

/etc/krb5.conf の設定で、デフォルトの暗号方式を変更する、と書かれている記事は多いのですが、肝はむしろ、プリンシパルの鍵がどの形式か、です。

プリンシパル作成時、もしくは、パスワード変更時に「-e」オプションを使うと、作成する鍵の種類が指定できます。

kadmin.local:  addprinc -e des-cbc-crc:normal test
NOTICE: no policy specified for test@EXAMPLE.COM; assigning "default"
Enter password for principal "test@EXAMPLE.COM":
Re-enter password for principal "test@EXAMPLE.COM":
Principal "test@EXAMPLE.COM" created.

確認してみると、下記のようになります。

kadmin.local:  getprinc test
Principal: test@EXAMPLE.COM
Expiration date: [never]
Last password change: Thu Aug 04 14:58:09 JST 2011
Password expiration date: Tue Jan 31 14:58:09 JST 2012
Maximum ticket life: 1 day 00:00:00
Maximum renewable life: 0 days 00:00:00
Last modified: Thu Aug 04 14:58:09 JST 2011 (root/admin@EXAMPLE.COM)
Last successful authentication: [never]
Last failed authentication: [never]
Failed password attempts: 0
Number of keys: 1
Key: vno 1, DES cbc mode with CRC-32, no salt
Attributes:
Policy: default

で、ここで作った鍵と同じ物が使えるように、Windows 側で「ksetup /setcomputerpassword 〜」を実行する訳ですが、この時に保存される物が何か、という事がはっきりしません。どうやら、少なくとも XP では des-cbc-crc が使えるようです。CentOS 5.6 上で、普通にホストプリンシパルをパスワード指定で作ると、デフォルトで作られる鍵は以下のようになります。

  • Triple DES cbc mode with HMAC/sha1, no salt
  • ArcFour with HMAC/md5, no salt
  • DES with HMAC/sha1, no salt
  • DES cbc mode with RSA-MD5, no salt
  • DES cbc mode with CRC-32, Version 4
  • DES cbc mode with CRC-32, AFS version 3

des-cbc-crc に該当しそうなものが最後の2つですが、それぞれ後ろに「Version 4」と「AFS version 3」というのが付いています。一方、先に「-e des-cbc-crc:normal」として鍵を作った時は、「no salt」になってます。

この「normal」の部分は、鍵を生成する際に、パスワード文字列に付けるソルトの種類らしく、「normal」は「default for Kerberos 5」と Kerberos V5 System Administrator’s Guide*6 に書かれています。で、その「default for Kerberos 5」が「no salt」らしい。

ということで、Windows クライアントに対するホストプリンシパルを作成する場合、クライアントが XP であれば、「des-cbc-crc:normal」で鍵を作る必要があるようです。Vista 以降の場合は、「aes128-ctc:normal」とかになりそうな気がしますが、いつか、確認してみたいと思います。

暗号方式で問題になるのは、この「Windows クライアントのホストプリンシパル」だけで、ユーザやサービスのプリンシパルに関しては、デフォルトのままで大丈夫でした。但し、前述のソルトの関係で、結果的に使えるのが rc4-hmac(getprinc の表示で「ArcFour with HMAC/md5」)になってしまいます。

ちなみに、kadmin で鍵を作る際に、どの種類の鍵を作るかは、/etc/krb5.conf の [realms」セクションで「supported_enctypes」に列記すると変更できます。より強力な暗号が使えるようにするためには、これを変更して、Windows からも使える暗号に対する鍵が、デフォルトでも生成されるようにしておくのが良さそうです。

DNS 上での SRV レコードの登録

DNS 上に Kerberos 関係の SRV レコードを追加する、という話がありますが、これは必須ではありません。

そもそも、「ksetup /AddKdc 〜」という手順を見た瞬間、SRV レコードは要らないだろうな、と思いましたが、実際に試して、不要な事を確認しました。おそらく、DNS 側に SRV レコードを用意すれば「ksetup /AddKdc 〜」の手順は不要なんだろうと思います。同様の事は、「ksetup /AddKpasswd 〜」にも言えると思います。

それにしても、やっぱり難しい

とりあえず、自分が試した成功した時のポイントを書くだけのつもりが、ずいぶん長い文章になりました。

自分もまだ、分からない事だらけで、間違ったことを書いている可能性大です。でも、シングルサインオン環境の快感はたまらない(^^;

*1:Kerberos そのものにポリシーという概念はありますが、パスワードの有効期限や長さの話で、デスクトップ環境を強制するような機能ではありません。

*2:Windows ネイティブでの Kerberos 対応の場合、KfW でも自動的にその認証情報を使えるようです。なので、SSPI 非対応の Kerberos 対応アプリケーションがある場合、KfW を入れるだけで、そのアプリケーションも使えるようになると思います。

*3:TGT の取得と破棄、というのが正しい言い方。まぁ、Kerberos の仕組から考えれば、ログイン、ログアウトという言い方が不適切なのは分かるんだけど、そこが敷居の高い原因じゃないかなぁ、と個人的には思います

*4:UNIX 系 OS でも、pam と Kerberos を組み合わせると、OS へのログインと同時に Kerberos に関しても準備完了、という状態にできます。

*5:Windows 7, Windows Server 2008 R2 では、DES がデフォルトでは無効になっているようです(Changes in Kerberos Authentication | Microsoft Docs)。

*6:CentOS 5.6 だと、/usr/share/doc/krb5-server/admin-guide.ps.gz にあります。Postscript ファイルを gzip で圧縮したものなので、展開後、ps2pdf とかで PDF にしておくと良いでしょう。

milter-greylist で差出人アドレスのドメイン部だけを評価する。

id:JULY:20100109 で id:kshiono さんとのコメントのやり取りの中、その存在がバレてしまった(^^;、「差出人アドレスのドメインパートだけを評価するキーワード」のパッチを公開します。

Ver 4.2.5 から作ったパッチですが、4.2.6 でもそのまま適用出来ます。

このパッチを当てると、「fromdom」というキーワードが使えるようになります。

racl whitelist fromdom example.jp domain mx.example.jp

と書くと、

  • 差出人のメールアドレスが example.jp で、
  • かつ、相手の MTA が mx.example.jp だった場合

に許可することになります。

普通の from との違いは、from の場合はメールアドレス全体に対して部分文字列一致か、正規表現で指定するのに対し、fromdom の場合は、メールアドレスのドメインパート(「@」より後ろ)だけを対象として評価します。その時の評価方法は domain と同じで、大文字小文字を無視します。また、domainexact が指定されていれば、ドメインの境界を考慮するようになり、「example.jp」と指定されている場合、

  • mail.example.jp はマッチする。
  • mailexample.jp はマッチしない。

といった具合に、サブドメインがマッチするようになるのも domain と同じです*1

このパッチでやっているのは、設定ファイルのキーワードとして fromdom を追加して、fromdom が来たら「@」の後ろの文字列を、domain キーワードを評価している関数に投げるようにしただけです。

きちんとテストした訳じゃないのですが、少なくとも手元では悪影響は出ていません。一応、お約束ですが、自己責任で(^^;

diff -uNr milter-greylist-4.2.5/acl.c milter-greylist-4.2.5-j/acl.c
--- milter-greylist-4.2.5/acl.c	2010-04-14 13:41:22.000000000 +0900
+++ milter-greylist-4.2.5-j/acl.c	2010-04-28 17:29:44.000000000 +0900
@@ -152,6 +152,10 @@
 void acl_free_prop_regex(acl_data_t *);
 #endif
 
+
+static void retrive_domain_part(const char *, char *, size_t);
+
+
 struct acl_clause_rec acl_clause_rec[] = {
 	/* Temporary types for lists */
 	{ AC_LIST, MULTIPLE_OK, AS_NONE, "list", 
@@ -180,6 +184,18 @@
 	  AT_LIST, AC_NONE, AC_NONE, EXF_ADDR,
 	  acl_print_list, acl_add_list, 
 	  NULL, acl_list_filter },
+	{ AC_FROMDOM, UNIQUE, AS_ANY, "fromdom", 
+	  AT_STRING, AC_FROMDOM_LIST, AC_DOMAIN, EXF_FROM,
+	  acl_print_string, acl_add_string,
+	  acl_free_string, acl_fromdom_cmp },
+	{ AC_FROMDOM_RE, UNIQUE, AS_ANY, "fromdom_re", 
+	  AT_REGEX, AC_FROMDOM_LIST, AC_REGEX, EXF_FROM,
+	  acl_print_regex, acl_add_regex,
+	  acl_free_regex, acl_fromdom_regexec },
+	{ AC_FROMDOM_LIST, UNIQUE, AS_ANY, "fromdom_list", 
+	  AT_LIST,  AC_NONE, AC_NONE, EXF_FROM,
+	  acl_print_list, acl_add_list, 
+	  NULL, acl_list_filter },
 	{ AC_DOMAIN, UNIQUE, AS_ANY, "domain", 
 	  AT_STRING, AC_DOMAIN_LIST, AC_DOMAIN, EXF_DOMAIN,
 	  acl_print_string, acl_add_string,
@@ -514,6 +530,47 @@
 	return (1);
 }
 
+static
+void
+retrive_domain_part(address, buf, bufsize)
+    const char *address;
+    char *buf;
+    size_t bufsize;
+{
+    char *domain_part, *bracket;
+
+    if (address == NULL || buf == NULL || bufsize < 1) return;
+
+    domain_part = strchr(address, '@');
+
+    if (domain_part != NULL) {
+        domain_part++;
+        strncpy(buf, domain_part, bufsize);
+        buf[bufsize - 1] = '\0';
+
+        bracket = strchr(buf, '>');
+        if (bracket != NULL)
+            *bracket = '\0';
+    } else {
+        buf[0] = '\0';
+    }
+}
+
+int
+acl_fromdom_cmp(ad, stage, ap, priv)
+	acl_data_t *ad;
+	acl_stage_t stage;
+	struct acl_param *ap;
+	struct mlfi_priv *priv;
+{
+    struct mlfi_priv copied_priv;
+
+    copied_priv = *priv;
+    retrive_domain_part(priv->priv_from, copied_priv.priv_hostname, sizeof(copied_priv.priv_hostname));
+
+    return acl_domain_cmp(ad, stage, ap, &copied_priv);
+}
+
 int
 acl_header_strstr(ad, stage, ap, priv)
 	acl_data_t *ad;
@@ -744,6 +801,21 @@
 }
 
 int
+acl_fromdom_regexec(ad, stage, ap, priv)
+	acl_data_t *ad;
+	acl_stage_t stage;
+	struct acl_param *ap;
+	struct mlfi_priv *priv;
+{
+    struct mlfi_priv copied_priv;
+
+    copied_priv = *priv;
+    retrive_domain_part(priv->priv_from, copied_priv.priv_hostname, sizeof(copied_priv.priv_hostname));
+
+    return acl_domain_regexec(ad, stage, ap, &copied_priv);
+}
+
+int
 acl_header_regexec(ad, stage, ap, priv)
 	acl_data_t *ad;
 	acl_stage_t stage;
diff -uNr milter-greylist-4.2.5/acl.h milter-greylist-4.2.5-j/acl.h
--- milter-greylist-4.2.5/acl.h	2010-04-19 01:19:36.000000000 +0900
+++ milter-greylist-4.2.5-j/acl.h	2010-04-28 17:31:17.000000000 +0900
@@ -111,6 +111,9 @@
 	AC_SA,
 	AC_SASCORE,
 	AC_RATELIMIT,
+	AC_FROMDOM,
+	AC_FROMDOM_RE,
+	AC_FROMDOM_LIST,
 } acl_clause_t;
 
 struct acl_clause;
@@ -302,6 +305,10 @@
 	           struct acl_param *, struct mlfi_priv *);
 int acl_domain_regexec(acl_data_t *, acl_stage_t, 
 		       struct acl_param *, struct mlfi_priv *);
+int acl_fromdom_cmp(acl_data_t *, acl_stage_t, 
+	           struct acl_param *, struct mlfi_priv *);
+int acl_fromdom_regexec(acl_data_t *, acl_stage_t, 
+		       struct acl_param *, struct mlfi_priv *);
 int acl_body_strstr(acl_data_t *, acl_stage_t, 
 		    struct acl_param *, struct mlfi_priv *);
 int acl_header_strstr(acl_data_t *, acl_stage_t, 
diff -uNr milter-greylist-4.2.5/conf_lex.l milter-greylist-4.2.5-j/conf_lex.l
--- milter-greylist-4.2.5/conf_lex.l	2010-04-15 16:52:16.000000000 +0900
+++ milter-greylist-4.2.5-j/conf_lex.l	2010-04-28 17:26:20.000000000 +0900
@@ -17,6 +17,7 @@
 addr		[Aa][Dd][Dd][Rr]:?
 helo		[Hh][Ee][Ll][Oo]
 from		[Ff][Rr][Oo][Mm]:?
+fromdom		[Ff][Rr][Oo][Mm][Dd][Oo][Mm]:?
 rcpt		[Rr][Cc][Pp][Tt]:?
 peer		[Pp][Ee][Ee][Rr]:?
 verbose		[Vv][Ee][Rr][Bb][Oo][Ss][Ee]
@@ -204,6 +205,7 @@
 {addr}		{ return ADDR; }
 {helo}		{ BEGIN(S_REGEX); return HELO; }
 {from}		{ BEGIN(S_REGEX); return FROM; }
+{fromdom}	{ BEGIN(S_REGEX); return FROMDOM; }
 {rcpt}		{ BEGIN(S_REGEX); return RCPT; }
 {peer}		{ return PEER; }
 {autowhite}	{ return AUTOWHITE; }
diff -uNr milter-greylist-4.2.5/conf_yacc.y milter-greylist-4.2.5-j/conf_yacc.y
--- milter-greylist-4.2.5/conf_yacc.y	2010-04-15 00:32:49.000000000 +0900
+++ milter-greylist-4.2.5-j/conf_yacc.y	2010-04-28 17:34:15.000000000 +0900
@@ -15,6 +15,7 @@
 %token LOGFAC_LOCAL0 LOGFAC_LOCAL1 LOGFAC_LOCAL2 LOGFAC_LOCAL3 LOGFAC_LOCAL4
 %token LOGFAC_LOCAL5 LOGFAC_LOCAL6 LOGFAC_LOCAL7 P0F P0FSOCK DKIMCHECK
 %token SPAMDSOCK SPAMDSOCKT SPAMD DOMAINEXACT ADDHEADER NOLOG RATELIMIT KEY
+%token FROMDOM
 
 %{
 #include "config.h"
@@ -117,6 +118,8 @@
 	|	lines rcptaddr '\n' 
 	|	lines fromregex '\n' 
 	|	lines rcptregex '\n' 
+	|	lines fromdomaddr '\n'
+	|	lines fromdomregex '\n'
 	|	lines domainaddr '\n'
 	|	lines domainregex '\n'
 	|	lines peeraddr '\n' 
@@ -250,6 +253,16 @@
 				acl_register_entry_first(AS_RCPT, A_WHITELIST);
 		}
 	;
+fromdomaddr:	FROMDOM DOMAINNAME {
+			acl_add_clause(AC_FROMDOM, $2);
+			acl_register_entry_first(AS_RCPT, A_WHITELIST);
+		}
+	;
+fromdomregex:	FROMDOM REGEX	 {
+			acl_add_clause(AC_FROMDOM_RE, $2);
+			acl_register_entry_first(AS_RCPT, A_WHITELIST);
+		}
+	;
 domainaddr:	DOMAIN DOMAINNAME {
 			acl_add_clause(AC_DOMAIN, $2);
 			acl_register_entry_first(AS_RCPT, A_WHITELIST);
@@ -799,6 +812,8 @@
 	|	fromregex_clause
 	|	rcptaddr_clause
 	|	rcptregex_clause
+	|	fromdomaddr_clause
+	|	fromdomregex_clause
 	|	domainaddr_clause
 	|	domainregex_clause
 	|	netblock_clause
@@ -984,6 +999,12 @@
 rcptregex_clause:	RCPT REGEX { acl_add_clause(AC_RCPT_RE, $2); }
 	;
 
+fromdomaddr_clause:	FROMDOM DOMAINNAME { acl_add_clause(AC_FROMDOM, $2); }
+	;
+
+fromdomregex_clause:	FROMDOM REGEX { acl_add_clause(AC_FROMDOM_RE, $2); }
+	;
+
 domainaddr_clause:	DOMAIN DOMAINNAME { acl_add_clause(AC_DOMAIN, $2); }
 	;
 
@@ -1496,6 +1517,8 @@
 			{ all_list_settype(glist, AC_FROM_LIST); }
 	|	RCPT OPENLIST email_list CLOSELIST
 			{ all_list_settype(glist, AC_RCPT_LIST); }
+	|	FROMDOM OPENLIST fromdom_list CLOSELIST
+			{ all_list_settype(glist, AC_FROMDOM_LIST); }
 	|	DOMAIN OPENLIST domain_list CLOSELIST
 			{ all_list_settype(glist, AC_DOMAIN_LIST); }
 	|	DNSRBL OPENLIST qstring_list CLOSELIST
@@ -1530,6 +1553,14 @@
 	|	REGEX 	{ list_add(glist, AC_REGEX, $1); }
 	;
 
+fromdom_list:	fromdom_item
+	|	fromdom_list fromdom_item
+	;
+
+fromdom_item:	DOMAINNAME	{ list_add(glist, AC_FROMDOM, $1); }
+	|	REGEX		{ list_add(glist, AC_FROMDOM_RE, $1); }	
+	;
+
 domain_list:	domain_item
 	|	domain_list domain_item
 	;

*1:domainexact が無いと mailexample.jp もマッチします

sendmail と dovecot と SASL と LDAP

SASL CRAM-MD5 のジレンマ

SMTP-AUTHPOP3IMAP で使われる、認証フレームワークの SASL。SASL という名前よりも「CRAM-MD5」といった、具体的な認証方式の方が有名かもしれません。

CRAM-MD5 や、POP3 専用の APOP といった認証方式は、平文のパスワードが保存されているのが前提で、

  • 平文のパスワードを保存しておく。
  • 平文のパスワードがネットワークを流れる。

のどっちかを選ぶ必要があります。で、「平文のパスワードがネットワークを流れる問題なら、SSL/TLS 化しちゃえば OK じゃん」ということで、

  • SSL/TLS 化した上で、平文の認証(PLAIN or LOGIN)

というのが正解、とされます。Dovecot のデフォルトだと、LOGIN や PLAIN を使う場合には、SSL/TLS 化されている事が条件となっていて、「disable_plaintext_auth = no」という Bad Know How が広まっていたりします。

オレオレ証明書を用意すれば SSL/TLS 化は出来るのですが、オレオレ証明書自体が推奨出来るものじゃないし、かと言って、メールサーバへのアクセスの為に証明書のコストを払う事に、あまり理解が得られない状況だったり*1、ましてや、自宅サーバだったりしたら...。

ということで、今でも CRAM-MD5APOP の需要はあると思います。対応しているクライアントも多いし、とりあえず、パスワードが平文でネットワークを流れないし*2

でも、サーバを預かる側としては、平文のパスワードがサーバ上に保存されている、というのは、とにかく落ち着かない。

DIGEST-MD5 なら

CRAM-MD5 の他に、DIGEST-MD5 という認証方式もあって、CRAM-MD5 の様なチャレンジ&レスポンスの認証方式で、ネットワークにパスワードが流れないのですが、実は、この場合だと、平文のパスワードを保存する必要がありません。

RFC 2831の「2.1.2.1 Response-value」をよく見ると、

  response-value  =

     HEX( KD ( HEX(H(A1)),
             { nonce-value, ":" nc-value, ":",
               cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))

If authzid is specified, then A1 is

  A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
       ":", nonce-value, ":", cnonce-value, ":", authzid-value }

If authzid is not specified, then A1 is

  A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
       ":", nonce-value, ":", cnonce-value }

(中略)

If the "qop" directive's value is "auth", then A2 is:

  A2       = { "AUTHENTICATE:", digest-uri-value }

If the "qop" value is "auth-int" or "auth-conf" then A2 is:

  A2       = { "AUTHENTICATE:", digest-uri-value,
               ":00000000000000000000000000000000" }

A1 を計算しているところの「H( { username-value, ":", realm-value, ":", passwd } )」の部分は、先に計算しておいて保存しても大丈夫なのが分かります*3。実際、Dovecot Ver 2 であれば、「doveadm pw」*4でこの値の計算結果を表示します。

$ doveadm pw -s DIGEST-MD5 -u JULY -p password
{DIGEST-MD5}3d206b3a34d0d5f8e54320d57bec654c

この「{DIGEST-MD5}〜」という文字列をそのまま、LDAP の userPassword アトリビュートに設定しておき、DovecotLDAP を参照する様にしておくと、DIGEST-MD5 の認証が出来ます。

ただ、この場合、あくまでもアトリビュートの値として「{DIGEST-MD5}〜」を保持している、というのが LDAP 側の立場で、例えば、ldappasswd でパスワード変更をしても、この値が更新される訳ではありません。

LDAP の userPassword 自体は、同時に複数の値を持つことができ、例えば、OpenLDAP の設定で、slapd.conf に

password-hash {SSHA} {CLEARTEXT}

と書くと、ldappasswd を使ったパスワード変更で、SSHA(ソルト付き SHA ハッシュ値)を保持している userPassword と、平文のパスワードを保持している userPassword の、2つの userPassword が更新されます。

なので、理想的には、

password-hash {DIGEST-MD5} {SSHA}

と書ければ、通常の LDAP の認証や PAM 経由の認証に加えて、SASL の DIGEST-MD5 認証で同じパスワードが使える事になります*5。しかし、残念ながら、上記のような設定は出来ません。

OpenLDAP に overlay というプラグインの仕組があり、この中に smbk5pwd という物があって、これを使うと、Samba とのパスワード同期が実現出来ます。同じように DIGEST-MD5 用の overlay があれば...*6

Dovecot の CRAM-MD5

DIGEST-MD5 に関しては、原理的に平文のパスワードを保存していなくても良い事が分かります。実は、Dovecot の場合、CRAM-MD5 でも同様に、「平文ではないパスワード」を使って CRAM-MD5 が使えるようになっています。

$ doveadm pw -s CRAM-MD5 -p password
{CRAM-MD5}9186d855e11eba527a7a52ca82b313e180d62234f0acc9051b527243d41e2740

この値は、MD5ハッシュ値を計算している最中の中間値らしく、下記のページで解説されています。

Dovecot が保存する CRAM-MD5 認証用パスワード - snbhsmt_log

この計算方法を OpenLDAP の plugin に移植すれば*7

  • LDAP でユーザ情報を一元管理。
  • SASL の CRAM-MD5 が使える。
  • 平文パスワードを保存する必要が無い。

と思ったのですが、落とし穴が...。

SASL の実装

SASL を実装しているライブラリとして、良く使われているのが Cyrus SASL Library です。SASL をサポートする多くのソフトでは、この Cyrus SASL が使われていて、OpenLDAP の SASL 認証も、このライブラリを使っています。

一方、Dovecot は自前で SASL の仕組を実装しています。Cyrus SASL は使いません。POP3IMAP での SASL 認証はこれで良いのですが、困るのは SMTP-AUTH です。

実は、postfix なら Dovecot が実装した SASL を利用できます。逆に、Dovecot の SASL を使える Dovecot 以外のソフトは postfix(と exim)ぐらいです。postfix と並んでよく使われる sendmail は Cyrus SASL を使います。

先の、DIGEST-MD5 や CRAM-MD5 で、「平文のパスワードを保存しないで実現する」というのは、あくまでも Dovecot SASL の実装であって、Cyrus SASL ではそんな事は出来ません。

つまり、OpenLDAP の userPassword を頑張っても、「sendmail + Dovecot」の組み合わせでは使えない事になります。

まとめ

LDAP でアカウントを集中管理して、メール関係だけでなく、OS や Samba も含めて認証情報を統一したい、という要望は、かなり昔からあるんだけど、現実には、ちょっとずつ都合が悪いところがあって、納得した環境にならない、という感じがします。

上記の話だって、「SSL/TLS 化して、CRAM-MD5 や DIGEST-MD5 はサポートしない」とすれば問題はないし、さらに言えば、「CRAM-MD5 や DIGEST-MD5 のような半端な物を使わず、Kerberos を使えば、GSSAPI 経由でシングルサインオン」という話もあります。

ただ、SSL/TLS 化であれば、証明書の管理とコストの問題が付いて回るし、GSSAPI にすると、サポートしていないクライアントソフトが多いし、妥協点として CRAM-MD5 を使おうとすると、上記のような問題にぶつかる、というのは歯がゆいです。

一応、OpenLDAP の部分を頑張れば、postfix(or exim) + Dovecot で良い感じになるのですが、postfix に関しては milter との関係がイマイチだったり、かと言って exim に手を出すのは躊躇するし、と、なかなか、満足のいく組み合わせは無いですね。

*1:既に、www.example.jp という Web サーバ用の証明書を持っていて、同じ FQDN でメールサーバも運用するなら、追加の費用も発生しません。最初からワイルドカード証明書、という手もありますが、メールクライアントがきちんと対応しているかは微妙です。

*2:でも、メールの中身が平文で流れるんですけどね。

*3:但し、同じパスワードでもユーザ名が変わったり、レルム(ドメイン名)が変わると、同じパスワードでも値が変わるので、これらに変更が無ければ、というのが条件。レルムはデフォルトだと空。なので、ユーザ名が JULY、パスワードが password なら、"JULY::password" という文字列に対する MD5 の値の値になります。

*4:Ver 1.x であれば、dovecotpw で同様の事が出来ます

*5:但し、Dovecot が一人のユーザに対して複数のパスワードを持っている場合、扱いが微妙です。Authentication/PasswordSchemes - Dovecot Wiki を読むと、非プレーンテキスト認証、すなわち、CRAM-MD5 や DIGEST-MD5 を、同時に使いたければ、平文のパスワードを保存しておく必要がある、という事が書いてあり、1ユーザに対して複数の違うスキーマのパスワードを使えるようにするのは、「In future」となっています。ところが実際には、Dovecot としては CRAM-MD5 しか使わないけど、pam 経由でも使うから SSHA の userPassword も、という事をやると、LDAP を検索した時に、「2つある userPassword をどの順序で返すか」で Dovecot の認証結果が変わってきます。先に SSHA のハッシュ値が返ってくると、その時点で Dovecot としては諦めてしまっているようです。password-hash で指定する順序を変えて、ldappasswd で更新すると、検索結果も password-hash の順序で返ってくるように見えます。だとすれば、password-hash の順序は、Dovecot に拾わせたい物を先に書く必要があります。ちなみに、pam 経由に認証は、この順序の影響は受けませんでした。ただ、password-hash の順序が検索結果と連動する、とは明確になっていないので、あくまでも「経験的に」です。

*6:overlay 以外にも、単に「plugin」と呼んでいる物があって、SHA256 や SHA512 を使ったハッシュ値を扱えるように出来るのですが、同じインタフェースを使うと、ユーザ名が取れないので、この DIGEST-MD5 用のハッシュ値が計算できません。overlay なら出来そうなんだけど、インタフェースが複雑で、しかも、ドキュメントがほとんど無いので、自分で作るのは無理そう...

*7:この移植は、比較的簡単に出来ました。実際のコードは別の機会にでも。

milter-greylist にまつわるその他諸々

本当はもっと早く書かなきゃいけなかった事など、諸々と。

設定ファイルの更新

greylist.conf の変更後に、milter-greylist の再起動は必要ない。milter-greylist が MTA からの要求を受けた時にファイルが更新されていると、勝手に読み込む。greylist.conf のシンタックスが間違っていないかを確認するには「milter-greylist -c」とすればチェックしてくれる。

もし、milter-greylist が死んだら

milter-greylist に限らず、MTA から milter を呼び出すのに失敗した場合、sendmail のデフォルトでは*1呼び出さなかったのと同じになる。つまり、メールが届かないとか、遅延する、ということはない。その代わり、大量に spam を受け取ることになる。

ただし、Postfix でのデフォルトでは一時エラー扱いになる(参照:Postfix設定パラメータ:milter_default_action)。

もし、greylist.conf の記述にシンタックスに間違いがあった場合、milter-greylist がエラーを返し、sendmail のデフォルトなら spam が流れてくることになり、Postfix のデフォルトならエラーの原因を解消するまで、一切のメールを受け付けなくなる。なので、greylist.conf を書き換えたら「milter-greylist -c」でチェックを忘れずに*2

デフォルトの変更

特に時間に関するデフォルト値は、Postgrey の方が適切な感じがする。前に timeout(Greylist に入ってから、再送が無くて、Greylist から削除されるまでの時間)が 5 日というのは長い、と書いたが、他にいくつか、調整した方が良いと思うパラメータがある*3

delay

Greylist に入ってから、再送を拒否する時間だが、デフォルト 45 分は長い。CentOS 5.4 で生成した sendmail であれば 30 分、Postfix のデフォルトが 1000 秒(16 分 40 秒)だから、45 分では最初の再送が拒否される可能性が高い。実際、5分程度で再送してくるものもあるので、Postgrey のデフォルトに合わせて5分で良いと思う。

delay 5m
autowhite

デフォルトで5日だが、これは、milter-greylist の稼働状況に、どれだけ気を配るかによると思う。

1日に1回、/var/lib/milter-greylist/db/greylist.db を見て、再送によって受信されたメールを確認する場合だと、autowhite は1日でも良いだろう。再送で救われてメールが、本来、受信すべきメールであったのならば、ホワイトリストに追加して遅延を発生させないようすれば良いし、spam だったら「困ったなぁ」と(^^;

greylist.db で自動ホワイトリストに入っているものは、

  • 疑いがかけられたけど、受信したメール

になるわけだから、ここにリストアップされるケースを少なくなるように、ACL を調整するのが良い。

subnetmatch

大規模なメールサービスなどの場合、再送時の IP アドレスは必ずしも一致しない。milter-greylist のデフォルトでは、IP アドレスが完全に一致していないと、再送と認識されない。これも、Postgrey のデフォルトに会わせて、24 bit のネットマスクで評価するようにしておいた方が良い。

subnetmatch /24

Tarpitting

Tarpitting は、TCP のコネクションを維持したまま、サーバからの応答メッセージをわざと遅らせる手法。Ver 4.3.4 で使えるようになっている*4。ただし、Ver 4.3.x は開発版という位置づけである。

この方法は原理的に理にかなっている。というのは、spam を送る側にとっては大量にメールを送る必要があるので、「無言電話」のような時間はもったいない。一方、通常の MTA であれば、数分は無言電話のままでも平気。

ただ、無言電話でも電話代は課金されるのと同様に、無通信でも TCP のコネクションを張っている以上、サーバのリソースを食っていることになる。本来であれば、1通のメールの為に保持しているコネクションなんて、サイズが大きくなければ1秒で終わるものが、分単位で残ることになる。

なので、これも Greylisting と同様、S25RDNSBL で引っかかった物を対象に Tarpitting するのが効果的。Tarpitting を耐えたられたら Greylisting してやる、とすれば、Greylist として保持するエントリ数を少なくする事も出来る。

greylist.conf のサンプル

とまぁ、こんな感じが良いかな、と。

socket "/var/run/milter-greylist/milter-greylist.sock"
dumpfile "/var/lib/milter-greylist/db/greylist.db" 600
dumpfreq 1
user "smmsp"

quiet
subnetmatch /24

list "my network" addr { 127.0.0.1/8 192.168.1.0/24 }

# SPF が pass だった時に許可したい差出人のメールアドレス
list "whitelist with spf" from { \
        /[.@]docomo\.ne\.jp[> ]*$/ \
        /[.@]gmail\.com[> ]*$/ \
        /[.@]apple\.com[> ]*$/ \
}

# DNSBL の結果が OK だった場合に許可したいメールアドレス
list "whitelist with dnsbl" from { \
        /[.@]example\.co\.jp[> ]*$/ \
}

# 差出人が自組織のメールアドレス
list "my domain" from { \
        /[.@]example\.com[> ]*$/ \
}

# DNSBL に spamhaus の ZEN を定義
dnsrbl "ZEN" zen.spamhaus.org 127.0.0.0/28

# 拡張正規表現を使う
extendedregex

# 再送が無かったときに Greylist から消える時間
timeout 12h

# 再送されても拒否する時間
delay 5m

# localhost や内部からのメールは無条件に許可
racl whitelist list "my network"

# SPF の設定が wide open になっている物を拒否
racl blacklist spf self msg "SPF is wide open"

# SPF の結果が pass になるものを許可
racl whitelist list "whitelist with spf" spf pass

# DNSBL の結果が OK だった場合に許可
racl whitelist list "whitelist with dnsbl" no dnsrbl "ZEN"

# 自分のドメインから届くのに、その IP が DNSBL で NG のものは拒否
racl blacklist list "my domain" dnsrbl "ZEN" msg "Where are you?"

# S25R に引っかかるものは Greylisting
racl greylist domain /^\[.+\]$/ msg "S25R rule 0"
racl greylist domain /^[^.]*[0-9][^0-9.]+[0-9].*\./ msg "S25R rule 1"
racl greylist domain /^[^.]*[0-9][0-9][0-9][0-9][0-9]/ msg "S25R rule 2"
racl greylist domain /^([^.]+\.)?[0-9][^.]*\.[^.]+\..+\.[a-z]/ msg "S25R rule 3"
racl greylist domain /^[^.]*[0-9]\.[^.]*[0-9]-[0-9]/ msg "S25R rule 4"
racl greylist domain /^[^.]*[0-9]\.[^.]*[0-9]\.[^.]+\..+\./ msg "S25R rule 5"
racl greylist domain /^(dhcp|dialup|ppp|[achrsvx]?dsl)[^.]*[0-9]/ msg "S25R rule 6"

# DNSBL に引っかかったものは Greylisting
racl greylist dnsrbl "ZEN"

# デフォルトは許可
racl whitelist default

Greylisting の感想

当初、Greylisting しても、ほとんど再送されるんじゃないか、と思ったら、意外なぐらいに再送して来ない。原理的には Tarpitting の方が効果的だと思うが、Greylisting だけでもボットから送ってくるようなものは、ほとんど防ぐ事が出来る。

spam を送る側も、いろいろ工夫してくるとは思うが、今のところ Greylisting の効果は高い。再送するような spam が増えてきたら、サーバの余裕を見て tarpitting を使う、という方向で考えておくと良いかな。

*1:milter プログラムの呼び出しに失敗した時にどうするかは、INPUT_MAIL_FILTER で「F=」の指定で変更出来る。参照:つれづれ日記:sendmail の MAIL_FILTER で指定するオプション

*2:この時、「-c」を忘れると、二重起動になった。で、待ち受ける UNIX ドメインのソケットが後から起動したプロセスに取られるらしく、間違って起動した milter-greylist のプロセスを殺すと、MTA から milter-greylist につながらなくなった。ということで、「-c」は忘れずに。

*3:調整の仕方としては、グローバルな設定として設定する方法の他に、ACL の行で指定する方法がある。この場合だと、「このルールに引っかかった時は delay は1時間」といった具合に、ACL 毎に時間を変える事が出来る。ただし、timeout は ACL では指定出来ない。

*4:CVS から引っ張り出したソースに付いている ChangeLog を見ると、4.3.5 で Tarpitting に関するデッドロックのバグを直した、と書いてある。

milter-greylist で DNSBL を使う

DNSBL は、IP アドレスを元に組み立てた FQDN に対して DNS を引くと、spam の発信源となっている場合には、何らかの A レコード(大抵は 127.0.0.x)を返す、というもの。例えば、IP アドレスが 192.168.1.1 だった時に spamhaus へ問い合わせる場合、

1.1.168.192.zen.spamhaus.org

という FQDN で問い合わせる。もし、spam の発信源として spamhaus に登録されていれば、127.0.0.10 といった結果が返ってくる*1

どのようにして、IP アドレスを登録するかは、DNSBL を提供する側のポリシー次第だが、有名なところとしては、

といったところがある。

DNSBL にも False Positive の危険性はある。が、実際に spamhaus を使ってみた*2ところ、S25R に比べるとはるかに少ない。リストへの登録ポリシーに関して、日本の ISP と一悶着があったらしいけど、実感としては、かなり良い感じがする。

とはいえ、DNSBL に False Postive はつきもの。リストに乗った経験がある人たちがヒステリックに「こんなもの使うな!」と叫んでいるけど、特に海外が発信源となっている spam の多くをはじく事が出来るのは魅力的。

ということで、DNSBL を milter-greylist で使うようにしてみる。

DNSBL の定義

まず、greylist.conf 中で利用する DNSBL に名前を付けてやる。

dnsrbl "ZEN" zen.spamhaus.org 127.0.0.10/28

注意する点はキーワードが「dnsbl」ではなく「dnsrbl」というところ。最後の IP アドレスの範囲は、返ってきた IP アドレスがこの範囲内に入った場合に、DNSBL にリストされていると判断される。同じ、zen.spamhaus.org でも、特定のポリシーのリストにマッチするようにしたければ、範囲を絞れば良い*3

DNSBLGreylisting

DNSBL に引っかかった時に Greylisting したければ、下記のような記述になる。

racl greylist dnsrbl "ZEN" msg "Listed in DNSBL"

DNSBL で Whitelisting

前回(id:JULY:20100109:p1)、SPF との組み合わせでホワイトリストに入れる場合を書いたが、SPF を書いてくれていないところをホワイトリストに入れる際に、DNSBL を参照して「DNSBL に引っかからなかったら、この差出人のメールは受信する」とする。

list "Whitelist with DNSBL" from { \
    /[.@]example\.ne\.jp[> ]*$/ \
}

racl whitelist list "Whitelist with DNSBL" no dnsrbl "ZEN"

「no」は、その後ろの条件を否定するもので、「no dnsrbl 〜」なら、DNSBL にリストアップされていない場合を意味する。例えば、「no spf pass」と書けば、SPF の結果が pass じゃ無かった場合になり、fail や soft fail だけじゃなく、error や none も含まれる。

DNSBL そのもの評価

何かと話題の spamhaus だが、実際に使ってみるとそんなに悪くない。登録されている IP アドレスは多いが、かと言って、False Positive になるケースは目立たない。False Positive なら S25R の方が圧倒的に多い。

日本で運用している RBL.JP は、たまに確認する程度にしか使ってないが、登録されている IP アドレスはかなり少ない。False Positive はその分少ないだろうと思うが、Greylisting するためのふるいとして使うには、物足りない感じがする。逆に、spamhaus でクロで、RBL.JP でもクロだったら、本当にクロだろう、という気にはなるが、本当に拒否するのには、ちょっと勇気が必要。

先に書いた通り、DNSBL に入ってしまった人からの恨みはすごい。そもそも IP アドレスで判断するということ自体、間違っとる! という事を書いている人もいる。確かに、たまたま本当の spammer が同じホストに居合わせたりすると、その巻き添えを食ってしまうので、「DNSBL 憎し」となるのは理解は出来る。しかも、自分に落ち度が無くても、勝手に「こいつは spammer だ」と決めつけられるのだから、腹が立つのは分かる。

とはいえ、有効な手段である事には違いない。問題は、DNSBL にリストアップされている事で無条件に受け取りを拒否したり、捨てたりすることである。Greylisting であれば、遅延は発生するものの、相手には届く。さすがにクライアント PC から直接、相手の MTA にメールを送るのは、現在は「非常識」と言われても仕方がないし、現に、多くの国内 ISPOP25B が実施されている。

遅延して届いた事はログから分かるので、その遅延したメールが spam で無いと分かれば、ホワイトリストに追加すればよい。

ただ、「どういうメールが spam と判断されやすいか?」という事に、無頓着なホスティングサービス業者がいることも確か。S25R に引っかかりやすい FQDN、というのは、管理上の都合もあるのでしょうがないと思うが、

  • 逆引き結果が無い。
  • 逆引き結果を正引きすると、その結果が無い。

というのは困る*4。確かに、Web だけなら正引きの結果さえあれば繋がるので、それで十分と思われがちだが、メールが絡む場合は、きちんとした逆引き結果を用意する、という事が、spam の False Positive を防ぐ第一歩だと思う。

できれば、SPF レコードも書いておけば、DNSBL に登録されても、「うちはちゃんとしているんですよ」という主張を正当化する要因の1つになるだろう(多分(^^;)。

*1:どの IP アドレスが返ってくるかは、「どんなタイプとして登録されているか?」によって変わってくる。spamhaus では4種類のタイプがあり、zen.spamhaus.org を付けると、これら4種類のいずれかに入っていれば、A レコードが返ってくる。どんな IP アドレスになるかは The Spamhaus Project - ZEN を参照

*2:spamhaus を無料で利用するには条件があり、「商品の機能や有料のサービスを提供する目的ではない」「SMTP の接続数が 100,000/日未満」「問い合わせ数が 300,000/日未満」のすべてを満たす必要がある。まぁ、大企業じゃない限り、自分のところに届くメールをチェックする目的で使う分には問題ないでしょう。

*3:といっても、zen.spamhaus.org で返ってくる IP アドレスが、ビット長の表記でうまく分割出来ないのだが...

*4:特に、逆引きの結果を正引きして辻褄が合わないのは、例えば sendmail の場合、ログに「may be forged」と、milter 以前に疑いの目を向けられる事になる。

milter-greylist で SPF を使う

milter-greylist には他の spam 判定要素を組み合わせて、最終的に拒否するのか、許可するのか、Greylisting するのか、という判断をする。例えば、

  • GeoIP を使って、送信元の国によって判断。
  • SpamAssasinの結果を参考にして判断。
  • DNSBL の結果を参考にして判断。
  • SPF の結果を参考にして判断。

といったことが可能(ただし、コンパイル時に有効になっている必要あり)。

で、milter-greylist での SPF の使い方でおもしろい方法がある。

SPF のメリットとデメリット

SPF は、エンベロープの差出人アドレスのドメイン部を使って、DNS の TXT レコードを取得し、そこに記述されている「うちのドメインの付いたメールは、ここから送信するよ」という情報を元に、正規のメールかどうかの判断材料とするもの。送信する側とすると、送ったメールを spam 扱いされないようにするために DNSSPF の情報を用意しておく、といった具合になる。

弱点は、メールが alias や .forward などで、自動転送されるような場合、SPF の結果は「pass」にならない。メーリングリストの場合は、メーリングリストのソフト次第だが、同様の問題が生じる可能性はある。こればっかりは、SPF の性質上、転送するメールサーバ自体がエンベロープの差出人アドレスを書き換えるような仕組みになっていない限り、False Positive を避けることはできない*1

また、False Negative となるケースもある。一見、SPF の結果が「pass」になれば、whitelist 扱いに出来そうな気がするが、spammer が下記のような SPF 情報を記述する場合がある。

v=spf1 +all

こうなると、どんな IP アドレスから届いても、SPF の結果は「pass」になる。本来は「pass」となっていたら、積極的に許可したいのに、無条件に SPF で許可してしまうと spammer の思う壺になってしまう。

milter-greylist + SPF の隠し技*2

普通に、SPF の結果を使った ACL を書くと、下記のようになる。

racl whitelist spf pass

こう書けば、SPF が pass のものは(この行より前にある ACL に引っかからなければ)無条件で受信する。しかし、これでは前述の「どこからでも pass」も通ってしまう。この「どこからでも pass」というケースを検出するための仕組みが、milter-greylist にひっそりと入っている。

greylist.conf の man には、下記のように書かれている。

spf

This is used to test SPF status. Possible values are pass, softfail, fail, unknown, error, none, and self. The first six values are plain SPF validation status. The self value is a special test that checks the server's local IP address against the sender's SPF record. If that test validates, odds are good that the sender SPF record is wide open, and this is hint that SPF should not be trusted.

「self」という独自の値があり、これが指定されると、サーバ自身の IP アドレスを使って SPF レコードをチェックする。もし、その結果が pass だったとすれば、その SPF レコードは「wide open」、つまり「どこからでも pass」という可能性が高いので、その場合の SPF の結果は信用できない、ということになる。

はっきり言って、まっとうな送信元で SPF レコードが「どこからでも pass」になっているのは皆無と考えて良い*3。なので、思い切って、

racl blacklist spf self msg "Your SPF record is wide open."

としても良いだろう。

SPF を使ってホワイトリスト

spf self」を使ってあらかじめ拒否しておけば、「どこからでも pass」で False Nagative となるケースを回避できる。とはいえ、それでも SPF が pass を無条件でホワイトリストにしてしまうのは気が引ける。実際にあったケースとして、spam 発信者がホスティングサービスと契約していて、取得しているドメインSPF レコードに、そのホスティングサービスの SPF レコードを取り込む*4ことで、SPF が pass になるようにしているケースがあった。

もちろん、ホスティングサービスを使っているのだから、ちゃんと再送もする。見かけ上は健全なメールと区別出来ないが、大抵はホスティングサービス側の契約で「spam を送るために使わないこと」みたいなことになっているので、送信元の IP から利用しているホスティングサービスを見つけ出し、その業者に「あんたのとこから、こんなメールが送信されてるよ」と報告すると、業者が対処してくれる場合が多い。

とはいえ、spam 送信者もそんなことは百も承知で、また別のホスティングサービスに乗り換えるだけだとは思うが、それでも、ある期間、大量にメールを送信出来れば OK、と割り切っているのだろう。

とまぁ、こういった spam は SpamAssasin などを使ってメールの中身を評価するしかないので、この際、あきらめることにする(^^;

ただ、S25R による False Positive を避けるために積極的に使う方法もある。

S25R に引っかかりやすいものとして、小規模な ISP の配下やホスティングサービスの他に、大規模なメールサービス、ISP もひっかかりやすい。例えば、携帯電話からのメールや、gmail や Yahoo メールといった場合が S25R に引っかかる。しかも gmail に至っては、SPF に記述されている IP アドレスの範囲がものすごく大きく、再送してくる時の IP アドレスが大きく違ってくる可能性はある*5

こういった「有名どころ」からのメールを SPF で評価してホワイトリストにしてやると、Greylisting による遅延を回避できる。

list "Whitelist with SPF" from { \
    /[.@]docomo\.ne\.jp[> ]*$/ \
    /[.@]ezweb\.ne\.jp[> ]*$/ \
    /[.@]softbank\.ne\.jp[> ]*$/ \
}

racl whitelist list "Whitelist with SPF" spf pass

上記のように書けば、SPF が pass の時にホワイトリストにしたいものを、「list "Whitelist with SPF"」の中に追加すれば良い*6

条件付きホワイトリスト

ホワイトリストというと、送信元の IP アドレスや、送信元メールアドレスのドメイン部だけで判断して通す、というイメージがあるが、

  • IP アドレスだと、相手が引っ越しや ISP の変更、回線種別の変更で IP アドレスが変わった時にホワイトリストから漏れる。
  • 送信元メールアドレスを元にすると、そもそも、送信元メールアドレスは詐称しやすいので、spammer がたまたま、そのメールアドレスを送信元メールアドレスに使うと False Negative になる。

という問題がある。

前述の SPF との組み合わせでれば、上記の問題は解消される。IP アドレスが変わっても SPF が正しければ OK だし、spammer が送信元メールアドレスに使ってもホワイトリストとはならない。

ただ、SPF レコードを設定しているところは多くない。有名どころでは記述しているケースが多いが、中小企業などは記述している方が珍しい。

SPF レコードが無いけどホワイトリストに入れたい時の方法として、「DNSBL に引っかからなければ OK」という方法もある。

ということで、次回は DNSBL との組み合わせを書くことしようっと(^^;

*1:SPF と似た仕組みに Sender ID があるが、こちらはヘッダ情報を参照するので、転送時にヘッダを追加するようになっていると回避できる。ただ、メールヘッダを参照する、ということは、SMTP の RCPT TO の段階では判定できない、ということになる

*2:ただし、Postfix との組み合わせでは、Postfix が {if_addr} マクロを milter 側へ渡さないので利用出来ない。--enable-postfix を付けてビルドすると、明確に「spf self clause is broken on Postfix」と怒られる。milter-manager 経由にすると回避出来るかな?

*3:まぁ、設定した人間が間違って「+all」と書いてしまった、という可能性もないわけではないが...

*4:具体的には、「include:example.jp」と書けば、example.jp の SPF レコードを取り込むこになる

*5:まぁ、見ていると 24 bit 程度のネットマスクで評価(subnetmatch /24 と設定)しておいて、再送メールが永遠に Greylisting されるような事はなさそうではある。

*6:ただ、メールアドレスを正規表現で引っかける場合、大文字小文字を区別してしまうので、本当は「/[.@][Dd][Oo][Cc][Oo][Mm][Oo]\.[Nn][Ee]\.[Jj][Pp][> ]*$/」と書く必要がある。もし、「docomo.ne.jp」だけ書くと、部分文字列による比較なので、xyz@docome.ne.jp.example.com とか docomo.ne.jp@example.com も引っかかってしまう。部分文字列による比較の時は、大文字小文字を区別しない。

milter-greylist だとこんな感じ

Postgrey に比べて、こっちの方が日本語の情報が少ない感じがする。

  • そもそも、Milter って sendmai 発祥。
  • 日本語情報なら Postfix の方が圧倒的に多いし、sendmail は難しすぎる。
  • Postfix 側の設定さえ、ちゃんとしてしまえば、Postgrey はそれなりに動く。
  • それに比べ、milter-greylist は milter-greylist 用の設定ファイルを書かなきゃいけないから面倒。

といった辺りが原因かなぁ、と勝手に思う。

かくいう自分も最初に試したのは Postgrey。milter-greylist を調べ始めたときには、軽くめまいが...。

その他にも導入を妨げる要因として、CentOS 用の milter-greylist のパッケージが無いこともある。いや、あることはある。確かに rpmforge にはある。ところが、とにかく古い。2009-12-26 現在、Ver 4.2.3 が正式リリース版として最新だが、rpmforge にあるのは Ver 3.0。新しいバージョンでは SPF の結果と組み合わせたり、Greylisting にとどまらない、いろんな処理ができるのに、Ver.3.0 ではその辺が...。

ということで、Fedora あたりから拾ってきて、パッケージを作り直すしかない。

で、どうせ作り直すなら、libspf2 も有効にしておきたい。後述(の予定)するが、milter-greylist と libspf2 の組み合わせで、おもしろい処理ができる。

しかし、libspf2 のパッケージもなかなかなくて、別の Milter プログラム(smc_milter)が用意したパッケージとか、Peter Pramberger さんのパッケージ(http://www.pramberger.at/peter/services/repository/rhel5/)から取得することになる。

ということで、参考までに自分が要した手順を書くと、こんな感じ。

  1. まず、gcc とか bison、flex、rpmbuild といった、ビルドに必要なプログラムをインストール。
  2. Peter Pramberger さんのサイトから、libspf2 のパッケージをダウンロードしてインストールする。
  3. Fedora から milter-greylist のソースパッケージを取得する。
  4. ソースパッケージをばらして、spec ファイルを適時編集。
  5. rpmbuild -ba とかで、パッケージを作成

...って、かなり敷居が高いなぁ(^^;

その辺はともかく、無事、milter-greylist がインストールできたとする(料理番組みたいだなぁ)。

greylist.conf の基本

  • 「#」の後ろは行末までコメント
  • キーワードと値の間はスペースで、「=」などは入らない。
  • 行末で「\」とすれば継続行
  • 自分で定義した「名前」はダブルクォーテーションでくくる。
  • 正規表現を使う場合は「/」でくくる。
  • 時間を表す値の場合、数値の後ろに「d」「h」「m」「s」を付ける。

milter-greylist の設定ファイルは、/etc/mail/greylist.conf(Fedora ベースのパッケージの場合)になる。ファイルに記述する内容は、主に 3 つに分けられる。

グローバルな設定

milter-greylist の全体的な挙動を設定する。たとえば、milter-greylist の動作ユーザ(sendmail が相手で CentOS 上だと smmsp)とか、待ち受けするための UNIX ドメインソケットのパスとか。

グローバルな設定で調整が必要なのは timeout の設定で、デフォルトで 5 日になっている。この timeout の期間に同じ IP アドレスから、同じ差出人、宛先アドレスのメールが来れば再送と見なされる。

確かに、再送を繰り返して、あきらめるのが 5 日、という MTA は多いと思うが、あくまでも spam 除けとして、意図的に一度だけ一時エラーを返すのだから、5 日間も Greylisting で再送待ちをすると、自動ホワイトリスト入りするのが多くなってしまう。せいぜい長くても 1 日、12 時間程度でも十分だと思う。

それと、後々、S25R のパターンを書く時に拡張正規表現を使いたいので、extendedregex は指定したい。これを書かないと標準正規表現になる。

書き方はこんな感じ

timeout 12h
extendedregex

名前の定義

後で動作のルールを記述する時に利用する「名前」を定義する。

たとえば、無条件で転送を許可したい IP アドレスを列挙して、名前を付けておき、ルールを記述するときに利用する。

list "my network" addr { 127.0.0.1/8 10.0.0.0/8 192.0.2.0/24 }

上記のように書けば、IP アドレスが 127.0.0.1/8、10.0.0.0/8、192.0.2.0/24 の範囲にある場合を "my network" という名前でルールに記述できるようになる。

また、milter-greylist 中で DNSBL を使う場合には、利用する DNSBL をあらかじめ定義しておく必要がある。

dnsrbl "ZEN" zen.spamhaus.org 127.0.0.0/29

ルール

milter-greylist としては ACL と呼んでいるが、ACL と呼ぶのが適切なのか、個人的には微妙な気がするけど...。

ACL には「racl」「dacl」の 2 つがある。

  • racl ...... SMTP の RCPT TO を受け取った段階で評価する。
  • dacl ...... SMTP の DATA の後に送られてくるメールの内容で評価する。

ちなみに、Ver.3 では acl というキーワードを使っていたが、Ver.4 ではまだ互換性を持っているみたいだけど、非推奨になっている。

「racl」「dacl」のキーワードに続いて記述するのは、「blacklist」「greylist」「whitelist」の 3 つ。

  • blacklist ...... 受け取りを拒否する(SMTP の 500 番台のエラーを返す)。
  • greylist ...... Greylisting する(SMTP の 400 番台のエラーを返し、Greylist に記録する)。
  • whitelist ...... 受け取る 。

ACL は記述した順序で評価され、最後の ACL に「どの条件にも引っかからなかったらどうするのか」を記述する。

一番単純に、Greylisting をしたければ、こんな感じになる。

list "my network" addr { 127.0.0.1/8 10.0.0.0/8 192.0.2.0/24 }

racl whitelist list "my network"
racl greylist default

こう書けば、"my network" 以外からやってきたメールはすべて Greylisting される。

S25R だけを対象に Greylisting をしたい場合は、こんな感じになる。

list "my network" addr { 127.0.0.1/8 10.0.0.0/8 192.0.2.0/24 }
extendedregex

racl whitelist list "my network"
racl greylist domain /^\[.+\]$/ msg "S25R rule 0"
racl greylist domain /^[^.]*[0-9][^0-9.]+[0-9].*\./ msg "S25R rule 1"
racl greylist domain /^[^.]*[0-9][0-9][0-9][0-9][0-9]/ msg "S25R rule 2"
racl greylist domain /^([^.]+\.)?[0-9][^.]*\.[^.]+\..+\.[a-z]/ msg "S25R rule 3"
racl greylist domain /^[^.]*[0-9]\.[^.]*[0-9]-[0-9]/ msg "S25R rule 4"
racl greylist domain /^[^.]*[0-9]\.[^.]*[0-9]\.[^.]+\..+\./ msg "S25R rule 5"
racl greylist domain /^(dhcp|dialup|ppp|[achrsvx]?dsl)[^.]*[0-9]/ msg "S25R rule 6"
racl whitelist default

「domain」は接続してきた IP アドレスを逆引きした結果で、逆引き自体は MTA 側で行われる。逆引きの結果が無ければ、"unknown" という文字列が MTA から渡ってくる。で、逆引きの結果が S25R のパターンに該当したら Greylisting をする、というのが 7 行続いている*1

「msg」は、SMTP でエラーコードを返した際に付けられるメッセージで、これがログに残るので、後でどの ACL に該当したのか、という手がかりになる。

ACL はこの順序で評価されるので、上記の記述なら、

  1. "my network" からの接続は無条件に受け取る
  2. S25R に該当した場合は、どのルールに該当したのかをメッセージとして出力させて Greylisting する。
  3. "my network" でもなく、S25R にも引っかからなければ受け取る。

という事になる。これで「S25R に引っかかった時だけ Greylisting」が実現できる。

...... だいぶ長くなったので、SPF を使ったテクニックなどは次回へ(^^;

*1:この記述は、http://milter-manager.sourceforge.net/blog/ja/2009/5/3.html で紹介されている内容を引用