ApacheDSで適当にLDAP(S)ユーザ環境を用意する

練習用にLDAPサーバを用意するにはどういうのが良いかなということで、ApacheDSとApache Directory StudioでLDAP(S)サーバを用意してみる。なんといってもWindows上のGUIで構築できるので楽。

# LDAPとは

- データベースとそれにアクセスするプロトコルの一種。今で言うところのMongoDBのように任意の構造化データを格納できる。
- `/etc/passwd` 相当の情報をマシン間で共有するプロトコルとしては最も普及している。たとえば、WindowsのActive DirectoryもLDAPでアクセスできる。

LDAP自体にはクッソ歴史があり、必要以上に奥が深い。既存のシステムのやりとりとのためだけにLDAPを必要としているなら、ldapjsのガイド( http://ldapjs.org/guide.html )がコンパクトに纏まっていて参考になる。

最近のXML/JSONをベースにしたWeb界隈は、 **むしろLDAPシステムを再発明している** といっても過言ではなく、LDAP周辺の構成要素は1990年代から、YAMLのような人間に書ける交換形式( **LDIF** )、BSONやMessagePack、CBORのようなバイナリシリアライズフォーマット( **ASN.1** )、OpenID Connectのようなシングルサインオンを想定した認証フレームワーク( **Kerberos** や **SASL** )が存在している。

現代のWeb技術との重要な関わりとして、LDAPは X.500 規格をモデルにデザインされているため、同じX.500の規格であるPKI(= X.509)とコンセプトを共有している点がある。なので **SSL/TLS証明書を扱うときに良く見る** 形式のものをLDAPでも目にすることになる。

LDAPは例えばglauthのような新規実装が急に出てくるなど未だに人気を保っており、一度アイデアに振れておくことは意義があるような気がしている。例えば、Engine Yardの約10年前のblog( https://www.engineyard.com/blog/ldap-directories-the-forgotten-nosql )では以下のように言及されている:

> **LDAP Directories: The Forgotten NoSQL**
> ...
> That said, I *do* think LDAP got a lot of things right (fast, distributable, scalable and standardized).

## 重要な用語とコンセプト

LDAP上のオブジェクトは `cn=tester1,ou=users,o=testing` のようなDistinguished Name ( **DN** と略される)で識別され、オブジェクトは属性(attribute)を複数持つことができる。オブジェクトはクラスを持ち、クラスに含めるべき内容はスキーマで定義される。

単純なパスワードデータベースとしてLDAPを使う場合、理解すべき操作は2つ:

**バインド** (bind)は、オブジェクトに格納されたパスワードを使ってあるDNで表わされるユーザとしてログインすることを指す。つまり、LDAPサーバには自身の収録内容を `/etc/passwd` のように使ってユーザをパスワード認証する機能がついていると言える。

**検索** (search)は、 **フィルタ** と呼ばれる検索文字列を使って該当するオブジェクトを絞り込む操作を指す。フィルタは例えば `(&(objectClass=groupOfUniqueNames)(uniqueMember=cn=tester1,ou=users,o=testing))` のように書かれ、CSSのセレクタの如く属性での抽出が可能で、 `&` (and) のような集合演算も可能となっている。

# Apache Directory Studio のインストール

GUIなLDAPクライアントとしてはApache Directory Studioがそれなりにメジャーで、 https://directory.apache.org/studio/ からダウンロードできる。

...Windowsの場合は .zip を展開して適当な場所に置けばインストールは完了となる。

# ApacheDSの設定

Javaで書かれたLDAPサーバとして、今回はApacheDSを使う。ApacheDSは https://directory.apache.org/apacheds/ からダウンロードできる。Windowsでインストールすると、サービスとしてインストールされ、自動的に起動する。

SSL証明書は起動時に自動的に自己署名証明書が作成される。今回はこれをそのまま使うことにする。

## ポート番号の確認とマスタパスワードの変更

- 設定画面をApache Directory Studioで開き、設定を確認する: https://directory.apache.org/apacheds/basic-ug/1.4.1-changing-server-port.html
- adminパスワードを変更する: https://directory.apache.org/apacheds/basic-ug/1.4.2-changing-admin-password.html

安全のためadminパスワードくらいは変えといた方が... configを開く練習にもなる。また、デフォルトではポート10389など非標準のポートで起動するようになっている。

ポイントは **設定もLDAPに格納される** 点で、Apache Directory Studio側で変な設定をすると起動しなくなったりする。その場合は諦めて最初からやるか、DBの中身であるテキストファイル(LDIF)を直接修正することになる:

```
C:\Program Files (x86)\ApacheDS\instances\default\conf\ou=config\ads-directoryserviceid=default
```

のディレクトリに実際の設定ファイルがある。YAML同様微妙にフォーマットにシビアだったりするので、手書きしようとは思わない方が良いと思う。

## パーティションの追加

- パーティションの追加: https://directory.apache.org/apacheds/basic-ug/1.4.3-adding-partition.html

`o=` で始まるDNのパーティションを作成する。 ...唐突にDN(Distinguished Name)という用語が出てくるが、データベース内でのオブジェクト名をLDAPではDNと呼び、このDNという名称が方々で出てくることになる。

ApacheDSでは、ユーザやグループはこのように作成したパーティションの内部に作成することになる。

ここでは `o=testing` というDNのパーティションを追加したものと仮定する。

## Organizational Unit(ou)の追加

Organizational UnitはRFC4519で"組織の一部を表現する"となっている。例えば、部とか課を表現するのに使われたりする。

- https://tools.ietf.org/html/rfc4519#section-3.11

```
   The 'organizationalUnit' object class is the basis of an entry that
   represents a piece of an organization.
   (Source: X.521 [X.521])
```

たとえば、ココQiitaのサーバ証明書は `C = US, O = Amazon, OU = Server CA 1B, CN = Amazon` から発行されていて、Amazonの組織 "Server CA 1B" から発行されていることになる。

追加の方法は簡単で、

1. パーティション(地球のアイコン)を右クリック → New → New Entry... → Create Entry from Scratch を選択
2. オブジェクトクラスとして `organizationalUnit` を入力 → Add をクリック(自動的に `top` も追加される)
3. RDNの欄に `ou` = `users` となるように入力
4. Next → Finish

![SnapCrab_NoName_2019-8-21_4-11-7_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67446/dff2699c-48d9-4262-7ded-efb61297b456.png)

以降、同様の手順を繰り返してユーザやグループを作成することになる。最終的には

![SnapCrab_NoName_2019-8-21_4-19-15_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67446/d825dac7-7b68-337e-5935-c597f2aa516a.png)

のような木構造となる。

## ユーザの追加

ユーザは `person` クラスのオブジェクトで表わされる。RFC4519によると、

- https://tools.ietf.org/html/rfc4519#section-3.12

```
   The 'person' object class is the basis of an entry that represents a
   human being.
   (Source: X.521 [X.521])

      ( 2.5.6.6 NAME 'person'
         SUP top
         STRUCTURAL
         MUST ( sn $
               cn )
         MAY ( userPassword $
               telephoneNumber $
               seeAlso $ description ) )
```

定義の下に書いてあるのはスキーマで、 `person` には `cn` と `sn` の各属性が必須であることがわかる。 `sn` は surname で名字を表わし、 `cn` は Common Nameを表わす(これらの定義も同様にRFC4519にある)。通常、cnがUI上の表示名として使用される。

追加は先の `organizationalUnit` と同様に行えるが、 MAY レベルである `userPassword` については自動的には追加されない。追加したユーザをクリックして属性一覧を出し、そこを右クリック → New Attribute... → `userPassword` をAttribute type:欄に入力して作成できる。

![SnapCrab_NoName_2019-8-21_4-29-31_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67446/60a942f2-a06c-fef4-bad7-1c83c256791f.png)

userPassword属性には専用のエディタが有り、パスワードのハッシュ化などもGUI上でできる。

# ldapsearchで確認してみる

作成したデータベースをOpenLDAPの `ldapsearch` コマンドで確認してみる。

## SSL証明書の抽出

SIMPLE認証を使った場合、LDAPではプロトコル上パスワードを平文で送信する。このため、事実上LDAPの運用はSSL/TLSでの接続が前提となる。今回はApacheDSが作成した自己署名証明書をそのまま使っている。このSSL証明書を抽出するには、 `openssl` の `s_client` コマンドが便利に使える。

```
openssl s_client -starttls ldap -showcerts stripe.local:10389
```

( `-starttls ldap` LDAPのStartTLSでTLS接続を開始、 `-showcerts` 証明書を画面に出力。)

のようなコマンドでLDAPサーバに接続したら、

```
-----BEGIN CERTIFICATE-----
MIIBcTCCARsCBgFso5FSijANBgkqhkiG9w0BAQUFADBCMQswCQYDVQQGEwJVUzEM
MAoGA1UEChMDQVNGMRIwEAYDVQQLEwlEaXJlY3RvcnkxETAPBgNVBAMTCEFwYWNo
ZURTMB4XDTE5MDgxODA3MTE0NVoXDTIwMDgxNzA3MTE0NVowQDELMAkGA1UEBhMC
VVMxDDAKBgNVBAoTA0FTRjESMBAGA1UECxMJRGlyZWN0b3J5MQ8wDQYDVQQDEwZz
dHJpcGUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAlWJvWfgNsE2wBELXb4wSAk1S
FfojfPH7sI3nQla1EAeS1B4+dsUCCvQqdAvn4wcaX2caGaHkGDGDhQx/RPTjcwID
AQABMA0GCSqGSIb3DQEBBQUAA0EAJEyq83NsmRSx0Rd5Paipouk40Rud+JOPzOUy
smt2x3gpV8TM9JIfAefcbsmiih43AuqZPMSkWUvYjnSMjNVCww==
-----END CERTIFICATE-----
```

のような部分を切り出して、テキストファイルに保存することで証明書として扱える。(Windowsなら、拡張子を `.crt` にすることでエクスプローラから開ける)

また、同時に出力される情報のうち、

```
Certificate chain
 0 s:C = US, O = ASF, OU = Directory, CN = stripe
   i:C = US, O = ASF, OU = Directory, CN = ApacheDS
```

のような `CN = stripe` の部分を確認する必要がある。TLSでは、証明書に何も検証できる項目が無いときは最後にCommon Nameとホスト名の一致を確認することになっている:

- https://tools.ietf.org/html/rfc6125#section-6.4.4

```
              if and only if the presented identifiers do not include a
   DNS-ID, SRV-ID, URI-ID, or any application-specific identifier types
   supported by the client, then the client MAY as a last resort check
   for a string whose form matches that of a fully qualified DNS domain
   name in a Common Name field of the subject field (i.e., a CN-ID).
```

このため、 `/etc/hosts` なり何なりの方法でこのCommon nameの部分(今回の場合は `stripe`)をホスト名に使って接続を確立する必要がある。

## `ldapsearch` コマンドで作ったデータベースをクエリする

今回は自己署名証明書を使っているので環境変数に証明書の場所を設定することで使用する。

FIXME: **環境変数によるオーバライドはmacOSでは効かないらしいので要調査。キーチェーンに一時的に鍵を置くことで同じことができるはず。**

FIXME: ここでは特に理由なくグループとして `groupOfUniqueNames` を使っているが、正しくない。

`ldapsearch` のようなOpenLDAPのコマンドの設定は `man ldap.conf` で一覧を確認できる。 `ldap.conf` はファイルで設定する場合だが、設定項目の先頭に `LDAP` を付けた環境変数でオーバーライドすることができる。↑で取得した自己署名SSL証明書を `ldapsearch` に使わせるには、

```
export LDAPTLS_CACERT=/home/oku/ldap.crt
```

のようにして、 `LDAPTLS_CACERT` 環境変数を設定する。

この設定と、必要に応じて `/etc/hosts` の設定をした上で、

```
ldapsearch -x -v -W -D cn=lookup,ou=users,o=testing -ZZ -H ldap://stripe:10389 -b "ou=users,o=testing"
```

( `-x` : Simple認証を使用、 `-v` verbose 、 `-W` パスワードプロンプトを表示 、 `-D` 検索用ユーザDNの指定 、 `-ZZ` **StartTLSによるTLS通信を強制** 、 `-b` 検索rootを指定 )

のようにして、`ou=users,o=testing` 以下のユーザを検索できる。ここではユーザ `cn=lookup,ou=users,o=testing` に **バインド** して `"ou=users,o=testing"` 以下のオブジェクトを全て検索している。

`LDAPTLS_CACERT` に指定した証明書がサーバの証明書と認められない場合はエラーになる。例えば、ホスト名を `stripe` → `stripe.local` のように変更して同じホストを別の名前で参照すると:

```
$ ldapsearch -x -v -W -D cn=lookup,ou=users,o=testing -ZZ -H ldap://stripe.local:10389 -b "ou=users,o=testing"
ldap_initialize( ldap://stripe.local:10389/??base )
ldap_start_tls: Connect error (-11)
        additional info: TLS: hostname does not match CN in peer certificate
```

のように失敗してしまう。(ここでは `stripe.local` と `stripe` は同じIPv4アドレスに解決され、物理的には同じホストに接続しているものとする。)

ユーザと一緒にグループを作った場合は、コマンドラインに **フィルタ** を追加することで "あるユーザが所属しているグループ" を列挙できる。

```
ldapsearch -x -v -W -D cn=lookup,ou=users,o=testing -ZZ -H ldap://stripe:10389 -b "o=testing" "(&(objectClass=groupOfUniqueNames)(uniqueMember=cn=tester1,ou=users,o=testing))"
```

# See also

↑でも引用している **RFC4519** ( https://tools.ietf.org/html/rfc4519 ) はLDAPにユーザ情報を格納するにあたって標準となるスキーマを定義している。もっとも、実際のLDAPデプロイメントは方言があることが多く、その方言に合わせた設定をLDAPのクライアント側(nginxのようなサーバや、KeycloakのようなID managementなど)に行う必要がある。

本当にアカウントをマシン間で共有したいならば、このように手で実装するよりFreeIPA( https://www.freeipa.org/ 、389 Directory server https://directory.fedoraproject.org/ のフロントエンド)やglauth( https://github.com/glauth/glauth 、[自前のLDAPサーバ](https://github.com/nmcclain/ldap) )のような既存のLDAP実装を検討すべきと言える。単にLDAPサーバを立ててユーザを作ることよりも、それを使うようにクライアントを設定して回ることの方がずっと面倒で、FreeIPAのような何か実績のあるものをそのまま使う方が簡単だろう。