オフィシャルサイト: OpenLDAP.org

LDAP (OpenLDAP)

LDAP とは Lightweight Directory Access Protocol のこと。一種のデータベースだが、トランザクション管理など、いわゆるリレーショナルデータベースの備えるような高度な機能は持たない。しかし大きな特徴がふたつある。 PostgreSQL などのリレーショナルデータベースの場合、データを投入する以前に、入れ物となるデータベースやテーブル、テーブルに持たせる項目、各項目の保持できるデータの「型」をまず考えて作らなくてはならない。一方、LDAP は、既製品のテーブル/データ定義や制約 (Constraint) を最初から持ったデータベースだといえる。 LDAP はネットワークやコンピュータアカウントの管理に利用されることが多い。というのも、LDAP のテーブル/データ定義は RFC で規定/公開されているので、環境や機器メーカー/機種、LDAPサーバソフトウェアを問わず、LDAP 共通のフォーマットで利用できるからだ。

もうひとつの特徴は、データをツリー構造として扱う点。 jp の下に company や government や organization があり、 company の下に myCompany がありそのまた下に sales や accounting があり、その下に ID: 10101 を持った Yamada Taro や 10102 を持った Itoh Hiroshi がいる... といった具合。そりゃ DNS だろ、と思った人は正解で、 DNS もディレクトリサービスの一種だ。どっかの Active Directory とやらは 「独自拡張を施したLDAP」 そのものである。 OpenLDAP パッケージには、主デーモンである slapd の他に、slurpd というデーモンが付属している。 slurpd はプライマリLDAPサーバとバックアップLDAPサーバ群との間でデータの同期を保つ (replication) ためのデーモンだ。こう聞くと、ますます DNS と同じものに思えてくる。

インストールで困ったという話はほとんど聞いたことがないので、サーバ設定もそこそこに、いきなりデータ投入から話を始める。筆者が実験台にしたのは Win XP 上の Cygwin 環境にインストールした Cygwin標準の OpenLDAP 2.2.x と、 Fedora Core 5 上に標準 RPM でインストールした openldap, openldap-servers-2.3.x パッケージ。後日、RHEL 4.5, CentOS 5.1 での検証も少々加えた。

他に、メールサーバ Postfix 及び Dovecot のメールアカウントを LDAP で管理する実装についてのページ 「Postfix + LDAP 時々 Dovecot」 も制作した。LDAP のライブな実例として参考になると思うので、併せて読んでみていただくといいだろう。

Table of Contents

サーバの基本設定

とりあえず実験用に動かしてみるだけなら、ほとんどやることはない。最低限調整の必要な項目は下の数カ所だけだ。アクセス制限やバークレイDBのパフォーマンス最適化など細かい点は、基本的な実験が終わってから煮詰めていくつもりだ。サーバデーモンの設定ファイルは /etc/openldap/slapd.conf

include ...

slapd.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

てなところ。スキーマは、オブジェクトクラス定義 (後で触れる) を幾つも組み合わせた、一式の定義セットのこと。

pidfile, argsfile

RedHat Enterprise Linux 4.x の pidfileargsfile の設定値は変だ。デフォルトでは、

pidfile    /var/run/slapd.pid
argsfile   /var/run/slapd.args

となっている。しかし、/etc/init.d/ldapslapd はユーザ ldap の権限で稼働するようになっていて、slapd は、root しかファイルが作成できないパーミッション設定の /var/run/ 直下にはこれらのファイルを出力できないのだ。正しくは、/var/run/openldap/ ディレクトリをパーミション ldap:ldap 755 で作成した上で、下記のように定義するべき。

pidfile    /var/run/openldap/slapd.pid
argsfile   /var/run/openldap/slapd.args

※ CentOS 5.1 では直っていた。

データベース位置とスーパーユーザの指定

database    bdb

「LDAP はデータベースだ」 と言いながら、実は OpenLDAP はデータベースファイルの読み書きを外部の仕組みに頼っている。 bdb つまり Sleepycat Berkeley DB () の使用が推奨されている。 SQLサーバでデータを管理することもできるそうだが、(執筆時点では) まだ実験段階らしい。

suffix    "dc=example-net,dc=com"
rootdn    "cn=Manager,dc=example-net,dc=com"

suffix は、管理するデータベースの「名前空間 (namingContext)」の基底を定義する。これから作ろうとするデータベースはこの名前空間に属さなければならない。別の名前空間のデータベースも作りたい場合には、 suffix "..." "..." のように複数定義することも可能。 rootdn は LDAP の読み書き全ての権限を持つスーパーユーザのことで、上記のように Manager という cn を指定する例が多いが、 hogeHoge でも何ら問題はない。実際の認証の際には、大文字小文字は全く無視されるので、例えば ldapdelete -x -D "cn=HOGE,DC=Example-nEt,DC=COM" ... とやっても認証は通る。 rootdnsuffix の名前空間に必ず属していなければならない (最近は層でもないらしい。自分の環境で `man slapd.conf' せよ)。スーパーユーザはデータベース上に実際に作成する必要はない。

rootpw    {SSHA}oWrcebewts8+gUfY6M3pYxzvDsSNSfN2

前述 rootdn のパスワード。生テキストで書くのは好ましくない。上記は "secret" という文字列を SSHA (ソルト付きSHA) でハッシュした文字列。こうしたハッシュを作成するためのツールは OpenLDAP に付属しており、

/usr/sbin/slappasswd -h {SSHA} -s secret

という風にコマンドすれば標準出力に出力される。ハッシュ方法は {SSHA} の他、{SHA}, {MD5}, {SMD5}, {CRYPT} がサポートされている。 -s で元文字列を指定しなければ、 slappasswd プログラムが対話式に訊いてくる。

directory    /var/lib/ldap

データベースファイルを作る、ファイルシステム上のディレクトリ。 OS や OpenLDAP のパッケージによっても場所は異なるだろうが、たいていデフォルトのままでいい。ただ、初めて slapd デーモンを起動する前に、確かにそのディレクトリが存在し、パーミションが slapd サービス実行ユーザ (通常 rootldap) 所有の 700 または 770 であることを確認した方がよい。データベースが壊れて slapd が固まったりするようになった際には、 slapd デーモンを止めてからこのディレクトリを物理的に空にすれば、データは戻ってこないがサービスは起動するようになる (Win + Cygwin 環境では度々これに悩まされている)。

password-hash

この段階で必要なわけではないが、どうも OpenLDAP の FAQ のようなので触れておく。データの属性のひとつに、ユーザのパスワードの格納に使う userPassword というエントリがある。slapd.confpassword-hash ディレクティブは、フロントエンドアプリケーションからのデータ作成/更新要求で値を userPassword エントリへ格納する際に、サーバ側で行うハッシュの方式を指定する。指定可能なハッシュ方式の種類は前に触れた slapppasswd コマンドで使用可能なものと、ハッシュしないことを示す {CLEARTEXT}。最大の注意点は、この機構は、フロントエンドが LDAP Password Modify Extended Operation (RFC3062) に則って通信を行う場合にだけ働くということだ。そうしたものには、例えば、ユーザ情報を一括管理するために利用する nss_ldap + PAM モジュールなどがある。しかし、 OpenLDAP パッケージ付属ユーティリティの ldapaddldapmodify はパスワード変更拡張手順を使わないので、専らそれらを使ってデータを登録/変更するつもりなら、このパラメータを {CRYPT}{SSHA} に設定したとしても全く無駄だ。つまり、ハッシュは行われず、元のテキストが Base64 エンコードされた状態で userPassword に格納される (※2)。逆に、フロントエンドアプリケーション自体がハッシュ機能を備えていてそれを有効にしている場合には、こちらのパラメータは {CLEARTEXT} にしておく必要がある。

※ えっ? Sleepycat って 06/2月に Oracle に買収されたんだっ?! 飼い殺しにされなきゃいいが。
※2 別件で LDAPプロトコルの検索フォーマット (RFC4515) を調べていたところ、Base64エンコード処理を行っているのはサーバでなく、フロントエンドが行っているらしいことが分かった。

slapd (OpenLDAPサーバ) の起動と停止

起動

Initスクリプトがある場合には、ごく普通に service コマンドなどで起動すればよい。 Fedora Core 5 の /etc/init.d/ldap 起動スクリプトを見てみると、/etc/sysconfig/ldap というファイルがあれば追加で読み込み、そこから然るべきシェル変数を拾って slapd 起動引数を調整している。

slapd プログラムを直接叩いて起動する場合には、概ね下記のようなコマンドになる;

root# /usr/sbin/slapd -h ldap://127.0.0.1 -u ldap

-h は LDAPデーモンのリッスンするネットワークインターフェイス及びポートを指定する。上記の例のようにするとループバックインターフェイスだけに聞き耳を立てるので、他のマシンやネットワークからはアクセス不能となる。実験段階には打って付けの設定だ。ポートはデフォルトの TCP 389 となる。 -h ldap://localhost-h ldap://localhost:389 も意味は全く同じだ。 -h オプションを全く指定しないと、暗黙的に -h ldap:/// の状態となる。これは -h ldap://0.0.0.0-h ldap://0.0.0.0:389 と同義であり、有効な全てのネットワークインターフェイスに対してデフォルトポートで通信を待ち受ける。

-uslapd デーモンプロセスの実行ユーザを指定する (省略可能)。 -g オプションを追加して別のグループを指定しない限り、実行グループは -u で指定したユーザのプライマリグループが使用される。当然ながら、 -u を指定する場合にはそのユーザがあらかじめシステム上に存在しなければならない。

なお、推奨通りに Berkeley DB をデータベースに使っていれば、この時点では slapd の起動時に "No DB_CONFIG file found ... Expect poor performance" (DB_CONFIGファイルが見つからない。高いパフォーマンスはとても見込めないぞ) という警告メッセージが出るかもしれない。これについては後で面倒を見る。

起動の確認

プロセスが立ち上がっているかを 'ps ax' コマンドで確認後、検索を掛けてみる。

user$ ldapsearch -x -b '' -s base namingContexts

ldapsearch コマンドの説明は後でする。初めて起動したこの状態では、まだデータは存在しないので、設定ファイルで規定した基底名前空間 (namingContext) が表示されれば問題なしだ。なんらかの回答は得られるものの基底名前空間が表示されない場合、権限の問題でデータが読めないのかもしれない。まだこの段階では slapd.conf のアクセス制御 `access to ...' のセクションはコメントアウトしておくことをお勧めする。

何らかの問題でどうしても起動しないようなら、 /usr/sbin/slapd を直接、デバグオプション付きで起動してみるのも手だ;

root# /usr/sbin/slapd -d <LEVEL> -h ldap://127.0.0.1

-d <LEVEL> を指定すると、 slapd は起動に使われた親プロセス (この場合ターミナル) から自らを切り離さず、フォアグラウンドで走る。 <LEVEL> には、収集する情報の種別ナンバーを加算式で指定する。レベル定義は ldap.h ファイルに書かれているが、 1 がファンクションコール、 2 がパケット処理、 4 がヘビートレースデバグ(?)、 8 がコネクション処理...などとなっており、複数の種類を表示させたい場合は、 1 と 4 なら -d 5、 2 と 8 なら -d 10 といった具合に足し算して指定する。

停止

init起動の場合は説明の必要もないだろう。 init起動でない場合、プロセスに INT シグナルを送って礼儀正しく終了させなければならない。このように;

root# kill -s INT $(cat /var/run/openldap/slapd.pid)

slapd の PIDファイルの位置については「サーバの基本設定」で述べた pidfile, argsfile を参照。

データの検索 (ldapsearch)

ここからの実験で度々使うことになるので、データを投入する前に検索フロントエンドユーティリティ ldapsearch について必要最小限のことは知っておかなければならない。基本形は;

user$ ldapsearch [[OPTION] [OPTION]...] [FILTER] [[ATTRIBUTE] [ATTRIBUTE]...]
例:
user$ ldapsearch -x -LLL -D "cn=Manager,dc=example-net,dc=com" -W \
  -b "ou=work,dc=example-net,dc=com" -s one \
  '(&(objectClass=person)(cn=Mike Smith))' uidNumber mail

OPTION部

オプション 意味
-H URI 省略可能。検索先のLDAPサーバを指定する。例えば -H ldap://localhost。ローカル以外でも LDAPサーバが動いている場合には指定したほうがいいだろう
-x SASLを使わず簡易認証でサーバに接続する
-D dn サーバに接続する (LDAPの世界では "Bindする" と呼ぶ) 際のユーザ名。例えば -D "cn=Manager,dc=example-net,dc=com"。通常、閲覧だけならスーパーユーザ権限は必要ないので、省略できる場合が多い
-w passwd 簡易認証用のパスワードを指定する。通常、検索だけならスーパーユーザ権限は必要ないので、指定しなくてもよい場合が多い
-W 簡易認証用のパスワードを、コマンドラインで渡す代わりにインタラクティブに訊いてこさせる
-b "basedn" 検索を開始する名前空間の基底を指定する。例えば -b "ou=work,dc=example-net,dc=com" となる。 -b "" とすると根底から根こそぎ
-s base|one|sub 上記の基底名前空間から、どの階層を検索するかを指定する。 base は基底名前空間そのものの情報だけ、 one は基底名前空間の直下だけ、 sub は基底以下の全階層を総舐め。省略した時のデフォルトは sub
-a TYPE 省略可能。 aliasObject (データのシンボリックリンクのようなもの) の参照方法を指定する。 never はエイリアスを一切辿らない。 find は基底オブジェクトを特定する段階でのみエイリアスを辿る。 search は、検索段階でのみ、リンク先オブジェクトの持つ属性をエイリアスそのもの属性のように見なしてヒット対象にする。 always は全ての面においてリンク先の情報を使う。デフォルトは never
-LLL -L は本来、LDIF (LDAP Data Interchange Format) 出力フォーマットのバージョンを換えるオプションだが、左記のように 3つ重ねると、邪魔なコメントが表示されず、結果が読みやすくなる。 L ひとつだと LDIF Ver.1、ふたつだと LDIF Ver.1 からバージョン表示行を除いたかたちとなる。省略すると 拡張 LDIF フォーマットで出力される

ATTRIBUTE部

特定の属性の値だけを表示させたい場合に、属性名を指定する。 cn sn uid mail roomNumber といった具合。

FILTER部

引数のうちで最も便利かつ凶悪な部分だ。RFC4515 Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters (文字列としてのサーチフィルタ) で解説されているが、日本語訳がまだどこにもないようだったので翻訳した。 RFC2254 を置き換える内容で、例もかなり分かりやすさを増した。

ldapsearch ユーティリティにおいては、フィルタ部を省略すると '(objectClass=*)' (つまり全てのオブジェクトクラス) が暗黙的に使われる。LDAP検索フィルタはシェルの特殊文字を多分に含むので、それらがシェルに解釈されてしまわないようフィルタ全体をシングルクォート ('') で囲まなければならない。

データの投入

ここでは OpenLDAP パッケージ標準のフロントエンドユーティリティ ldapadd で行うやり方を示す。ターミナルで直接叩くやり方と、あらかじめテキストファイル (LDIFファイル) に書いておく方法がある。
いずれにせよ、LDAPのデータはツリー構造なので、葉を付ける前に枝オブジェクト、枝を伸ばす前に幹オブジェクトを先に作っておかなければならない。

基礎知識

dnとは何か

DN (=Distiguished Name) は、直訳すれば「識別名」、もう少し砕いて言うと「識別のための一意な名前」。 LDAP のデータは全て、この識別名によって識別/管理される。当然、ひとつの LDAPサーバに、重複した dn は存在できない。

ObjectClass の基礎知識

具体的な投入方法を示す前に、ちょっと道草をして objectClass というものに触れておかなくてはならない。
objectClass (オブジェクトクラス) とは、属性の定義や、複数の然るべき属性の集合から成る構造体のこと。構造体の中の属性には、データ登録の際には必ず指定しなくてはならないものと、指定しても良い(MAYあるいはallowな)属性がある。例えば organizationオブジェクトクラスでは、o (organizationNameつまり団体名) は必須属性。dc は、dcObjectオブジェクトクラスの必須属性だ。

また、構造体そのものにも「継承」関係があり、或る構造体は或るもっと基礎的な構造体にプロパティを加えるものとして定義されていたり、まさに拡張するためだけのものであって単独では使えないオブジェクトクラスもある。こうした定義規則は RFC4512 で規定されている。日本語で読みたければ、 RFC4512 によって廃止(obsolete) された旧文書 RFC2252 の日本語訳がある。

自分の環境で現在どのようなオブジェクトクラスが利用可能になっているかは、以下のクエリを実行すると知ることができる (ここで知った)。出力は長いので、テキストファイルへリダイレクトするのが得策。

user$ ldapsearch -x -b "cn=Subschema" -s base \
 "(objectclass=*)" attributetypes objectclasses >schema.txt

定義の読み解き方を示すために、以下に、出力から抜き出したふたつのオブジェクトクラス定義と、その要点を示す;

例1
objectClasses: ( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' DESC 'RFC2798: I
 nternet Organizational Person' SUP organizationalPerson STRUCTURAL MAY ( audi
 o $ businessCategory $ carLicense $ departmentNumber $ displayName $ employee
 Number $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials 
 $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ room
 Number $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferred
 Language $ userSMIMECertificate $ userPKCS12 ) )
"NAME 'inetOrgPerson'" :
このオブジェクトクラスの名前。"NAME ( 'o' 'organizationName' )"のように幾つかの別名を持つオブジェクトもある。
"SUP organizationalPerson" :
このオブジェクトクラスは organizationalPerson オブジェクトを親 (SUPERCLASS あるいは SUPERTYPE) に持つ、つまり organizationalPerson の持つ属性を継承する。
"STRUCTURAL" :
このオブジェクトクラスはれっきとした構造体であって、単独でも使え、その場合は、LDIF命令の中で上位クラス (superclass) のオブジェクト (この場合 organizationalPerson) を明示的に objectclass:宣言しなくても organizationalPerson の持つ属性が網羅される。この例の inetOrgPersonクラスの場合、実はその親である organizationalPersonクラスが更にまた personクラスに SUP しているのだが、この場合 person クラスの属性も芋づる式に引き継がれる。 STRUCTURAL でないものは STRUCTURALなオブジェクトクラスと併用 (LDIF命令の中で objectclass: を必要な分だけ複数回宣言) する必要がある。
"MAY (...)" :
「持っても良いが必須ではない」属性のリスト。各属性は $ で区切られている。
例2
objectClasses: ( 1.3.6.1.4.1.1466.344 NAME 'dcObject' DESC 'RFC2247: domain co
mponent object' SUP top AUXILIARY MUST dc )
"AUXILIARY" :
他オブジェクトにプロパティを加える「補佐専門」のオブジェクトであり、単独では使えないことを表す。ちなみに "オーグズィルャリー" と発音する。
"MUST dc" :
dc プロパティが必須である。

スキーマを覗いておくと、例えば "dc" が domain component の意味であることが分かったりして、指定に迷いが無くなるのもありがたいことだ。これでやっと本題に入れる。

ターミナルから直接投入する方法

ターミナルでデータを直接入力する場合の例を以下に示す。最後の Ctrl + D キーは入力の終わり "EndOfFile" を表す。コマンドラインオプション -x, -D, -W の意味は ldapsearch の時のオプションと同じだ。ただし -c だけは ldapsearch にはないオプションで、これは「エラーがあっても EXIT するな」という意味。

user$ ldapadd -x -c -D "cn=Manager,dc=example-net,dc=com" -W
Enter LDAP Password:       # スーパーユーザのパスワードを入力
dn: dc=example-net,dc=com  # このエントリの識別名(Distinguished Name)
objectclass: dcObject      # 「これから指定する属性は dcObjectクラスのものです」。指定クラス外の属性は投入できない
objectclass: organization  # 「これから指定する属性には organizationクラスのものも使います」
o: example-net             # o 属性の実際の指定
dc: example-net            # dc属性の実際の指定。dn で指定している dc=  と一致しないと受け付けられない
                           # ここでEnterをもう一回
adding new entry "dc=example-net,dc=com"
 
[Ctrl + d]

LDIFファイルに書いておく方法

上記で言えば "dn:" から "dc: example-net" までをテキストファイル (便宜上 example.ldif とでもしておこう) に書いておき、以下のコマンドでデータベースに流し込む。ちなみに、ここではオプション -W の代わりに -w を指定して、「パスワードは"secret"だ」 とコマンドしているが、上記と同様に -W を使って尋ねてこさせるやり方でもできる。また、前項と同じように -c オプションを加えてコンティニュアスモードにするのも一興。

user$ ldapadd -x -D "cn=Manager,dc=example-net,dc=com" \
 -w secret -f example.ldif

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

前述した ldapsearch コマンドを使って、データが期待通りに登録されたかどうか確認してみよう。 -s sub はデフォルトなので省略可能。

user$ ldapsearch -x -s sub -b 'dc=example-net,dc=com'

先ほど登録したはずの dn: dc=example-net,dc=com がヒットすればOKだ。
これで「木の幹」はできた。実験のため、続いて、枝 (部署) と葉 (社員) を登録してみる。

user$ ldapadd -x -D "dn=Manager,dc=example-net,dc=com" -w secret
dn: ou=network,dc=example-net,dc=com
objectclass: organizationalUnit
ou: network
 
dn: uid=0001,ou=network,dc=example-net,dc=com
objectclass: person     # 下の inetOrgPersonクラスが personクラスを SUP しているので本当は省略できる
objectclass: inetOrgPerson
uid: 0001
sn: Nonogaki
cn: Tatsuya
mail: tat@example-net.cxm
userPassword: secret
roomNumber: 101

ldapsearch でデータを確認してみると、おかしなことに気付く。 userPassword 属性の値だ。入力した "secret" とは似ても似つかぬ値になっている;

user$ ldapsearch -x -LLL -b "dc=example-net,dc=com" '(sn=Nonogaki)'
dn: uid=0001,ou=network,dc=example-net,dc=com
objectClass: inetOrgPerson
uid: 0001
sn: Nonogaki
cn: Tatsuya
mail: tat@example-net.cxm
secret: c2VjcmV0

種明かしをすると、格納されたのは "secret" を Base64 でエンコードした文字列だ。ハッシュされたわけでなく可逆性のエンコードであり、 その証拠に、上記の変な文字列をテキスト変換ユーティリティで Base64デコードしてやると、元の secret という文字列が得られる。また、データを比較する ldapユーティリティ ldapcompare (用途は userPassword の比較に限らない) で確かめることもできる;

user$ ldapcompare -x -D "cn=Manager,dc=example-net,dc=com" \
 -w secret "uid=0001,ou=network,dc=example-net,dc=com" \
 userPassword:secret

結果は TRUE/FALSE で表示される (-z オプションを加えると表示はせずリターンコードとして返してくる)。 ldapcompare では、このようにエンコード前の文字列と比較することもできるし、上記 userPassword:secret のパートに二重コロン (::) を使うと、Base64 エンコード済みの値を与えて比較させることもできる。例;

user$ ldapcompare -x ... \
 userPassword::c2VjcmV0

OpenLDAPで(※3)、本当にハッシュしたデータを格納させたい場合には、データ登録の前段階で、別途、何らかのハッシュコマンドでハッシュ値を計算してから、それを登録する必要がある。その場合にも、更に Base64 エンコード処理を受ける。

※3 厳密に言うと、ハッシュしたパスワードを userPassword属性に登録することは 「userPassword のパスワードは、 8進法の文字列書式で、暗号化されずに格納される」 と規定している RFC4519 "LDAP: Schema for User Applications (RFC2256 を廃止) に違反している。また、OpenLDAP にも ldapcompareユーティリティにも、平文パスワードをハッシュされたパスワードと直接比較する能力はないので、ハッシュ化パスワードを格納した場合には、その照合処理はフロントエンドアプリケーションが全面的に責任を持たなければならない。ただし、例えば Sun の LDAPドキュメントを読むと、登録時に自動的にハッシュされるフシもあり、上記事項は RFC に厳格な OpenLDAP に「特有」の仕様だと言わざるを得ず、LDAPサーバが全てそうだとは限らない。