利用例1 単純ユーザー認証(ローカル)

既に説明した通り、LDAPをユーザ認証に使うことができます。簡単にいえばLDAPを/etc/passwdやNISのかわりに使うことができるわけです。

アカウント情報をLDIFで表現する

LDAPをユーザ認証に使う場合、LDAPにユーザのアカウント情報を格納しておくことになります。LDAPではアカウント情報はユーザごとにディレクトリエントリを作ることで表現されます。

passwdをLDIFで表現する

まずは簡単な例を説明しましょう。リスト1のようなpasswdエントリをもつアカウント情報をLDAPに格納しようとしているとします。これはリスト2のような情報をあらわしています。

リスト1 アカウント情報例(passwd)
foo:x:1001:1001:foo,,,:/home/foo:/bin/bash
リスト2 アカウント情報例の説明
ユーザ名		foo
ユーザID		1001
グループID		1001
GECOSフィールド	foo,,,
ホームディレクトリ	/home/foo
ログインシェル		/bin/bash

これをLDAPに格納する時はLDIF形式を使ってあらわすとリスト3になります。ドメインはこれまでに説明したようにexample.jpを使っており、人に関する情報はou=Peopleという相対識別名(RDN)でつくられるディレクトリツリーに格納することとします。ouというのはorganizationalUnitの略で組織における部局などを表現するための属性です。

リスト3 アカウント情報例(LDIF)
dn: uid=foo,ou=People,dc=example,dc=jp
uid: foo
objectclass: posixAccount
uidNumber: 1001
gidNumber: 1001
gecos: foo,,,
homeDirectory: /home/foo
loginShell: /bin/bash
cn: foo bar

どのようなディレクトリエントリであっても識別名(DN)が決まっていなければいけません。識別名(DN)を構成する相対識別名(RDN)はどの属性を使ってもかまいませんが、その相対識別名(RDN)により識別名(DN)が一意になるような属性を使う必要があります。ここではユーザ名で一意に決まるものとしてユーザ名をあらわすuidという属性を相対識別名(RDN)としています。uidのかわりにユーザID(uidNumber)などを使う場合もありえます。

属性に関してはリスト2リスト3を見比べるとわかると思いますが、ほぼ一対一に対応しています。違うところはobjectclassとnがあるところです。objectclassというのは既に説明したとおり、このディレクトリエントリがどのような情報であるかをあらわすための属性です。ここではこのディレクトリエントリはアカウント情報をあらわしているのでposixAccountというobjectclassを使っています。

OpenLDAP 2.0.xでは、objectclassはschemaファイルで定義されています。定義されていないobjectclassは使うことができません。objectclass posixAccountはnis.schemaに定義されています(リスト4)。prefix=/usr/localでインストールしているとこのファイルは/usr/local/etc/openldap/schema/nis.schemaにインストールされているはずです。この定義によるとobjectclass posixAccountはcn属性(common name; 一般名)を持たないといけない(MUST)ため、このディレクトリエントリにはcn属性を登録しています。

リスト4 objectclass posixAccountの定義
objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY
       DESC 'Abstraction of an account with POSIX attributes'
       MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
       MAY ( userPassword $ loginShell $ gecos $ description ) )

passwdの情報とLDIFの対応をまとめると図1のようになります。

図1 passwdとposixAccountのattributeの関係
| foo:x:1001:1001:foo,,,:/home/foo:/bin/bash
|  |  |  |    |    |         |       |
|  |  |  |    |    |         |      loginShell
|  |  |  |    |    |         homeDirectory
|  |  |  |    |    gecos
|  |  |  |   gidNumber
|  |  |  uidNumber
|  |  (userPasswordだがshadowを使っている場合は/etc/shadowの方になる)
|  uid

                       objectclass: posixAccount
                       cn (common name; 一般名)
  ユーザ名		uid
  ユーザID		uidNumber
  グループID		gidNumber
  GECOSフィールド	gecos
  ホームディレクトリ	homeDirectory
  ログインシェル	loginShell

shadowをLDIFで表現する

最近のUNIXではパスワードに関してはshadowファイルに格納されています。これもLDAPで管理することができます。

shadowファイルではパスワードはリスト5のように格納されています。これはリスト6のような意味をもっています。

リスト5 パスワード情報例(shadow)
foo:SAUcuDpGYyyD2:10953:0:99999:7: : :
リスト6 パスワード情報例の説明
ユーザ名				foo
暗号化されたパスワード	(crypt)		Rlth8/fbmfjD2
最終パスワード変更日時			10953
パスワード変更不能日数			0
パスワード変更要求迄の日数		99999
パスワード期限満了警告日数		7
アカウント無効までの日数		(なし)
アカウント期限満了の日付		(なし)
フラグ(将来の使用に予約)		(なし)

これをLDAPに格納する時はLDIF形式を使ってあらわすとリスト7になります。普通はアカウント情報とあわせてパスワード情報を管理するのでリスト3の情報とあわせてあります。

リスト7 アカウント情報およびパスワード情報例(LDIF)
dn: uid=foo,ou=people,dc=example,dc=jp
uid: foo
objectclass: posixAccount
objectclass: shadowAccount
uidNumber: 1001
gidNumber: 1001
gecos: foo,,,
homeDirectory: /home/foo
loginShell: /bin/bash
cn: foo bar
userPassword: {CRYPT}SAUcuDpGYyyD2
shadowLastChange: 10953
shadowMin: 0
shadowMax: 99999
shadowWarning: 7

passwdはobjectclass posixAccountを使いましたがshadowに関してはobjectclass shadowAccountを使います。objectclass shadowAccountもobjectclass posixAccountと同様nis.schemaに定義されています(リスト8)。

リスト8 objectclass shadowAccountの定義
objectclass ( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' SUP top AUXILIARY
       DESC 'Additional attributes for shadow passwords'
       MUST uid
       MAY ( userPassword $ shadowLastChange $ shadowMin $
             shadowMax $ shadowWarning $ shadowInactive $
             shadowExpire $ shadowFlag $ description ) )

shadowの情報とLDIFの対応をまとめると図2のようになります。

図2 shadowとshadowAccountのattributeの関係
| foo:SAUcuDpGYyyD2:10953:0:99999:7: : :
|  |     |            |   |  |    | | | |
|  |     |            |   |  |    | | |  shadowFlag
|  |     |            |   |  |    | | shadowExpire
|  |     |            |   |  |    | shadowInactive
|  |     |            |   |  |    shadowWarning
|  |     |            |   |  shadowMax
|  |     |            |   shadowMin
|  |     |            shadowLastChange
|  |     userPassword
|  uid

ユーザ名				uid
暗号化されたパスワード	(crypt)		userPassword
最終パスワード変更日時			shadowLastChange
パスワード変更不能日数			shadowMin
パスワード変更要求迄の日数		shadowMax
パスワード期限満了警告日数		shadowWarning
アカウント無効までの日数		shadowInactive
アカウント期限満了の日付		shadowExpire
フラグ(将来の使用に予約)		shadowFlag

userPassword属性

userPassword属性だけが特殊な記述方法になります。userPassword属性には様々な暗号化方法を使って暗号化された文字列を格納できるようになっています。どの暗号化方法を使ったかをあらわすために文字列の最初に{}の中に暗号化方法を書くようになっています。UNIXのpasswd,shadowでつかっている暗号化方法は{CRYPT}になります。他に{MD5}、{SMD5}、{SSHA}、{SHA}が使えます。このような{}による暗号化方法の指定がないと平文がつかわれることになります。ちなみに、この表記はslapd.confのrootpw行でも使うことができます。

このuserPasswordの値を平文のパスワードから生成するためのツールとしてslappasswdがあります。これを使えばOpenLDAPで使える様々な暗号方法をつかった暗号化されたパスワード文字列を生成することができます。cryptで暗号化されたパスワードは次のようにして得ることができます。なおcryptでパスワードを暗号化する時はsalt文字が必要となるのでそれを-cオプションであたえます。

% /usr/local/sbin/slappasswd -h '{CRYPT}' -c "%s"
New password: 
Re-enter new password: 
{CRYPT}SAUcuDpGYyyD2

passwd(1)などと同じように二度同じパスワード文字列を入力すると、それを暗号化したパスワード文字列を最後に出力します。これはuserPassword attributeの値に使うことができます。二度タイプするかわりに-sオプションで平文のパスワードを渡すこともできます。

% /usr/local/sbin/slappasswd -h '{CRYPT}' -s secret -c "%s"
{CRYPT}2AHpdR1TnKiLc

アカウント情報をLDAPに登録する

slapdの設定

ここまでで設定したslapd.confではcore.schemaしかincludeしていなかったのでそのままではobjectclass: posixAccountなディレクトリエントリを追加することはできません。まず、slapd.confでnis.schemaをincludeする必要があります。nis.schemaの中ではcosine.schemaで定義している属性なども参照しているためにcosine.schemaもincludeするようにしないといけません(リスト9)。

リスト9 追加すべきschema
include  /usr/local/etc/openldap/schema/core.schema
include  /usr/local/etc/openldap/schema/cosine.schema	<- 追加
include  /usr/local/etc/openldap/schema/nis.schema	<- 追加

さらにLDAPでパスワードを変更する時も{CRYPT}を利用するようにするためにslapd.confにリスト10のようにpassword-hashを定義しておくといいでしょう。

リスト10 password-hachの設定
password-hash {CRYPT}

cosine.schema、nis.schemaをslapd.confに追加したらslapdを再起動します。

# kill `cat /usr/local/var/slapd.pid`
# /usr/local/libexec/slapd

もしcosine.schemaなどをincludeしわすれていると次のようのエラーがでてslapdは起動しません。

# /usr/local/libexec/slapd
/usr/local/etc/openldap/schema/nis.schema: line 193: AttributeType not found: "manager"

アカウント情報のLDIFをLDAPに登録

それではリスト7のアカウント情報、パスワード情報をLDAPに登録してみましょう。LDAPに登録したからといっていきなりそれがUNIXのアカウントとして利用できるようにはなりません。そのようにするためにはさらに設定が必要となりますが、それに関しては後述することにします。ここではまずLDAPに登録することからはじめます。

この例ではou=People,dc=example,dc=jpにアカウント情報を登録することにしていました。しかし、dc=example,dc=jpは既に登録してありますが、ou=People,dc=example,dc=jpはまだ登録していあせんでした。従ってまずou=People,dc=example,dc=jpを登録しておく必要があります。

もし、ou=People,dc=example,dc=jpがない状態で、uid=foo,ou=People,dc=example,dc=jpを登録しようとしても次のようなエラーがでて登録はできません。

% ldapadd -x -D 'cn=Manager,dc=example,dc=jp' -W -f /tmp/foo.ldif 
Enter LDAP Password: [cn=Manager,dc=example,dc=jpのパスワード; rootpwの値]
adding new entry "uid=foo,ou=people,dc=example,dc=jp"
ldap_add: No such object
       matched DN: "dc=example,dc=jp"
       additional info: parent does not exist

ldif_record() = 32

ou=People,dc=example,dc=jpのLDIFはリスト11のような感じにしておきます。

リスト11 ou=People,dc=example,dc=jp
dn: ou=People,dc=example,dc=jp
ou: people
objectclass: organizationalUnit

これをldapaddで追加しておきます。

% ldapadd -x -D 'cn=Manager,dc=example,dc=jp' -W -f people.ldif  
Enter LDAP Password: [cn=Manager,dc=example,dc=jpのパスワード; rootpwの値]
adding new entry "ou=People,dc=example,dc=jp"

このようにou=People,dc=example,dc=jpを追加しておいてから、uid=foo,ou=People,dc=example,dc=jpを登録します。

% ldapadd -x -D 'cn=Manager,dc=example,dc=jp' -W -f foo.ldif 
Enter LDAP Password: [cn=Manager,dc=example,dc=jpのパスワード; rootpwの値]
adding new entry "uid=foo,ou=People,dc=example,dc=jp"

このように登録するとldapsearchで検索できるようになります。

% ldapsearch -x -b 'dc=example,dc=jp' 'uid=foo'            
version: 2

#
# filter: uid=foo
# requesting: ALL
#

# foo, people, example, jp
dn: uid=foo,ou=people,dc=example,dc=jp
uid: foo
objectClass: posixAccount
objectClass: shadowAccount
uidNumber: 1001
gidNumber: 1001
gecos: foo,,,
homeDirectory: /home/foo
loginShell: /bin/bash
cn: foo bar
userPassword:: e0NSWVBUfVNBVWN1RHBHWXl5RDI=
shadowLastChange: 10953
shadowMin: 0
shadowMax: 99999
shadowWarning: 7

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

このように検索ベースとしてdc=example,dc=jpを指定すれば、その直接の子供のエントリ(今の場合はou=People,dc=example,dc=jpとcn=Manager,dc=example,dc=jpだけが直接の子供のエントリ)だけでなくdc=example,dc=jpの子孫すべての中からuid属性がfooであるエントリを検索することができます。

uid属性だけでなく他の属性でも検索できます。例えばuidNumber属性で検索する場合は次のようになります。

% ldapsearch -x -b 'dc=example,dc=jp' 'uidNumber=1001'
version: 2

#
# filter: uidNumber=1001
# requesting: ALL
#

# foo, people, example, jp
dn: uid=foo,ou=people,dc=example,dc=jp
uid: foo
objectClass: posixAccount
objectClass: shadowAccount
uidNumber: 1001
gidNumber: 1001
gecos: foo,,,
homeDirectory: /home/foo
loginShell: /bin/bash
cn: foo bar
userPassword:: e0NSWVBUfVNBVWN1RHBHWXl5RDI=
shadowLastChange: 10953
shadowMin: 0
shadowMax: 99999
shadowWarning: 7

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

userPassword属性はuserPassword: {CRYPT}SAUcuDpGYyyD2で登録したはずですが、ここでは userPassword:: e0NSWVBUfVNBVWN1RHBHWXl5RDI= となっています。このようにuserPassword属性の値はエンコードされて表示されます。LDIFではこのように属性名の後に:が二つつく場合は後ろはbase64でエンコードされるという約束になっています。これはバイナリデータなどのための属性値などでも使われます。

base64されているだけなので、このままでは userPassword属性の値を誰でも見ることができてしまうという問題があります。これを防ぐためにアクセス制御を設定しておくべきです。

アクセス制御

defaultのslapd.confではアクセス制御はなにも設定されていません。これはrootdnは読み書きができ、その他のユーザ(bindしてない状態のanonymousも含めて)すべてのディレクトリエントリが読めるということをことになっています。少なくともuserPassword属性が誰でも見られるという状態は問題があります。

簡単な制御としてuserPassword属性はそのユーザだけが読み書きでき、他の人はなにもできないようにしておくのがいいでしょう。ただしbindする前はanonymous状態ですからその時には認証だけはできるようにしてしておかないとそもそもそのユーザとして認識されないためにuserPasswordを使うことができないことに注意する必要があります。(リスト12)

リスト12 userPassword属性に対する簡単なアクセス制御
access to attribute=userPassword
       by self write
by anonymous auth
by * none

これによりuserPassword属性は、そのディレクトリエントリの識別名(DN)でbindしていれば書きこみ(write)権限をもちます("by self write")。書きこみ(write)権限が一番強くて書きこみ(write)権限があれば読み込み(read)権限ももつことになっています。UNIXのファイルのパーミッションなどとは違って書けるけれでも読めないという設定はできません。anonymous状態の場合、つまりbindする前の状態は認証目的のためだけにuserPassword属性を使うことができます("by anonymous auth")。読んだり書いたりすることはできません。それ以外の状態、このuserPassword属性をもつディレクトリエントリの識別名(DN)と違う識別名(DN)にbindしている場合などはuserPassword属性に対してなにもすることができません("by * none")。

他の属性に関しては自分自身では書きこみができて誰でも読めるようにするためのアクセス制御はリスト13のようになります。

リスト13 自分は書けて、他の人でも読めるようにするアクセス制御
access to *
      by self write
      by * read

なお、rootdnで設定した識別名(DN)にbindすればアクセス制御にかかわらずあらゆる操作をおこなうことができます。rootdnを設定するかわりにアクセス制御である識別名(DN)であらゆる操作ができるようにする場合はアクセス制御はリスト14のように書きます。userPassword属性に関してもrootdn的に権限をもてるようにするのならaccess to attribute=userPasswordのところに by dn="cn=Manager,dc=example,dc=jp" write を追加しておくといいでしょう。

リスト14 cn=Manager,dc=example,dc=jpは特権的に書きこめるようにするアクセス制御
access to *
      by dn="cn=Manager,dc=example,dc=jp" write
      by self write
      by * read

アクセス制御はこのように「ある対象(to なんとか)」に対して「誰(by なんとか)」が「どういう操作までできるか(write/read/auth/noneなど)」を記述していきます。

今回の例ではlapd.confにリスト15のアクセス制御を追加することにします。slapd.confにリスト15を追加してからslapdを再起動します。

リスト15 簡単なアクセス制御
access to attribute=userPassword
       by self write
       by dn="cn=Manager,dc=example,dc=jp" write
       by anonymous auth
       by * none
access to *
      by dn="cn=Manager,dc=example,dc=jp" write
      by self write
      by * read

# kill `cat /usr/local/var/slapd.pid`
# /usr/local/libexec/slapd

このようにしてから検索してみると今度はuserPassword属性が見えないことがわかります。

% ldapsearch -x -b 'dc=example,dc=jp' uid=foo
version: 2

#
# filter: uid=foo
# requesting: ALL
#

# foo, people, example, jp
dn: uid=foo,ou=people,dc=example,dc=jp
uid: foo
objectClass: posixAccount
objectClass: shadowAccount
uidNumber: 1001
gidNumber: 1001
gecos: foo,,,
homeDirectory: /home/foo
loginShell: /bin/bash
cn: foo bar
shadowLastChange: 10953
shadowMin: 0
shadowMax: 99999
shadowWarning: 7

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

uid=foo,ou=People,dc=example,dc=jpでbindするとuserPassword属性が見られるかどうか試してみましょう。

% ldapsearch -x -D 'uid=foo,ou=People,dc=example,dc=jp' -W -b 'dc=example,dc=jp' uid=foo
Enter LDAP Password: [uid=foo,ou=People,dc=example,dc=jpのuserPassword属性に設定したパスワード]
version: 2

#
# filter: uid=foo
# requesting: ALL
#

# foo, people, example, jp
dn: uid=foo,ou=people,dc=example,dc=jp
uid: foo
objectClass: posixAccount
objectClass: shadowAccount
uidNumber: 1001
gidNumber: 1001
gecos: foo,,,
homeDirectory: /home/foo
loginShell: /bin/bash
cn: foo bar
userPassword:: e0NSWVBUfVNBVWN1RHBHWXl5RDI=
shadowLastChange: 10953
shadowMin: 0
shadowMax: 99999
shadowWarning: 7

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

パスワードを間違えると次のようなエラーになります。

% ldapsearch -x -D 'uid=foo,ou=People,dc=example,dc=jp' -W -b 'dc=example,dc=jp' uid=foo
Enter LDAP Password: [間違ったパスワード]
ldap_bind: Invalid credentials

LDAP passwdの変更

LDAPに登録されているディレクトリエントリのパスワードつまりuserPassword属性を変更するにはldappasswdコマンドを使います。uid=foo,ou=People,dc=example,dc=jpがuid=foo,ou=People,dc=example,dc=jp自体のパスワードを変更するには次のようにします。

% ldappasswd -x -D 'uid=foo,ou=People,dc=example,dc=jp' -W 'uid=foo,ou=People,dc=example,dc=jp' -S
New password: [新規パスワード]
Re-enter new password: [新規パスワードをもう一度]
Enter bind password: [古いパスワード(bindするため)]
Result: Success (0)

-xオプションは既に説明した通りSASLを使わないことを意味します。 -D 'uid=foo,ou=People,dc=example,dc=jp'でuid=foo,ou=People,dc=example,dc=jpでLDAPにbindすること、つまりこのユーザ権限でLDAPに接続することを意味します。 -Wオプションでbindする時のパスワードを聞いてくるようになります。 次の'uid=foo,ou=People,dc=example,dc=jp'がパスワードを変更する対象です。 -Sで新規パスワードを尋ねてきます。

New password:に新規パスワードを入力します。Re-enter new password:でもう一度尋ねてくるので同じ新規パスワードを入力します。その後-WオプションによりEnter bind password:と-Dオプションで指定した識別名(DN)でbindするためのパスワードを入力します。2回入力した新規パスワードが一致して、bindパスワードが正しければ、この操作は成功しResult: Success (0)で終了します。

-Sオプションを指定しないと次のように新規パスワードを自動的に生成します。

% ldappasswd -x -D 'uid=foo,ou=People,dc=example,dc=jp' -W 'uid=foo,ou=People,dc=example,dc=jp' 
Enter bind password: [古いパスワード(bindするため)]
New password: Cy7nCCms
Result: Success (0)

このようにNew password:の行に自動的に新規生成されたパスワードが表示されます。この例ではuid=foo,ou=People,dc=example,dc=jpのパスワードは今後Cy7nCCmsとなります。 ldappasswordではパスワードを変更する人(-Dオプションで指定した識別名)と、パスワードを変更する対象は同じである必要はありません。パスワードを変更する人が、パスワードを変更する対象のuserPassword属性に対する書きこみ権限を持っていれば変更することができます。例えばcn=Manager,dc=example,dc=jpがrootdnとして設定されているとすると、cn=Manager,dc=example,dc=jpは全てのディレクトリエントリのあらゆる属性に対してあらゆる操作ができますから、当然のことながらパスワード(userPassword属性)も変更することができます。cn=Manager,dc=example,dc=jpがuid=foo,ou=People,dc=example,dc=jpのパスワードを変更する時は次のようにします。

% ldappasswd -x -D 'cn=Manager,dc=example,dc=jp' -W 'uid=foo,ou=People,dc=example,dc=jp' -S
New password: [uid=foo,ou=People,dc=example,dc=jpの新規パスワード]
Re-enter new password: [uid=foo,ou=People,dc=example,dc=jpの新規パスワードをもういちど]
Enter bind password: [cn=Manager,dc=example,dc=jpのパスワード]
Result: Success (0)

このように-Dオプションで変更操作をするユーザの識別名(DN)を指定するわけです。New password:、Re-enter new password:にはuid=foo,ou=People,dc=example,dc=jp(変更対処うの識別名(DN))の新規パスワードを指定し、Enter bind password:にはcn=Manager,dc=example,dc=jp(変更操作をおこなう識別名(DN))のパスワードを入力します。

slapdのその他の設定

slapdには一度のリクエストで返答することができる最大のディレクトリエントリ数の制限があります。それを設定するのがsizelimitです。defaultは500に設定されています。それを越えるディレクトリエントリ数が検索にマッチすると検索結果を返答しません。したがって返答しうる最大のディレクトリエントリ数以上をslapd.confファイルの中のsizelimitに設定しておく必要があります(リスト16)。

リスト16 sizelimit
sizelimit 8192

何も設定しないとslapdの検索はそれほど速くはありません。少しでも検索を速くするためにはインデックスを作成する必要があります。インデックスを作成しなくても検索はできますが、その場合はすべてのディレクトリエントリを読んで比較することになるのでディレクトリエントリの数が増えるについれて時間がかかるようになってしまいます。よく検索するような属性に関してはインデックスを作成しておけば高速な検索が可能になります。ただしインデックスを作成すればその分ディスク容量などが必要になるので必要なものだけ作るのがいいでしょう。

objectclass posixAccountやshadowAccountのディレクトリエントリを使う場合は、検索はuid属性やuidNumber属性、gidNumber属性を見ることがほとんどです。gecos属性やhomeDirectory属性、loginShell属性などで検索することは滅多にありません。従ってインデックスはuid属性、uidNumber属性、gidNumber属性にたいして作成しておくことにします。またcn属性で検索することも多々あるのでこれに関してもインデックスを作成します。その他の属性は、これらの属性で検索してからそのディレクトリエントリの属性を見るという使い方になるのでインデックスは作成しません。インデックスを作成する時はどのような検索条件のためのインデックスを作成するかを指定します。一致についてのインデックスはeq、部分文字列ならsub、類似ならapprox、存在ならpresを指定します(リスト17)。

リスト17 index
index uid,uidNumber,gidNumber pres,eq
index cn pres,eq,sub

slapd.confにindexの設定の行を書くだけでは、既にLDBMに登録されている情報に関してのインデックスは作成されていません。この状態でslapdを動かしてもインデックスがあるものとして検索しにいくために、実際にはLDBMに登録されている情報でもインデックスがないために検索にはひっかからないということになってしまいます。既にLDBMに登録してあるディレクトリエントリからインデックスを作成するためにslapindexコマンドを使います。

slapindexでインデックスを作るためには、まずslapdを止めます。

# kill `cat /usr/local/var/slapd.pid`

slapd.confにsizelimit,index行を追加してから、slapindexを実行します。

# /usr/local/sbin/slapindex

slapindexはslapd.confを読んで必要なインデックスファイルを作成します。slapindexを実行した後のLDBMのディレクトリ/usr/local/var/openldap-ldbmを見るとindexで指定した属性のためのインデックスファイルが作成されているのを見ることができます。

# ls /usr/local/var/openldap-ldbm
cn.dbb     gidNumber.dbb  nextid.dbb       uid.dbb
dn2id.dbb  id2entry.dbb   objectClass.dbb  uidNumber.dbb

ちゃんとインデックスファイルができていたらslapdを起動します。ldapsearchを使って問題なく検索できるかどうかを確認しておきましょう。

# /usr/local/sbin/slapindex
# /usr/local/libexec/slapd
% ldapsearch -x -b 'dc=example,dc=jp' '(uid=foo)'

LDAPによるアカウント管理

ここまでで説明したやり方で登録したアカウント情報をシステムで参照できるようにしてみましょう。

slapdにはuid=foo,ou=People,dc=example,dc=jpが登録されているとします。また/etc/passwdなど通常使うパスワードデータベースにはfooとユーザはまだ登録されてないとします。

% id foo
id: foo: そのようなユーザは存在しません

slapdに登録されているuid=foo,ou=People,dc=example,dc=jpをfooというユーザのアカウント情報として利用できるようにする方法を説明します。

従来のUNIXではpasswdの情報は/etc/passwdからか、NISを使って検索するようになっていました。最近のLinuxやSolarisではName Service Switch(NSS)という仕組をつかってpasswdなどの情報をどこからとってくるかを制御することができるようになっています(図3)。NSSの設定ファイルが/etc/nsswitch.confです。Linuxのglibc2.2ではdefaultはリスト18のようになっています。このようにそれぞれのネームサービスごとにどこからどの順番で検索するかを指定できるようになっています。passwdなどはcompatになっていますが、これは+によるNIS検索などに対応しています。glibcのNSSでは検索方法に応じた共有オブジェクトライブラリで実装されています。たとえばcompatに対応したものは/lib/libnss_compat.so.2です。

図3: Name Service Switch
|             Application
|                 |
|   ----------  getpw*() ----------- libc API
|
|    nss_file    nss_nis   nss_ldap
|      |            |         |
|   --------------------------------
|      |            |       <LDAP>
|    /etc/passwd   NIS        |
|                           ldap server
リスト18 defaultのnsswitch.conf
# /etc/nsswitch.conf
#
# Example configuration of GNU Name Service Switch functionality.
# If you have the `glibc-doc' and `info' packages installed, try:
# `info libc "Name Service Switch"' for information about this file.

passwd:         compat
group:          compat
shadow:         compat

hosts:          files dns
networks:       files

protocols:      files
services:       files
ethers:         files
rpc:            files

netgroup:       nis

NSSのバックエンドモジュールとしてLDAPを利用したnss_ldapというものがあります。これを使うと、LDAPを使ったアカウント情報の管理ができるようになります。nss_ldapはftp://ftp.padl.com/pub/nss_ldap.tar.gzに最新版があります。

Debian potatoではlibnss-ldapパッケージになっているのでこれをインストールします。なお、このパッケージではOpenLDAP 1.2.xのライブラリを利用していますが、OpenLDAP 2.0.xから通信することはできます。また暗号化に関してはcryptパスワードのサポートのみのようです。

apt-get install libnss-ldap
Reading Package Lists... Done
Building Dependency Tree... Done
The following extra packages will be installed:
  libopenldap-runtime libopenldap1 
The following NEW packages will be installed:
  libnss-ldap libopenldap-runtime libopenldap1 
0 packages upgraded, 3 newly installed, 0 to remove and 0 not upgraded.
Need to get 124kB of archives. After unpacking 365kB will be used.
Do you want to continue? [Y/n] y
Get:1 http://http.debian.or.jp stable/main libopenldap-runtime 1:1.2.12-1 [26.3kB]
Get:2 http://http.debian.or.jp stable/main libopenldap1 1:1.2.12-1 [63.5kB]
Get:3 http://http.debian.or.jp stable/main libnss-ldap 122-1 [34.2kB]
Fetched 124kB in 0s (277kB/s)  
Selecting previously deselected package libopenldap-runtime.
(Reading database ... 7923 files and directories currently installed.)
Unpacking libopenldap-runtime (from .../libopenldap-runtime_1%3a1.2.12-1_all.deb) ...
Selecting previously deselected package libopenldap1.
Unpacking libopenldap1 (from .../libopenldap1_1%3a1.2.12-1_i386.deb) ...
Selecting previously deselected package libnss-ldap.
Unpacking libnss-ldap (from .../libnss-ldap_122-1_i386.deb) ...
Setting up libopenldap-runtime (1.2.12-1) ...

Setting up libopenldap1 (1.2.12-1) ...

Setting up libnss-ldap (122-1) ...

これで/lib/libnss_ldap.so.2およびその設定ファイル/etc/libnss-ldap.confがインストールされます(リスト19)。

リスト19 defaultのlibnss-ldap.conf
#
# $Id: ldap.conf,v 2.10 2000/01/14 23:29:47 lukeh Exp $
#
# This is the configuration file for the LDAP nameservice
# switch library and the LDAP PAM module.
#
# PADL Software
# http://www.padl.com
#

# If the host and base aren't here, then the DNS RR
# _ldap._tcp.<defaultdomain>. will be resolved. <defaultdomain>
# will be mapped to a distinguished name and the target host
# will be used as the server.

# Your LDAP server. Must be resolvable without using LDAP.
host 127.0.0.1

# The distinguished name of the search base.
base dc=padl,dc=com

# The LDAP version to use (defaults to 2)
#ldap_version 3

# The distinguished name to bind to the server with.
# Optional: default is to bind anonymously.
#binddn cn=manager,dc=padl,dc=com

# The credentials to bind with. 
# Optional: default is no credential.
#bindpw secret

# The port.
# Optional: default is 389.
#port 389

# The search scope.
#scope sub
#scope one
#scope base

# The following options are specific to nss_ldap.

# The hashing algorith your libc uses. 
# Optional: default is des
#crypt md5
#crypt sha
#crypt des

この/etc/libnss-ldap.confではbaseがdc=padl,dc=comになっているので、ここだけは必ず直す必要があります。今の例ではdc=example,dc=jpを使っているのでそのように変更します。また、パスワードの変更やユーザ認証時にはuserPassword属性を読むことが必要となるのでrootbinddnにuserPassword属性が読み書きできる識別名(DN)を指定しておく必要があります。ここではrootdnであるcn=Manager,dc=example,dc=jpを指定することにします(リスト20)。rootbinddnでLDAPにbindするためのパスワードは/etc/ldap.secretに平文で書きます。当然のことながら、この/etc/ldap.secretファイルはroot所有のファイルにしてパーミッションは0600(rootのみ読み書き可能)としておく必要があります。

リスト20 修正したlibnss-ldap.conf(不要の部分は省略)
# Your LDAP server. Must be resolvable without using LDAP.
host 127.0.0.1

# The distinguished name of the search base.
base dc=example,dc=jp

# The distinguished name to bind to the server with
# if the effective user ID is root. Password is
# stored in /etc/ldap.secret (mode 600)
rootbinddn cn=Manager,dc=example,dc=jp


# echo パスワード > /etc/ldap.secret
# chown root /etc/ldap.secret
# chmod 0600 /etc/ldap.secret

ここまでで説明したアクセス制御ではanonymousでもuserPassword属性以外必要な属性は読むことができるようになっているのでbinddnやbindpwを設定する必要はありません。

/etc/libnss-ldap.confを設定できたら、次は/etc/nsswitch.confでnss_ldapを使ってLDAPを参照するように変更する必要があります。LDAPに登録してあるのはpasswdとshadowの情報だけなのでpasswdおよびshadowのcompatの後ろにldapを追加します(リスト21)。このようにするとcompatで指定したところ(つまり/etc/passwdやNIS)になければldapを参照するようになります。まだgroupをLDAPで管理するようにしていませんがついでにgroupもldapを見るように設定しています。

リスト21 ldapを追加したnsswitch.conf
# /etc/nsswitch.conf
#
# Example configuration of GNU Name Service Switch functionality.
# If you have the `glibc-doc' and `info' packages installed, try:
# `info libc "Name Service Switch"' for information about this file.

passwd:         compat ldap
group:          compat ldap
shadow:         compat ldap

hosts:          files dns
networks:       files

protocols:      files
services:       files
ethers:         files
rpc:            files

netgroup:       nis

すべてがうまく設定できていればこれでユーザfooが見えるようになっています。

% id foo
uid=1001(foo) gid=1001(foo) groups=1001(foo)

うまくいかない時は?

もしユーザfooが見えなければ設定が正しいかどうか確認しましょう。

LDAPに登録されている情報がまちがっていないか?

nss_ldapではobjectclassがposixAccountでuid属性やuidNumber属性でユーザ情報を検索しています。したがってobjectclassがposixAccountになっていないとそれをユーザアカウント情報と認識しません。次のようにしてLDAPで検索できるかどうか試してみましょう。

% ldapsearch -x -b 'dc=example,dc=jp' '(objectclass=posixAccount)'
% ldapsearch -x -b 'dc=example,dc=jp' '(&(objectclass=posixAccount)(uid=foo))'

LDAPのアクセス制御は間違っていないか?

さまざまなユーザプロセスからNSSを介してLDAPへ検索がかけられます。アクセス制御が厳しいと場合によっては検索できない場合もありえるかもしれません。一番簡単なのは誰でもobjectclass posixAccountの情報を読めるようにしておくことです。

libnss-ldap.confで正しいLDAPサーバを指定しているか?

当然のことながらlibnss-ldap.confで正しいLDAPサーバを指定している必要があります。hostがLDAPサーバが動いているサーバになっているか確認しましょう。また、baseが正しい検索ベースでないとこれも検索できません。

パスワード

libnss-ldapだけではパスワードの変更をpasswd(1)からすることができません。passwd(1)からパスワードを変更するのにLDAPを使うPAMモジュールが必要になります。これはftp://ftp.padl.com/pub/pam_ldap.tar.gzにあります。Debianではlibpam-ldapというパッケージになっているのでそれをインストールします。

# apt-get install libpam-ldap
Reading Package Lists... Done
Building Dependency Tree... Done
The following NEW packages will be installed:
  libpam-ldap 
0 packages upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 15.6kB of archives. After unpacking 47.1kB will be used.
Get:1 http://http.debian.or.jp stable/main libpam-ldap 43-2 [15.6kB]
Fetched 15.6kB in 0s (284kB/s)
Selecting previously deselected package libpam-ldap.
(Reading database ... 7957 files and directories currently installed.)
Unpacking libpam-ldap (from .../libpam-ldap_43-2_i386.deb) ...
Setting up libpam-ldap (43-2) ...

基本的にlibnss-ldapと同じような設定で問題ありません。リスト20のような/etc/libnss-ldap.confを使っているのなら、これをほぼ同じ内容の/etc/pam_ldap.confで問題ありません。ただしpam_password cryptという行が必要になるので、これを追加しておく必要があります(リスト22)。rootbinddnを設定しているので/etc/ldap.secretが当然必要になりますが、これは/etc/libnss-ldap.confを設定した時に使ったのがそのまま使えるはずです。(ちなみにsidにあるDebianパッケージではdebconfを使って他のLDAP関連のパッケージの設定情報を使って/etc/pam_ldap.confが作成されるのでほとんど設定を変更しなくてもすみます。)

リスト22 修正したpam_ldap.conf(不要の部分は省略)
# Your LDAP server. Must be resolvable without using LDAP.
host 127.0.0.1

# The distinguished name of the search base.
base dc=example,dc=jp

# The distinguished name to bind to the server with
# if the effective user ID is root. Password is
# stored in /etc/ldap.secret (mode 600)
rootbinddn cn=Manager,dc=example,dc=jp

pam_password crypt

このLDAP PAMモジュールを有効にするためには/etc/pam_ldap.confを設定するだけでは足りません。PAMを必要としているアプリケーション毎にどのPAMモジュールを使うかを設定していく必要があります。passwd(1)のPAMは/etc/pam.d/passwdで設定します。デフォルトではリスト23のようになっています。LDAPを使うようにするにはリスト24のように変更します。このように「password sufficient pam_ldap.so」の行を他の行よりも上に書いておきます。LDAP PAMモジュールがうまく動いていれば次のようにEnter login(LDAP) passwordのように尋ねてくるようになります。

リスト23 デフォルトのpasswdのPAM設定
#
# The PAM configuration file for the Shadow `passwd' service
#

# The standard Unix authentication modules, used with NIS (man nsswitch) as
# well as normal /etc/passwd and /etc/shadow entries. For the login service,
# this is only used when the password expires and must be changed, so make
# sure this one and the one in /etc/pam.d/login are the same. The "nullok"
# option allows users to change an empty password, else empty passwords are
# treated as locked accounts.
#
# (Add `md5' after the module name to enable MD5 passwords the same way that
# `MD5_CRYPT_ENAB' would do under login.defs).
#
# The "obscure" option replaces the old `OBSCURE_CHECKS_ENAB' option in
# login.defs. Also the "min" and "max" options enforce the length of the
# new password.

password   required   pam_unix.so nullok obscure min=4 max=8

# Alternate strength checking for password. Note that this
# requires the libpam-cracklib package to be installed.
# You will need to comment out the password line above and
# uncomment the next two in order to use this.
# (Replaces the `OBSCURE_CHECKS_ENAB', `CRACKLIB_DICTPATH')
#
# password required       pam_cracklib.so retry=3 minlen=6 difok=3
# password required       pam_unix.so use_authtok nullok md5
リスト24 LDAP PAMを有効にする(コメントは省略)
password   sufficient pam_ldap.so
password   required   pam_unix.so nullok obscure min=4 max=8


% passwd
Enter login(LDAP) password:
New password:
Re-enter new password: 
LDAP password information changed for foo

LDAPにないユーザは以下のように UNIX passwordを聞いてきます。

% passwd
Changing password for baz
(current) UNIX password: 

正しく設定できてない場合、/etc/pam_ldap.confでbaseやrootbinddnが間違っているような場合はLDAPの方にアカウントがあってもUNIX passwordを聞いてきますが正しく動かないので注意が必要です。

gecosの変更、シェルの変更

通常はgecosの変更やシェルの変更はchfn(1)、chsh(1)でおこなっています。しかし、LDAPにしかアカウント情報がないとこれらのコマンドは/etc/passwdを変更するために使うことができません。実際にやってみると次のようになります。

% chsh
Password:
Changing the login shell for foo
Enter the new value, or press return for the default
        Login Shell [/bin/bash]: /usr/bin/zsh
chsh: foo not found in /etc/passwd

このように/etc/passwdにエントリがあることを必要とします。chfnの場合も同様です。

このように/etc/passwdにエントリがあることを必要とします。chfnの場合も同様です。

これらの情報はchsh(1)やchfn(1)を使うかわりに直接LDAPの情報を変更することで同様のことができます。LDAPの情報を変更するためのコマンドはldapmodifyです。

例えばこの例のようにfooのユーザのログインシェルを/bin/bashから/usr/bin/zshに変更することをやってみると次のようになります。

% echo 'dn: uid=foo,ou=People,dc=example,dc=jp
changetype: modify
replace: loginShell
loginShell: /usr/bin/zsh
' | ldapmodify -x -D 'uid=foo,ou=People,dc=example,dc=jp' -W
Enter LDAP Password: 
modifying entry "uid=foo,ou=People,dc=example,dc=jp"

このように変更に対する要件もLDIFにより記述します。まず最初の行は対象とする識別名(DN)を書きます。

dn: uid=foo,ou=People,dc=example,dc=jp

次の行はどのように変更するかを示すchangetypeです。追加や削除ではないのでここはodify(修正)を使います。

changetype: modify

その次の2行でどのように変更するかを指示しています。replaceによりloginShell属性を以前のものから置きかえることを意味し、次のloginShellの行で実際にどの値に置きかえるかを意味しています。

replace: loginShell
loginShell: /usr/bin/zsh

以上で識別名(DN)がuid=foo,ou=People,dc=example,dc=jpのloginShell属性をloginShell: /usr/bin/zshにおきかえるという意味になります。

これをldapmodifyに入力します。既に何度も使っているように-xオプションはSASLを使わないことを意味しています。-D 'uid=foo,ou=People,dc=example,dc=jp'はこの識別名(DN)でbindすることを意味し、-Wはパスワードを尋ねてくることを意味しています。

chfnと同等のことをする時はgecosを変更することになります。ちなみに同時に変更するような場合は次のような記述になります。

dn: uid=foo,ou=People,dc=example,dc=jp
changetype: modify
replace: loginShell
loginShell: /usr/bin/zsh
-
replace: gecos
gecos: foo,0000,1111,2222

このようにloginShell属性の変更の指示とgecos属性の変更の指示は`-'だけの行で区切ります。

アカウントの削除

アカウントを削除する時は、LDAPから対応するエントリを削除することになります。削除するにはldapdeleteを使います。

% ldapdelete -x -D 'cn=Manager,dc=example,dc=jp' -W 'uid=foo,ou=People,dc=example,dc=jp'

なお、当然のことながらLDAPからアカウント情報を消してもホームディレクトリが削除されることはありません。またホームディレクトリを作成する時のようにPAM moduleを使って自動的にすることもできません。

/etc/passwdからLDAPへの一括登録

既存のアカウント情報/etc/passwdなどからLDAPへ一括して登録するには、/etc/passwdファイルからユーザ一人毎にobjectclassがposixAccountおよびshadowAccountであるディレクトリエントリを生成すればいいわけです。したがってそのような処理をするスクリプトを書けば既存のアカウント情報を一括してLDAP側に登録することができます。

PADL Software では、従来のアカウント情報からLDAPへ登録するためのLDIFを生成するようなmigrate_passwd.plをはじめとする様々な移行のためのツールをまとめたMigrationTools.tar.gzを配布しています。これは <URL:ftp://ftp.padl.com/pub/MigrationTools.tar.gz> から入手できます。これらのツールを使えば簡単に既存の情報をLDAPにもっていくことができます。ただし、最新のMigrationTools-39.tar.gzでもMD5パスワードの対応はないようです。

例えば、passwdをLDIFに変換する場合はMigrationTools.tar.gzを展開して得られるmigration_passwd.plを使います。設定はmigration_common.phに書かれていますが、必要な設定は環境変数を設定することでかえることができます。basednは設定する必要があります。basednは環境変数LDAP_BASEDNで変更できます。また、migrate_passwd.plではパスワード情報を得るために/etc/shadowも参照するのでroot権限で実行しなければなりません。

% tar zxf MigrationTools.tar.gz
% ls
MigrationTools-39  MigrationTools.tar.gz
% cd MigrationTools-39/
% ls
CVSVersionInfo.txt              migrate_base.pl
Make.rules                      migrate_common.ph
MigrationTools.spec             migrate_fstab.pl
README                          migrate_group.pl
ads                             migrate_hosts.pl
migrate_aliases.pl              migrate_netgroup.pl
migrate_all_netinfo_offline.sh  migrate_netgroup_byhost.pl
migrate_all_netinfo_online.sh   migrate_netgroup_byuser.pl
migrate_all_nis_offline.sh      migrate_networks.pl
migrate_all_nis_online.sh       migrate_passwd.pl
migrate_all_nisplus_offline.sh  migrate_profile.pl
migrate_all_nisplus_online.sh   migrate_protocols.pl
migrate_all_offline.sh          migrate_rpc.pl
migrate_all_online.sh           migrate_services.pl
migrate_automount.pl            migrate_slapd_conf.pl
% cp /etc/passwd passwd
% vi passwd
[LDAPに登録するユーザだけ残す。システムユーザなどは消してしまえばいい]
% sudo -s
# LDAP_BASEDN='dc=example,dc=jp' ./migrate_passwd.pl passwd > /tmp/account.ldif

これでpasswdにあるアカウント情報をaccount.ldifにLDIF形式に書きだすことができます。普通はrootのアカウント情報など/etc/passwdにあるアカウントを全部LDAPに登録する必要がないので、必要なエントリだけ別のファイルにかきだしてそれをmigrate_passwd.plの入力ファイルに指定してLDIF情報に変更させます。一旦LDIFにできればあとはそれをldapaddでLDAPに登録するだけです。

% ldapadd -D 'cn=Manager,dc=example,dc=jp' -W -f /tmp/account.ldif
Enter LDAP Password:

/etc/passwd,/etc/shadowからLDIFへの変換はそれほど複雑ではないので自分でperlやrubyなどで簡単なスクリプトを書いて済ますことも可能です。(リスト25)(リスト26)

このスクリプトを応用すれば、元が/etc/passwdなどでなくても例えばCSVなどからLDAPへ登録するためのLDIFを作成することができます。LDIFを作成する時に注意しないといけないのは次の通りです。

各ディレクトリエントリの先頭にdnを書く

各ディレクトリエントリはdn: 識別名(DN)ではじまっています。この識別名(DN)にはslapd.confでsuffixに指定した識別名(DN)のサブツリーに含まれるものを使うようにします。そのサブツリーの中ならばどのような階層構造でも構いません。例えば、部署ごとにou(organizationalUnit)をきって、その中にユーザ情報などを含めるようにすることもできます。PADL Softwareのmigrate_passwd.plではユーザ情報は ou=People にまとめていれるようにしています。また識別名(DN)にuid属性をいれていますが、これも必ずuid属性を入れる必要はありません。例えば識別名(DN)ではcn属性を使うようにしても問題ありません。識別名(DN)で使うものはディレクトリエントリの中で、その属性名と属性値をいれるようにしておきます。たとえば識別名(DN)が「cn=foo,ou=People,dc=example,dc=jp」の場合、「cn: foo」という属性をディレクトリエントリの中にいれることになります。

識別名(DN)は重複できない。

それぞれのディレクトリエントリは他と重ならない識別名(DN)をもちます。同じ識別名(DN)をもつ二つのディレクトリエントリというのはありえません。従って識別名(DN)の最初で使う属性名と属性値のペアは同じレベルにあるディレクトリエントリの中では重複できないことになります。例えば識別名(DN)が「cn=foo,ou=People,dc=example,dc=jp」というものだとすると、ou=People,dc=example,dc=jpには他の「cn=foo,ou=People,dc=example,dc=jp」というディレクトリエントリは存在しえないことになります。

各ディレクトリエントリは空行で区切る

各ディレクトリエントリ間は空行で区切ることになっています。これを忘れると二つのディレクトリエントリがつながってひとつになってしまいます。

識別名(DN)では属性名と属性値は=でむすぶが、ディレクトリエントリの中では:で区切る

uid属性の値がfooである時、識別名(DN)では「dn: uid=foo,ou=People,dc=example,dc=jp」のようにuid=fooと記述しますが、ディレクトリエントリの中では「uid: foo」と記述します。dnでの表記につられて「uid=foo」とすると正しいLDIFになりません。

objectclass: posixAccount, objectclass: shadowAccountをいれる

正しいobjectclass属性をいれておかないと正しく処理がおこなわれません。LDAPをNISのかわりに使おうと思っていてNSSで参照するのならばobjectclass: posixAccount、objectclass: shadowAccountになっている必要があります。

objectclassの定義にしたがった属性をいれる

objectclassは、schemaファイルに記述されているようにそれぞれ必要とする属性や含むことができる属性が決まっています。これにあうように記述していく必要があります。objectclass posixAccountは既に述べたようにcn, uid, uidNumber, gidNumber, homeDirectory属性が必要で、userPassword,loginShell,gecos,description属性をもつことができます。objectclass shadowAccountも既にのべたようにshadow関係の属性をもつことができます。またNSSでLDAPを使う場合は結局/etc/passwdや/etc/shadowに書かれている情報が必要とされるので、objectclass posixAccountで必要とされている属性に加えてuserPasswordやloginShell,gecos属性なども書いておくことになります。

userPassword属性は/etc/shadowのパスワード欄そのままではない

userPassword属性での値は/etc/shadowのパスワード欄の文字列をそのまま使えるわけではありません。userPassword属性にはどのような暗号化がほどこされているかについてを文字列の最初に{}でかこって記述することになっています。cryptの場合は{CRYPT}となります。/etc/shadowでは$1$で始まっている場合はmd5ですが、これも{CRYPT}の一種として扱われます。なお、userPasswordに含む値を得るためのツールとしては既に述べた通りslappasswdがあります。

ホームディレクトリ、ドットファイルの作成

adduserなどを使ってアカウントを作成する場合には、/etc/passwdや/etc/shadowのエントリにしたがってホームディレクトリなども作成してくれますが、LDAPにアカウント情報を登録するだけではホームディレクトリの作成などはおこなわれないので何らかの手段で作成する必要があります。大量のユーザを登録する必要がある場合にはそれぞれのユーザのホームディレクトリを作成することも大変です。

それらを楽にするための仕組としてmkhomedirというPAM moduleがあります。PAMというのはPluggable Authentication Moduleのことで、認証処理などにplug-inできるモジュールを提供する仕組です。Linux-PAMには多くのPAM moduleが含まれていますが、その中にmkhomedirというPAMのmoduleがあります。これを使うとユーザが最初にログインした時に、ホームディレクトリがないと自動的にホームディレクトリを作成するようにすることができます。ドットファイルなどもskelパラメータを指定しておけば、ホームディレクトリを作成する時にskelパラメータで指定したディレクトリからコピーされるようになります。またデフォルトのパーミッションもumaskパラメータで設定が可能です。最近のLinuxディストリビューションではPAMはほぼ最初からはいっているのであらためてインストールする必要はないでしょう。mkhomedir moduleを有効にするには/etc/pam.d/loginや/etc/pam.d/sshなどにリスト27のような行を追加します。

リスト27 pamに追加する行
session    required   pam_mkhomedir.so skel=/etc/skel/ umask=0022

これによりログインに成功してセッションを開始する時にsessionのPAM moduleがよびだされます。つまりsessionの必須(required)であるpam_mkhomedir.soがパラメータskelが/etc/skel/、umaskが0022として呼びだされます。/etc/skel/にあるファイルがホームディレクトリ作成時にコピーされるのでデフォルトのドットファイルなどはここに置いておくとよいでしょう。

グループの管理

アカウントと同様グループもLDAPで管理することができます。アカウントはobjectclass posixAccountとobjectclass shadowAccountを使いましたが、グループではobjectclass posixGroupを使うことになります。これはposixAccountなどと同様にnis.schemaに定義されています(リスト28)。

リスト28 objectclass posixGroupの定義
objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top STRUCTURAL
       DESC 'Abstraction of a group of accounts'
       MUST ( cn $ gidNumber )
       MAY ( userPassword $ memberUid $ description ) )

このposixGroupと/etc/groupの対応は図4の通りになります。

図4 groupとposixGroupの属性の関係
|  foo:x:1001:foo,bar
|   |  |  |    |  |
|   |  |  |    |  memberUid
|   |  |  |   memberUid
|   |  |  gidNumber
|   |  userPassword
|   cn

従ってこの/etc/groupの行をLDIFで表現するとリスト29のようになります。

リスト29 groupをposixGorupであらわした例
dn: cn=foo,ou=Group,dc=example,dc=jp
cn: foo
objectclass: posixGroup
gidNumber: 1001
memberUid: foo
memberUid: bar

今までと同様example.jpなのでdc=example,dc=jpになります。またアカウントはou=Peopleを使っていましたが、グループに関してはou=Groupを使います。ou=Peopleの時と同じようにまずou=Group,dc=example,dc=jpというエントリをあらかじめ追加しておく必要があります。

% echo 'dn: ou=Group,dc=example,dc=jp
ou: Group
objectclass: organizationalUnit
' | ldapadd -x -D 'cn=Manager,dc=example,dc=jp' -W
Enter LDAP Password: 
adding new entry "ou=Group,dc=example,dc=jp"

このようにou=Group,dc=example,dc=jpを追加してからグループの情報を追加します。

% echo 'dn: cn=foo,ou=Group,dc=example,dc=jp
cn: foo
objectclass: posixGroup
gidNumber: 1001
memberUid: foo
memberUid: bar
' | ldapadd -x -D 'cn=Manager,dc=example,dc=jp' -W
Enter LDAP Password: 
adding new entry "cn=foo,ou=Group,dc=example,dc=jp"

既存の/etc/groupからLDIFに変更するためのツールもMigrationTools.tar.gzの中にmigrate_group.plがあります。

# LDAP_BASEDN='dc=example,dc=jp' perl ./migration_group.pl /etc/group > /tmp/group.ldif

グループ名はcnなので、cnのインデックスもつくっておく方が検索のパフォーマンスはよくなるでしょう。

グループへユーザの追加、削除

グループへユーザを追加したり削除したりするのも、gecosやshellの変更と同じくldapmodifyを使っておこなうことになります。

グループfooにユーザbazを新たに追加する場合は次のような内容をldapmodifyに入力します。

% echo 'dn: cn=foo,ou=Group,dc=example,dc=jp
changetype: modify
add: memberUid
memberUid: baz
' | ldapmodify -x -D 'cn=Manager,dc=example,dc=jp' -W
Enter LDAP Password:
modifying entry "cn=foo,ou=Group,dc=example,dc=jp"

「add: memberUid」が次のmemberUid属性を新たに追加することを意味します。以前からあるmemberUid属性はそのまま残ります。一度に複数追加したい場合は次のようにします。

dn: cn=foo,ou=Group,dc=example,dc=jp
changetype: modify
add: memberUid
memberUid: user1
memberUid: user2
memberUid: user3

逆にグループfooからユーザbarを削除したい場合は次のような内容をldapmodifyに入力します。

dn: cn=foo,ou=Group,dc=example,dc=jp
changetype: modify
delete: memberUid
memberUid: bar

「delete: memberUid」によりmemberUid属性を削除することを意味します。どのmemberUid属性を削除するかは次の行で指示します。複数を削除する時も追加する時と同様にすることができます。

リスト25 passwd,groupをldifにするperl script
#!/usr/bin/perl
#
# $0 > account.ldif
#

$BASEDN = 'dc=example, dc=jp';
if (defined($ENV{'LDAP_BASEDN'})) {
   $BASEDN = $ENV{'LDAP_BASEDN'};
}

open(PW, "/etc/passwd") or die "cannot open passwd, $!";
while (<PW>) {
    chomp;
   next if /^#/;
   ($uid,$x,$uidNumber,$gidNumber,$gecos,$homedir,$shell) = split(/:/);
   if ($uidNumber < 1000) {
        # ignore, system users
        next;
   }
   $users{$uid} = {
        'uid' => $uid,
        'uidNumber' => $uidNumber,
        'gidNumber' => $gidNumber,
        'gecos' => $gecos,
        'homeDirectory' => $homedir,
        'loginshell' => $shell,
   }; 
}

open(SHADOW, "/etc/shadow") or die "cannot open shadow, $!";
while (<SHADOW>) {
    chomp;
    ($uid,$pw,$shadowlastchange,$shadowmin,$shadowmax,$shadowwarning,@_) = split(/:/);
    if ($pw =~ s/^\$1\$//) {
        $pw = "{md5}" . $pw;
    } else {
        $pw = "{crypt}" . $pw;
    }
    if (defined($users{$uid})) {
         $users{$uid}->{userpassword} = $pw;
         $users{$uid}->{shadowlastchange} = $shadowlastchange;
         $users{$uid}->{shadowmin} = $shadowmin;
         $users{$uid}->{shadowmax} = $shadowmax;
         $users{$uid}->{shadowwarning} = $shadowwarning;
    }
}
close(SHADOW);

foreach $uid (keys %users) {
    $u = $users{$uid};
    print <<END;
dn: uid=$u->{uid}, ou=People, $BASEDN
uid: $u->{uid}
cn: $u->{uid}
objectclass: account
objectclass: posixAccount
objectclass: shadowAccount
objectclass: top
uidNumber: $u->{uidNumber}
gidNumber: $u->{gidNumber}
gecos: $u->{gecos}
homeDirectory: $u->{homeDirectory}
loginShell: $u->{loginshell}
END
      print <<PW if $u->{userpassword} ne "";
userpassword: $u->{userpassword}
PW
      print <<SHADOW if $u->{shadowlastchange} != 0;
shadowlastchange: $u->{shadowlastchange}
shadowmin: $u->{shadowmin}
shadowmax: $u->{shadowmax}
shadowwarning: $u->{shadowwarning}
SHADOW
    print "\n";
}
リスト26 passwd,groupをldifにするruby script

BASEDN = ENV['LDAP_BASEDN'] || 'dc=example,dc=jp'

class Account

SHADOW = ['shadowLastChange', 'shadowMin', 'shadowMax',
        'shadowWarning', 'shadowInactive', 'shadowExpire', 
        'shadowFlag']
def initialize(uid, uidNumber, gidNumber, gecos, homeDirectory, loginshell)
       @uid = uid
       @uidNumber = uidNumber
       @gidNumber = gidNumber
       @gecos = gecos
       @homeDirectory = homeDirectory
        @loginshell = loginshell
        @userPassword = ''
        @shadow = {}
        SHADOW.each {|k| @shadow[k] = '' }
 end
 def uid
        return @uid
 end
 def userPassword= (pw)
        setUserPassword(pw)
 end
 def setUserPassword(pw)
        if pw.gsub!(/\$1\$/,"")
           @userPassword = "{MD5}#{pw}"
        else
           @userPassword = "{CRYPT}#{pw}"
        end
 end
 def setShadowLastChange(lc)
        @shadow['shadowLastChange'] = lc
 end
 def setShadowMin(min)
        @shadow['shadowMin'] = min
 end
 def setShadowMax(max)
        @shadow['shadowMax'] = max
 end
 def setShadowWarning(warning)
        @shadow['shadowWarning'] = warning
 end
 def setShadowInactive(inactive)
        @shadow['shadowInactive'] = inactive
 end
 def setShadowExpire(expire)
        @shadow['shadowExpire'] = expire
 end
 def setShadowFlag(flag)
        @shadow['shadowFlag'] = flag
 end

 def toldif
        r = "dn: uid=#{@uid}, ou=People, #{BASEDN}
objectclass: posixAccount
objectclass: shadowAccoutn
objectclass: top
uid: #{@uid}
uidNumber: #{@uidNumber}
gidNumber: #{@gidNumber}
gecos: #{@gecos}
homeDirectory: #{@homeDirectory}
loginshell: #{@loginshell}
"
         if @userPassword != ''
        	 r += "userPassword: #{@userPassword}\n"
         end
         SHADOW.each {|k|
            if /^\s+$/ =~ @shadow[k]
        	 r += "#{k}: #{@shadow[k]}\n"
            end
         }
         return r
  end
end

accounts = {}
File.open("/etc/passwd") {|fp|
 fp.each {|line|
    line.chomp!
    uid,x,uidNumber,gidNumber,gecos,homedir,shell = line.split(/:/)
    if uidNumber.to_i > 100
      accounts[uid] = Account.new(uid,uidNumber,gidNumber,gecos,homedir,shell)
    end
 }	
}

File.open("/etc/shadow") {|fp|
 fp.each {|line|
    line.chomp!
    uid,pw,shadowlastchange,shadowmin,shadowmax,shadowwarning,shadowinactive,shadowexpire,shadowflag = line.split(/:/)
    if accounts[uid]
         accounts[uid].userPassword = pw
         accounts[uid].setShadowLastChange(shadowlastchange)
         accounts[uid].setShadowMin(shadowmin)
         accounts[uid].setShadowMax(shadowmax)
         accounts[uid].setShadowWarning(shadowwarning)
         accounts[uid].setShadowInactive(shadowinactive)
         accounts[uid].setShadowExpire(shadowexpire)
         accounts[uid].setShadowFlag(shadowflag)
    end
 }
}

accounts.each_value {|acc|
  print acc.toldif
  print "\n"
}