そろそろLDAPにしてみないか?

第3回もう一歩進んだUNIXアカウント

第1回から紹介してきたLDAPによるUNIXアカウント管理ですが、実際に運用する上ではまだまだ問題が残っています。

MigrationToolsによる既存ユーザのインポート

前回までは手動でLDIFファイルを作成し、そのデータをLDAPサーバへインポートしたわけですが、/etc/passwdや/etc/groupをLDAPに移行させるための便利なツールがあります。それがMigrationToolsです.

これらは/etc以下にあるパスワード、グループ、ホスト、サービス情報などをLDAPに移行させるためのLDIFを作成してくれるツールで、Perlスクリプトの集合体となっています。試しに既存の/etc/passwd、/etc/group、/etc/hostsの情報をLDIF形式にしてみましょう(図1)。事前に設定ファイルとなるmigrate_common.phを編集したり、適切な環境変数を定義することで作成されるLDIFのBASE_DNなどの値を変更することができます。

図1 MigrationToolsを使用したLDIFの作成
% tar zxfv MigrationTools.tgz
% cd MigrationTools-47
# export LDAP_BASEDN="dc=example,dc=com"
# ./migrate_passwd.pl /etc/passwd passwd.ldif
# ./migrate_group.pl /etc/group group.ldif
# ./migrate_hosts.pl /etc/hosts hosts.ldif

カレントディレクトリにpasswd.ldif、group.ldif、hosts.ldifが作成されましたので、それぞれのエントリを追加してみましょう。現在はグループ、ホスト情報用の親エントリがない状態ですので、リスト1のような親エントリを登録した後それぞれを登録します(図2)。

リスト1 base.ldif
dn: dc=example,dc=com
objectClass: dcObject
objectClass: organization
dc: example
o: example

dn: ou=People,dc=example,dc=com
objectClass: organizationalUnit
ou: People

dn: ou=Group,dc=example,dc=com
objectClass: organizationalUnit
ou: Group

dn: ou=Hosts,dc=example,dc=com
objectClass: organizationalUnit
ou: Hosts
図2 LDIFファイルのインポート
% ldapadd -x -c -D "cn=manager,dc=example,dc=com" -w secret -f base.ldif
% ldapadd -x -c -D "cn=manager,dc=example,dc=com" -w secret -f passwd.ldif
% ldapadd -x -c -D "cn=manager,dc=example,dc=com" -w secret -f group.ldif
% ldapadd -x -c -D "cn=manager,dc=example,dc=com" -w secret -f hosts.ldif

注意点として、migrate_passwd.plは/etc/shadow中の暗号化されたパスワードエントリの頭に{crypt}という文字をつけてuserPassword属性の値とします。したがって、/etc/shadow中にMD5形式のパスワードが定義されていた場合でも、

userPassword: {crypt}$1$YBwRQlei$Lm.vrBe8AUFoxjcka35wT/

のように、cryptと定義してあるのに実際の値はMD5、と、つじつまの合わない形で出力されてしまうため、必要に応じてuserPassword属性の値を平文、または暗号化された形式に修正すべきです。参考までに、エンコード後の文字列を作成するにはslappasswdコマンドを使用します(表1参照)。

表1 暗号方式によるコマンドの違い
形式コマンド例
CRYPT形式slappasswd -h '{CRYPT}' -s パスワード文字
MD5形式slappasswd -h '{MD5}' -s パスワード文字
SMD5形式slappasswd -h '{SMD5}' -s パスワード文字
SSHA形式slappasswd -h '{SSHA}' -s パスワード文字
SHA形式slappasswd -h '{SHA}' -s パスワード文字
平文特になし

詳細はslappasswdコマンドのマニュアルを参照してみてください。

nscdによるキャッシング

少し話は変わりますが、idコマンドを使うと/etc/nsswitch.confに定義されたネームサービス情報を元にユーザ情報を表示させることができます。たとえばリスト2のようなシェルスクリプトを実行すると、LDAP上に登録されたldapuserの情報を100回表示します。

リスト2 ユーザ情報を100回表示させるスクリプト(count.sh)
#!/bin/sh
i=1
while [ $i -le 100 ]; do
  id ldapuser
  i=`expr $i + 1`
done

実際にこのスクリプトを実行してみるとわかりますが、LDAP上のユーザ情報を取得するためには、思いの外時間がかかってしまうことに気がつくはずです図3、4参照⁠⁠。

図3 /etc/nsswitch.confにてLDAP検索が有効な場合
# time ./count.sh 

real    0m8.450s
user    0m0.911s
sys     0m6.531s
図4 /etc/nsswitch.confにてLDAP検索が無効な場合
# time ./count.sh 

real    0m3.586s
user    0m0.382s
sys     0m3.199s

この問題は、ユーザ情報をある程度メモリ上にキャッシュしておけば防止することができます。そのためのデーモンがnscd(Name Service Cache Daemon)です.nscdを使用すると、最初の情報取得時にはLDAPサーバから検索を行い、以降はキャッシュされた値を使用することができます(図5)。

図5 nscdを有効化した状態でcount.sh(リスト2)を実行
# /etc/init.d/nscd start
# chkconfig nscd on
# time ./count.sh 

real    0m3.191s
user    0m0.361s
sys     0m2.578s

このように、キャッシュによって速度的な問題がほぼ解消されたことがわかります.nscdの設定ファイルは/etc/nscd.confです。一般的な運用であればデフォルトの状態から編集する必要はないかもしれませんが、設定も可能であるということを頭の片隅に置いておきましょう。

インデックス設定

ディレクトリエントリ数が少ない場合にはとくに問題ないのですが、データが多くなるにつれ大きなパフォーマンスダウンにつながるのが、インデックス関連の設定です。リスト3のスクリプトを図6のコマンドのように使うと、ダミーエントリを5,000個作成します。

リスト3 add.sh
#!/bin/sh
i=1
while [ $i -le 5000 ]; do
  echo "dn: uid=ldapuser$i,ou=People,dc=example,dc=com"
  echo "objectClass: account"
  echo "objectClass: posixAccount"
  echo "uid: ldapuser$i"
  echo "cn: ldapuser"
  echo "userPassword: ldapuser$i"
  echo "loginShell: /bin/bash"
  echo "uidNumber: 10$i"
  echo "gidNumber: 10$i"
  echo "homeDirectory: /home/ldapuser$i"
  echo ""
  i=`expr $i + 1`
done
図6 ダミーエントリの登録
% ./add.sh | ldapadd -x -c -D "cn=manager,dc=example,dc=com" -w secret

この状態で再びidコマンドを使って適当なエントリの情報を取得してみます(図7)。

図7 idコマンドの実行
% time id ldapuser996
uid=10996(ldapuser996) gid=10996 groups=10996

real    0m10.339s
user    0m0.002s
sys     0m0.055s

なんと、1ユーザのエントリを取得するのに約10秒もかかってしまいました.nscdが有効であるため、再び同じユーザ情報を得る場合にはそれほど時間がかかりませんが、最初の検索で10秒というのは大きな問題です。

LDAP、RDBMSにせよ、全文検索にしてもそうなのですが、インデックスとは検索を高速化するための効果的な手段です。いくらLDAPが検索重視のプロトコルだと言っても、インデックスを含め、適切な設定が行われていないサーバでは、その恩恵を実感することができません。

ということで、インデックスの設定を行ってみましょう。インデックスとは作成しさえすれば良いものではありません。不必要なインデックスは、ディスクを圧迫したり、かえってパフォーマンスダウンにつながる恐れもあるためです。用途に応じて適切なインデックスを作成する、これが正しい選択です。

今回問題となっているのは、idコマンドを実行した場合です。それではidコマンドを使用した場合にLDAPサーバ側ではどのようなエントリが検索されているのでしょうか?それはサーバ側のログを見ることで把握できます。

さて、ログを見るといっても、CentOSの場合/etc/syslog.confに適切な設定が存在しないため、そのままでは検索時のログを取得することができません.slapdはデフォルトでlocal4というファシリティを使用するため、LDAPサーバ側のsyslog.confをリスト4のように編集し、デーモンを再起動しておいてください(図8)。

リスト4 /etc/syslog.confに追加する内容
local4.*         /var/log/ldap.log
図8 ログファイルの作成とsyslogdの再起動
# touch /var/log/ldap.log
# chmod 600 /var/log/ldap.log
# /etc/init.d/syslog restart

syslog.confの設定が完了したら、まずは現在の設定のままで再びidコマンドを実行し、サーバ側でどのような検索が行われているのかを把握しておきます.LDAPサーバへ確実に接続できるよう、クライアント側のnscdは一時的に無効にしておくことをお勧めします。

それではidコマンドを実行しつつ、ldap.logの中身を確認してみます(図9、リスト5)。

図9 idコマンドの実行
% id ldapuser
/euid=1000(ldapuser) gid=1000 groups=1000
リスト5 /var/log/ldap.logの一部
Jul 15 17:19:31 localhost slapd[11014]: 

このように、objectClassやuid属性などのインデックス検索に失敗していることがわかりますので、それぞれのインデックス設定を行います。インデックス設定には表2の項目が用意されています。

表2 インデックスの種類
設定値意味検索例
pres存在uid=*
eq等価uid=ldapuser
approx近似uid=ldapusr
sub部分一致uid=ldap*
none無し-

ldap.logからfilterというキーワードを検索することで、今回LDAPサーバでは次のような検索が行われています。

filter="(&(objectClass=posixAccount)(uid=ldapuser))" 
filter="(&(objectClass=posixAccount)(uidNumber=1000))" 
filter="(&(objectClass=posixGroup)(gidNumber=1000))" 
filter="(&(objectClass=posixGroup)(|(memberUid=ldapuser)(uniqueMember=uid=ldapuser,ou=people,dc=example,dc=com)))"

これにより、今回の検索ではobjectClass、uid、uidNumber、gidNumber、memberUid、uniqueMember属性に対して等価検索が行われていることがわかるはずです。ではこれに従ってLDAPサーバ側のslapd.confに設定を追加します(リスト6)。

リスト6 slapd.confに追加する内容
index objectClass,uid,uidNumber,gidNumber,memberUid,uniqueMember eq

このように、インデックスを適切に設定した上でslapdを停止、slapindexコマンドでインデックスを作成した後各ファイルのパーミッションを適切に設定、slapd起動、という順序で検索を高速化することが出来ます。ただ、今回検証に用いたCentOSでは

equality index of attribute "uniqueMember" disallowed

というエラーが表示され、slapdの起動に失敗してしまいました。どうやらこのバージョンでは文字通りこのインデックスに対する処理がインプリされていないようです。少々気持ちが悪いかもしれませんが、実際にはnss_ldapにおいては、uidやuidNumberなどによる検索でエントリが発見されれば、uniqueMemberを使った検索は当面現れません。したがって、今回は

index objectClass,uid,uidNumber,gidNumber,memberUid eq

というインデックスのみを設定してみました。ちなみに最近の新しいバージョンのOpenLDAPではこのエラーは出ないことを確認しています(図10、11)。

図10 LDAPサーバ側での作業
# /etc/init.d/ldap stop
# slapindex
# chown -R ldap:ldap /var/lib/ldap
# /etc/init.d/ldap start
図11 クライアントからのidコマンド
# time id ldapuser3008
uid=103008(ldapuser3008) gid=103008 groups=103008

real    0m0.141s
user    0m0.021s
sys     0m0.102s

最終的には、ログ中にインデックス関連の警告が出なくなるまで設定を行ってみてください。

パスワードの暗号化

今までは便宜的にslapd.confやデータベース中になどに平文のパスワードを定義してきました。当然実際の運用では復号不可能なパスワードを定義し、適切なアクセス権限を付与しておくべきです.slapd.confをリスト7のように書き換えましょう。例のごとく暗号化されたパスワードはslappasswdコマンドを使って作成します。

リスト7 slapd.confを修正
rootpw          {SSHA}te0mm46b0qnQr7C/K8cWD56qi/8KhKSK
access to attrs=userPassword
  by anonymous auth
  by self write
  by * none
access to *
  by * read

accessからはじまる部分はACL設定です。1行では長くなってしまいわかりにくいので、通常このように複数行で定義を行います。最初の定義の意味は、userPassword属性に関して匿名バインド、つまり認証無しではアクセス禁止とし、バインドしたユーザ自身であればread/writeを許可という形です.2番目の設定は、それ以外の属性については全員にreadのみ許可、という形です.ACL設定の書式はいくつかあるので、今後の連載でも紹介できていけたらと思います。

チェック項目

LDAPサーバでUNIXアカウントを管理するための簡単なまとめです。これ以外でも冗長化対策や細かい点で改善すべき箇所はあるでしょうが、後々の連載で触れていきたいと思います。

  • サーバ側で適切なACLは設定されているか
  • サーバ側でパスワードは暗号化されているか
  • クライアント側でnscdは動いているか
  • LDAP上のユーザ名を用いてsshなどによるログインが可能か
  • suで他ユーザにスイッチできるか
  • passwdコマンドでLDAP上のパスワードを変更可能か

それでは次回をお楽しみに。

おすすめ記事

記事・ニュース一覧