LDAPデータベースの構築

バーチャルメールユーザのアカウント情報管理データベースを構築する。OpenLDAP の基本については LDAPのページを参照のこと。

※ LDAP の "ディレクトリ" という語彙はファイルシステムの "ディレクトリ" と区別しにくいため、当ドキュメントでは敢えて「データベース」と呼んでいる。

Qmail-ldapパッチの入手とqmail.schemaファイルの取り出し

まず、Qmail-LDAP プロジェクトのホームページから qmail-ldap-1.03-xxxxxxxx.patch.gz をダウンロードする。 それを解凍し、qmail-ldap-1.03-xxxxxxxx.patch ファイルの

diff -upN qmail-1.03/qmail.schema qmail-ldap/qmail.schema
--- qmail-1.03/qmail.schema Thu Jan 1 01:00:00 1970
+++ qmail-ldap/qmail.schema Mon Dec 6 13:55:26 2004
@@ -0,0 +1,279 @@
+#
+# qmail-ldap (20030901) ldapv3 directory schema

から、次の

diff -upN qmail-1.03/qmail.sh qmail-ldap/qmail.sh
--- qmail-1.03/qmail.sh Thu Jan 1 01:00:00 1970
+++ qmail-ldap/qmail.sh Mon Dec 6 13:55:26 2004

の直前までの行を別ファイルに保存する。/home/hoshu/cabinet/qmail.schema.patch ファイルとして保存したとしよう。そうしたら、空のディレクトリでパッチを適用することによって qmail.schema ファイルを生成する;

hoshu$ cd ~/cabinet
hoshu$ mkdir qmail-ldap
hoshu$ cd qmail-ldap
hoshu$ patch -p1 <../qmail.schema.patch

できた qmail.schema ファイルを root 権限で /etc/openldap/schema/ にコピーする。

slapdの設定

使用するクラス/スキーマをまとめると下表のようになる。■地は直接使用するクラス、■のものは直接使用クラスが継承しているため暗黙的に使用されるクラス。

Class 父Class 祖父Class 曾祖父Class 直接使用Classのタイプ Class提供スキーマ
      Top - core.schema
    uidObject   AUXILIARY
inetOrgPerson organizationalPerson person   STRUCTURAL inetorgperson.schema
(cosine.schema)
    posixAccount   AUXILIARY nis.schema
    qmailUser   AUXILIARY qmail.schema

※ cosine.schema は、inetorgperson.schema の中の一部のクラスがそれに依存しているためロードする必要がある。

オリジナルの /etc/openldap/slapd.conf を、必要ならバックアップしてから、下記の内容に置き換える。

/etc/openldap/slad.conf (単体ファイルは こちら)
include		/etc/openldap/schema/core.schema
include		/etc/openldap/schema/cosine.schema
include		/etc/openldap/schema/inetorgperson.schema
include		/etc/openldap/schema/nis.schema
include		/etc/openldap/schema/qmail.schema
 
allow bind_v2  <--これがないとPostfixでは動作が成り立たなかった
 
pidfile		/var/run/openldap/slapd.pid  <--※1
argsfile	/var/run/openldap/slapd.args
 
access to dn.subtree="ou=mail,o=hoge,dc=cxm"  <--LDAPアカウント mailadmin しか読めないよう規制
	by dn="cn=mailadmin,ou=mail,o=hoge,dc=cxm" read
	by anonymous auth
	by * none
 
database	bdb
suffix		"o=hoge,dc=cxm"
rootdn		"cn=Manager,o=hoge,dc=cxm"
rootpw		{SMD5}UFq1pseeDIL3bt6CzTMw6uJUdyg=   <--※2
directory	/var/lib/ldap
 
index objectClass                       eq,pres
index ou,cn,mail,surname,givenname      eq,pres,sub
index uidNumber,gidNumber,loginShell    eq,pres
index uid,memberUid                     eq,pres,sub
index nisMapName,nisMapEntry            eq,pres,sub
 
password-hash	{CLEARTEXT}   <--※3
※1
LDAPのページの「サーバの基本設定」を参照。
※2
`/usr/sbin/slappasswd -h '{SMD5}' -s PASSWORD ' で吐き出させたものを貼り付ける。
※3
ldappasswd など パスワード変更拡張手順 に則ったプログラムでパスワードを更新する際のパスワード格納形式。LDAP データベースにパスワードをどういう手順で登録するかという運用ポリシーによって、最適な形式は違ってくる。Dovecot 0.99 とに併用における現実的な候補は {CLEARTEXT} か {CRYPT} のどちらかに絞られる。詳しくは「メールユーザの登録」で述べる。

準備が整ったら LDAP サーバデーモン slapd を開始し、エラーが出なければスタートアップに登録;

root# service ldap start
root# chkconfig ldap on

データベースフレームワークの作成

Directoryツリーの作成

これから構築するのは左図のようなLDAP Directory ツリー。"o=hoge,dc=cxm" 直属の "cn=Manager"(rootdn) や、"ou=mail,o=hoge,dc=cxm"サブツリーの "cn=mailadmin" は LDAP管理/運営用の特別なユーザ、"cn=stray" や "cn=penguin" などが実際のバーチャルメールドメインユーザだ。

まずこの項では、入れ物となるツリーを作成し、管理用ユーザの登録までを行う。

サンプルとして、LDIF ファイル framework.hoge.cxm.ldif を用意しておいたので、適宜編集し、ldapadd で投入する;

hoshu$ ldapadd -x -D "cn=Manager,o=hoge,dc=cxm" \
  -w PASSWORD \
  -f framework.hoge.cxm.ldif

投入できたか確認してみる;

hoshu$ ldapsearch -x -LLL -D "cn=Manager,o=hoge,dc=cxm" -w PASSWORD \
  -b "o=hoge,dc=cxm"

データ読み取り専用ユーザの登録

次にユーザデータ読み取り専用の管理ユーザ mailadmin を登録する。サンプルファイルは mailadmin.ldif。userPassword 属性値は、後述の「事前MD5方式」や「LDAP=CRYPT方式」に関係なく LDAP-SMD5 形式で書いておく (サンプルファイルのパスワードは `password')。では登録しよう;

hoshu$ ldapadd -x -D "cn=Manager,o=hoge,dc=cxm" -w PASSWORD \
  -f mailadmin.ldif

好みのパスワードに変更するには、運用上の password-hash 設定に影響されずに希望の形式で格納させるため、敢えて、パスワード変更拡張手順の使用されない ldapmodify コマンドで行う;

hoshu$ /usr/sbin/slappasswd -h '{SMD5}' -s Password
{SMD5}xxxxxxxxxxxxxxxxx=
hoshu$ ldapmodify -x -D "cn=Manager,o=hoge,dc=cxm" -w PASSWORD
dn: cn=mailadmin,ou=mail,o=hoge,dc=cxm
userPassword: {SMD5}xxxxxxxxxxxxxxxxx= <--上記 slappasswd で出力されたものを貼り付ける
<Enter>
<Ctrl + d>

メールユーザの登録

エントリ属性の解説

qmail.schema によって実装される qmailUser クラスの力を借りて下表のような属性を備えたエントリを作成する。qmailUser クラスの属性の説明は Qmail-LDAP プロジェクトホームページの Category:LDAP Fields にある。属性の味付けや使い方は Postfix と Dovecot の LDAP参照定義でどうにでもなるのだが、なるべく qmailUser の規格に逆らわない使い方をしている。

ユーザ属性一覧
[凡例]  
必須 列: ◎=属性規格上も運用上も必須, □=属性の規格上必須, ○=運用上必須, △=省略可能
型 列: IA5=文字列型, int=数値, OctStr=オクテット文字列, DirStr=LDAPディレクトリ文字列
Case 列: 検索時に大文字小文字の区別がされるか。「無視」の場合でもデータ登録時に大文字が登録できないわけではない
比較 列: 検索時のマッチ条件。完全=長さも等しくなければ一致しない, 数値=数値として比較, OctStr=オクテット文字列比較
多重 列: 同じ属性を別の値で複数回登録できるか
Attribute 説明 属性パターン 属性提供元
最上流クラス
必須 型 Case 比較 多重
cn メールアドレスの @domain.tld より前の部分。 ◎ DirStr 無視 完全 不可 person
sn 本来の意味は苗字だが、実際には何にも活用されない。便宜上 organization (つまり "hoge") を入力しておく。複数のバーチャルメール(サブ)ドメインを運用する場合にはサブドメイン名 (例えば "sales") を入れておくという使い方もいいかもしれない。 ◎ DirStr 無視 完全 不可 person
uid cn と同じものを入力する。 ◎ DirStr 無視 完全 不可 uidObject
mail 正メールアドレス。ドメイン部も含めて stray@hoge.cxm のように登録する。 ◎ IA5 無視 完全 不可 inetOrgPerson
uidNumber Dovecot が内部的に使用する。全員共通でシステムユーザ mailadmin の UID を登録しておいてもいいが、できれば一意となるように 500, 501, 502... といった値を登録しておくのが確実。 □ int - 数値 不可 posixAccount
gidNumber 上記同様。こちらは全員必ず mailadmin の GID を登録しておく。 □ int - 数値 不可 posixAccount
userPassword Dovecot の POP3 認証用パスワード。Dovecot 0.99 で使うには、登録時に平文で渡して LDAP 側で CRYPT させるか、あらかじめ MD5 ハッシュしておいたものをそのまま格納させるかのどちらか。(※補足1) ○ OctStr 厳格 OctStr 不可 person
homeDirectory メールボックスの所在。Postfix (virtual_mailbox_maps 機能を経由) でも Dovecot (auth_userdb) でも使用される。/users/USERNAME の形で登録 (例えば /users/penguin)。当該ユーザのメールボックスパスは、Postfix, Dovecot の然るべき設定によって、最終的に /var/vmail<homeDirectory>/Maildir/ と解釈される。(※補足2) ○ IA5 厳格 完全 不可 posixAccount
mailForwardingAddress Forward または BCC 先メールアドレス。この属性の値を Forward 先として扱うか BCC 先として扱うかは下記の deliveryMode で決まる。Forward の場合に限っては、この属性を別の値で複数回登録しておくことも可能で、そうすると、受信メールを複数のメールアドレスへ転送することができる。BCC の場合の複数登録は Postfix がサポートしておらず、メールヘッダが異常なものになり送信不能となる。注意して使えば多重転送も可能だが、Postfix のループ防止機構により以下の制限がある;
●Forward先ユーザで更に Forward : 可能
●BCC先ユーザで更に Forward : 可能
●Forward先ユーザで更に BCC : 不可能
●BCC先ユーザで更に BCC : 不可能
△ IA5 無視 完全 可 qmailUser
deliveryMode mailForwardingAddress が Forward 先か BCC 先かを決定する。値は 3種類;
●noforward : 通常の配送。mailForwardingAddress が登録してあったとしても、Forward も BCC もしない。
●nolocal : Forward 動作。正メールボックスには配送せず mailForwardingAddress にだけ配送する。
●この属性が存在しない時 : BCC 動作。正メールボックスとともに mailForwardingAddress にも配送する。
△ IA5 無視 完全 不可 qmailUser
accountStatus このアカウントが有効か無効か。ユーザエントリを LDAP データベースから削除しなくても、そのユーザをいないことにできる。値は 2種類;
●disabled : 無効。LDAP検索にヒットしない。直接メールを受け取らないアカウントだとしても Forward/BCC 元として参照される必要がある場合には無効にしてはならない。
●active またはこの属性が存在しない時 : 有効。通常通り LDAP検索にヒットする。
△ IA5 無視 完全 不可 qmailUser
補足
userPassword
「登録時に平文で渡して」というのは正確な表現ではない。ldapadd や ldapmodify はパスワード変更拡張手順を使わない。よって、slapd.conf で "password-hash {CRYPT}" にしてある場合には、アカウント新規登録時は userPassword を暫定値で入れておいて、後で ldappasswd コマンドで上書きしてやる必要がある。逆に、MD5 ハッシュで格納したい場合には、slapd の設定を "password-hash {CLEARTEXT}" にしておき、ユーザを ldapadd で新規登録する際の LDIF ファイルにあらかじめ MD5 ハッシュした userPassword 値を書いておく。Dovecot 0.99 ではまだ、LDAPの生成する MD5 形式 (LDAP-MD5) を平文パスワードと比較する能力がないからだ。事前の MD5ハッシュ化は、Perl の Digest::MD5 モジュールで行うとうまくいく。Linux の md5sum コマンドで作ったものは Dovecot が読めない。筆者の作成した 平文パスワード -> MD5 変換 Perl スクリプトを置いておく (mkmd5.pl、といっても呆れるほど短い)。

以降、説明時に長くなるのを避けるため、LDAP に CRYPT させるやり方を 「LDAP-CRYPT方式」、あらかじめ MD5 ハッシュして LDAP 側でそれを平文として扱うほうを「事前MD5方式」と呼ぶことにする。CRYPT にはパスワードの最初の 8 バイトしか読み取らないという制約があり、基本的に長さの制限のない MD5 のほうがセキュリティ面や運用の柔軟性で勝る。
homeDirectory
/users/ を付けておくのは運用に柔軟性を持たせるため。或る一群のアカウントだけこのプレフィクスを例えば /users2/ にすることによって、それらユーザだけは /var/vmail/users2/ 下にメールボックスを配置したり、例えば sales.hoge.cxm というサブドメインのメールボックスを sales/ に分離するという運用も可能となる。Postfix と Dovecot で最終的に得られる SMTP配送/POP3読み取り先パスを同じにするには Postfix と Dovecot の LDAP参照定義に少々トリックを要するが、それは各デーモンの設定の章で述べる。

ユーザの登録

いよいよ実際にバーチャルメールドメインユーザを登録する。ただし、Postfix と Dovecot の設定が完了するまでは、2~3 のテストユーザを登録するに留めておくのがいいだろう。使っているバージョンの Dovecot がやりたいことに対応していなかったりして途中で実装方針が変わり、大量のエントリを全部登録しなおすハメになるのは悲しいではないか。

LDIF ファイル上の 1エントリは下のような記述になる。サンプルファイル (users.mail.hoge.cxm.ldif) を置いておくので、雛形にするといいだろう。

dn: cn=penguin,ou=mail,o=hoge,dc=cxm
objectClass: uidObject
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: qmailUser
uid: penguin
cn: penguin
sn: hoge
mail: penguin@hoge.cxm
uidNumber: 502
gidNumber: 1025
userPassword: 5f4dcc3b5aa765d61d8327deb882cf99
homeDirectory: /users/penguin
deliveryMode: noforward
accountStatus: active
パスワードを「事前MD5方式」で運用する場合

上記のサンプルファイルの userPassword 属性の値は、事前MD5方式 を前提としている。5f4dcc... は "password" というパスワードを mkmd5.pl スクリプトで MD5 ハッシュしたものだ。好きなパスワードのハッシュに入れ替えたければ、

hoshu$ mkmd5.pl sukina-password

として吐き出されたものを貼り付ける。そして登録;

hoshu$ ldapadd -x -D "cn=Manager,o=hoge,dc=cxm" -w PASSWORD \
  -f users.mail.hoge.cxm.ldif
パスワードを「LDAP-CRYPT方式」で運用する場合

LDAP-CRYPT方式 の場合、投入時の userPassword はダミーにしかならないので、とにかく何か書いてあればいい。まず登録;

hoshu$ ldapadd -x -D "cn=Manager,o=hoge,dc=cxm" -w PASSWORD \
  -f users.mail.hoge.cxm.ldif

そして、本当のパスワードに書き換える。上記サンプルで示した penguin アカウントの例;

hoshu$ ldappasswd -x -S -D "cn=Manager,o=hoge,dc=cxm" -w PASSWORD \
  "cn=penguin,ou=mail,o=hoge,dc=cxm"
New passsword: password
Re-enter new password: password
パスワードの確認

意図した形式で格納されたかどうか確認したい場合は、例えば以下のようにすればよい。userPassword 属性は Base64 でエンコードして格納されていることをお忘れなく。これまた penguin アカウントを例に採る。

hoshu$ ldapsearch -x -LLL -D "cn=mailadmin,ou=mail,o=hoge,dc=cxm" -w PASSWORD \
  -b "ou=mail,o=hoge,dc=cxm" -s one \
  '(&(objectClass=inetOrgPerson)(cn=penguin))' userPassword
dn: cn=penguin,ou=mail,o=hoge,dc=cxm
userPassword: xxxxxxxxxxxxxxxx=

下記コマンドの xxxx の部分に上記応答の xxxxxxxxxxxxxxxx= を貼り付けて、

hoshu$ perl -MMIME::Base64 -e 'print decode_base64("xxxx"), "\n";'

とすれば、「事前MD5方式」の場合は LDIF に書いたままの文字列、「LDAP-CRYPT方式」なら '{CRYPT}yyyyyyyy ' といった文字列が得られるはずだ。

ユーザメールボックスの作成

Postfix は、バーチャルメールユーザへの初めてのメールを配送する時に配送先ディレクトリが存在しないと、親ディレクトリも含めて自動的に作成しようとする。しかし、勝手に作られるのは気持ち悪い。また、1通も受信しないうちにそのユーザが POP3 アクセスして来るかもしれない。Dovecot は勝手にディレクトリを作ったりしないので、ユーザのメールクライアントへエラーが返る結果となってしまう。そこで、ユーザメールボックスは、LDAP データベースにユーザエントリを登録し次第、管理者側で用意してやることにする。下記に示す階層構造 (penguin/ 以下) を適切なパーミッションで作成しなければならない。

/var/vmail/users/
             \_ penguin/            [mailadmin:mailadmin 700]
                    \_ Maildir/     [mailadmin:mailadmin 700]
                            \_ new/ [mailadmin:mailadmin 700]
                               cur/ [mailadmin:mailadmin 700]
                               tmp/ [mailadmin:mailadmin 700]

いちいち手作業でこれをやるのは面倒臭いしミスも起こりやすいので、シェルスクリプトにした - vmaildirmake

root# vmaildirmake USER

という具合に使う。引数なしで呼ぶと簡単なヘルプが表示される。基底ディレクトリが /var/vmail/users/ 以外の時や読み書き専用 UNIXアカウントの名前が mailadmin でない場合はスクリプトファイル冒頭の変数を適宜調整していただきたい。

エイリアスユーザの登録

何かの条件下で、バーチャルメールドメインの root や postmaster つまり、root@hoge.cxm や postmaster@hoge.cxm へメールが届くケースがないともいえない。特に、メールトラブルなどの連絡先として postmaster を公表している場合だ。しかし、このメールサーバで複数のメールサブドメインを運営する場合、それぞれのバーチャルドメインの postmaster へメールが行くのは運用上煩雑だ。そこで、後述の Postfix 設定で、virtual_alias_maps 機能を利用して全てのバーチャルメールドメイン配下の特別なアカウントを実アカウントへ一旦エイリアスし、さらにマシンローカルの aliases ファイルで最終的なユーザなりメールアドレスなりへとエイリアスする。

そこで肝となるのが、そもそも Postfix は、アカウントデータベースに存在しないユーザへのメールは SMTPセッション段階で拒絶し、virtual_aliases を参照するまでに至らないという点だ。そのため、とにかく存在する ということを示すためだけに、それら特別なアカウントもダミー的に LDAP のユーザ階層に登録しておく必要がある。

ほとんどの場合、サンプルファイル aliasusers.mail.hoge.cxm.ldif に挙げたアカウント群で間に合うだろう。そのひとつ、postmaster の LDIF エントリを覗いてみるとしよう;

dn: cn=postmaster,ou=mail,o=hoge,dc=cxm
objectClass: uidObject
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: qmailUser
uid: postmaster
cn: postmaster
sn: hoge
mail: postmaster@hoge.cxm
uidNumber: 1025  <--使われないが、mailadmin の UID を登録
gidNumber: 1025  <--使われないが、mailadmin の GID を登録
userPassword: ffffffffffffffffffffffffffffffff  <--パスワードはダミーで構わない
homeDirectory: /users/postmaster  <--使われないが、他のバーチャルユーザと同様にしておく
deliveryMode: noforward  <--転送しない noforward にしておく
accountStatus: active    <--必ず active に

LDAP に登録する;

hoshu$ ldapadd -x -D "cn=Manager,o=hoge,dc=cxm" -w PASSWORD \
  -f aliasusers.mail.hoge.cxm.ldif
Postfix と Dovecot のLDAPクライアント実装状況について

ネットワークパケットをスニフして分かったのだが、Postfix の LDAPクライアント実装は非常に汚い。問い合わせの度に bind してくるのはまあいいとして、用事が終わっても unbind もせずにコネクションを放ったらかしにしてくれるのだ。しかも、複数の機能 (設定ディレクティブ) で LDAPを参照している場合 (本稿の実装例もそう)、ひとつの SMTPセッションで開始/放置される bind は 1本どころではない。bind 処理は LDAP サーバにとって軽い処理ではない。また、LDAP サーバアプリケーションによっては、bind セッションの数が飽和してしまう可能性もあるので、実装に際しては LDAPサーバのチューニングが必要になることがある。

一方の Dovecot は、オフィシャルマニュアルの質や LDAP参照定義の柔軟性では全然褒められないものの、LDAPクライアントの実装は非常にシンプルで行儀正しい。Dovecot は POP3 セッションではなく デーモンの起動時 に bind を行い、LDAPとの接続に何か異常の生じない限りそれを再利用し続ける。unbind はデーモンの停止時に行われる。ただし、裏を返せば、LDAP 自体やその経路の異常には少々弱い面があり、bind セッションが不意におかしな切れ方をした場合には Dovecot デーモンを再起動しないと正常な問い合わせが成り立たないことがある。