LDAPSサーバを初期ユーザ付きでDocker Composeで構築する

いつも手順を忘れて結構な時間を食うのでメモ化する。

ファイルを格納するディレクトリを作る。
```sh
mkdir -p {data/certs,data/ldap_config,data/ldap_db,data/ldif}
```
ここではホスト名にワイルドカードDNSサービスの[nip.io](https://nip.io)を使い、`https://ldap.<コンテナを起動するホストのIP>.nip.io`でアクセスする。
その設定のために、dcやホスト名に使うホストのIPをハイフン繋ぎに変換した環境変数を用意する。
IP部分は自分のIPに書き換えること。また環境によっては無理に使う必要はない。
```sh
export HOST_IP=$(sed "s/\./-/g" <<< 192.168.0.15)
```
自己署名証明書を作成する。LDAPのコンテナ側に証明書の自動生成機能があるが、後述する`--copy-service`をつけると自動生成した証明書が消えてしまうので（バグ？）、予め自作して読ませる。（持ち込みの人はここはスキップでOK）
```sh
cat <<'EOF' > ./make_certs.sh
#!/bin/bash
set -xe
mkdir certs
cd certs
export HOST_IP=$(sed "s/\./-/g" <<< 192.168.0.15)
export COMMONNAME=ldap.${HOST_IP}.nip.io
openssl genrsa -out ca.key 4096
chmod 400 ca.key
openssl req -key ca.key -new -x509 -days 3650 -sha512 -extensions v3_ca -subj "/C=JP/ST=Earth/L=Global/O=example/CN=$COMMONNAME" -out ca.crt
openssl genrsa -out client.key 4096
openssl req -new -subj "/emailAddress=demo@example.com/CN=$COMMONNAME/O=example/OU=example/C=JP/ST=Earth/L=Global" -key client.key -out client.csr
cat > v3.ext <<-EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1=$COMMONNAME
DNS.2=$HOST_IP
DNS.3=hostname
EOF
openssl x509 -req -sha512 -days 3650 -extfile v3.ext -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt
openssl dhparam -out dhparam.pem 2048
cd -
cp certs/client.crt data/certs/ca.crt
cp certs/client.crt data/certs/ldap.crt
cp certs/client.key data/certs/ldap.key
cp certs/dhparam.pem data/certs/dhparam.pem
EOF
chmod +x ./make_certs.sh
./make_certs.sh
```
初期ユーザを定義したLDIFファイル(`users.ldif`)を作成する。
```yaml:users.ldif
cat <<EOF > ./data/ldif/users.ldif
dn: cn=user1,dc=ldap,dc=${HOST_IP},dc=nip,dc=io
objectClass: top
objectClass: inetOrgPerson
cn: user1
sn: User1
uid: user1
userPassword: password1

dn: cn=user2,dc=ldap,dc=${HOST_IP},dc=nip,dc=io
objectClass: top
objectClass: inetOrgPerson
cn: user2
sn: User2
uid: user2
userPassword: password2
EOF
```

`docker-compose.yml`を作成する。
```yaml
cat << EOF > ./docker-compose.yml
services:
  slapd:
    image: osixia/openldap
    container_name: slapd
    restart: always
    hostname: "ldap.192-168-0-15.nip.io"
    ports:
      - 636:636
    environment:
      LDAP_ORGANISATION: "Example Inc."
      LDAP_DOMAIN: "ldap.192-168-0-15.nip.io"
      LDAP_BASE_DN: "dc=ldap,dc=192-168-0-15,dc=nip,dc=io"
      LDAP_ADMIN_PASSWORD: "admin_password"
      LDAP_READONLY_USER: "true"
      LDAP_READONLY_USER_USERNAME: "readonly"
      LDAP_READONLY_USER_PASSWORD: "readonly_password"
      LDAP_TLS_VERIFY_CLIENT: "try"
      LDAP_TLS_CA_CRT_FILENAME: "ca.crt"
      LDAP_TLS_CRT_FILENAME: "ldap.crt"
      LDAP_TLS_KEY_FILENAME: "ldap.key"
      LDAP_TLS_DH_PARAM_FILENAME: "dhparam.pem"
    volumes:
      - ./data/ldap_db:/var/lib/ldap
      - ./data/ldap_config:/etc/ldap/slapd.d
      - ./data/certs:/container/service/slapd/assets/certs
      - ./data/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom
    command: --copy-service
  phpldapadmin:
    image: osixia/phpldapadmin
    container_name: phpldapadmin
    restart: always
    environment:
      PHPLDAPADMIN_LDAP_HOSTS: "slapd"
      PHPLDAPADMIN_LDAP_CLIENT_TLS_CRT_FILENAME: "ldap.crt"
      PHPLDAPADMIN_LDAP_CLIENT_TLS_KEY_FILENAME: "ldap.key"
      PHPLDAPADMIN_HTTPS: "true"
      PHPLDAPADMIN_HTTPS_CRT_FILENAME: "ldapadmin.crt"
      PHPLDAPADMIN_HTTPS_KEY_FILENAME: "ldapadmin.key"
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./data/certs:/container/service/phpldapadmin/assets/apache2/certs
      - ./data/phpldapadmin:/var/www/phpldapadmin
    depends_on:
      - slapdEOF
```
ポイントとしては`/container/service/slapd/assets/config/bootstrap/ldif/custom`にLDIFを入れると勝手に`ldapadd`してくれるのと、`command: --copy-service`で既存の設定を残すようにすること。（参考：[公式の説明](https://github.com/osixia/docker-openldap/tree/master?tab=readme-ov-file#seed-ldap-database-with-ldif)）
`docker-compose.yml`を作ったら起動する。
```sh
docker compose up -d
```
ブラウザで`https://ldap.${HOST_IP}.nip.io`にアクセスするとログイン画面が表示されるので、`Login DN`に`cn=admin,dc=ldap,${HOST_IP},dc=nip,dc=io`を指定し、`Password`に`LDAP_ADMIN_PASSWORDの設定値`を入れてadminでログインする。`cn=admin`を忘れないように。
![1710315042207.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/ef77bbcf-e145-4d40-8778-76243c4bfaa4.png)
ログインするとLDIFで定義した初期ユーザが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/73495a3f-3449-a574-1adb-98d95b72635b.png)

`ldapsearch`でも確認できる。
先に作成した`ca.crt`をOS上で信頼する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/b046e0c5-66fa-0406-e684-aafc640f0e7c.png)
信頼したら、`ldapsearch`で検索すると上手く取得できることが確認できる。（定義したホスト名でないとエラーになるので注意）
```sh
$ ldapsearch -D "cn=admin,dc=ldap,dc=192-168-0-15,dc=nip,dc=io" -w "admin_password" -b "dc=ldap,dc=192-168-0-15,dc=nip,dc=io" -H ldaps://ldap.192-168-0-15.nip.io:636
:
# user1, ldap.192-168-0-15.nip.io
dn: cn=user1,dc=ldap,dc=192-168-0-15,dc=nip,dc=io
objectClass: top
objectClass: inetOrgPerson
cn: user1
sn: User1
uid: user1
userPassword:: cGFzc3dvcmQx
:
```

## 余談
ここで作成する証明書、Macだとよく分からない属性がつく。（アットマーク部分）
```bashsh
$ ls -l data/certs/
total 32
-rw-r--r--@ 1 imurata  staff  2216 Mar 14 13:14 ca.crt
-rw-r--r--@ 1 imurata  staff   424 Mar 14 13:14 dhparam.pem
-rw-r--r--@ 1 imurata  staff  2216 Mar 14 13:14 ldap.crt
-rw-------@ 1 imurata  staff  3272 Mar 14 13:14 ldap.key
```
属性の詳細は`com.apple.provenance`となっている。
```sh
ls -l@ data/certs/
total 32
-rw-r--r--@ 1 imurata  staff  2216 Mar 14 13:14 ca.crt
	com.apple.provenance	  11
-rw-r--r--@ 1 imurata  staff   424 Mar 14 13:14 dhparam.pem
	com.apple.provenance	  11
-rw-r--r--@ 1 imurata  staff  2216 Mar 14 13:14 ldap.crt
	com.apple.provenance	  11
-rw-------@ 1 imurata  staff  3272 Mar 14 13:14 ldap.key
	com.apple.provenance	  11
 ```
 今回の検証ではこれはついていても問題ないのだが、削除したい場合は`Terminal.app`から`xattr -d com.apple.provenance <ファイル名>`で削除できる。
 `Terminal.app`というのがミソで、iTermからだと削除できない。
 （情報元：[What is the `com.apple.provenance` xattr (extended attribute) and how can I delete it?](https://apple.stackexchange.com/questions/450118/what-is-the-com-apple-provenance-xattr-extended-attribute-and-how-can-i-dele)）