Raspberry Pi で LDAP サーバの構築（LDAPS (Secure LDAP) 対応）

認証周り，複数環境でってなったらやはり LDAP がデファクトスタンダードですよね。
Google の Cloud Identity や AWS の AWS Directory Service もいいですが，家庭用で使うにはややお値段が高いです...

そこで，逸般の誤家庭に1台は余っている Raspberry Pi で LDAP サーバを構築します！
また，昨今はセキュリティ意識の高まり（法規制など）もあり，よりセキュアなものが求められています。
なので，今回は LDAPS (LDAP over SSL/TLS; Secure LDAP) にも対応したサーバを構築します！


## 対象読者
* Raspberry Pi がある
* 簡単な UNIX コマンドは問題なく使える
* LDAP サーバを構築したい

という逸般人が対象です。


## 今回やること
* OpenLDAP のインストール
* OpenLDAP のセットアップ
* LDAPS 対応

Raspberry Pi のセットアップの手順は [【ヘッドレス】Raspberry Pi 3 セットアップ for macOS - Qiita](https://qiita.com/y_k/items/2e37583d4a5dcf81dc19) を参考してください。

## LDAP サーバのセットアップ
### ホスト名の変更
先にいい感じのホストに変更します。
なお，LDAPS 対応のために Let's Encrypt を用いる都合，自分で保有しているドメインを設定・読み替えしてください:

* `ldap.home.local` → `ldap.yk-lab.net`
* `dc=home,dc=local` → `dc=yk-lab,dc=net`

など。

逸般人向けなので vim の使い方は省略します。
適宜，お好きなエディタをお使いください。

```console
$ sudo vim /etc/hosts
```

```diff
- 127.0.1.1       raspberrypi
+ 127.0.1.1       ldap.home.local ldap
```


### OpenLDAP とユーティリティのインストール
```console
$ sudo apt install slapd ldap-utils
```
インストール中に admin のパスワードの設定を求められるので適宜設定してください。


インストールできたかは，以下のコマンドで確認できます: 

```console
$ sudo slapd -V
@(#) $OpenLDAP: slapd  (Sep  9 2019 13:45:58) $
	Debian OpenLDAP Maintainers <pkg-openldap-devel@lists.alioth.debian.org>
```

slapd は OpenLDAP server，ldap-utils は OpenLDAP utilities です。


### 状態の確認
#### LDAP サーバの設定に関するエントリを確認
```console
$ sudo ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b cn=config dn
```

<details><summary>実行結果</summary><div>

```console
$ sudo ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b cn=config dn
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
dn: cn=config

dn: cn=module{0},cn=config

dn: cn=schema,cn=config

dn: cn={0}core,cn=schema,cn=config

dn: cn={1}cosine,cn=schema,cn=config

dn: cn={2}nis,cn=schema,cn=config

dn: cn={3}inetorgperson,cn=schema,cn=config

dn: olcBackend={0}mdb,cn=config

dn: olcDatabase={-1}frontend,cn=config

dn: olcDatabase={0}config,cn=config

dn: olcDatabase={1}mdb,cn=config
```
</div></details>

コマンドのオプションの意味は [OpenLDAPでユーザー認証までやってみる - Qiita](https://qiita.com/toshihirock/items/fc6ba5b6be8040e69ac9) に解説があります。


#### そのほかのエントリを確認
- ベースツリー
- frontendデータベース
- configデータベース
- monitorデータベース
- デフォルトのデータベース

これらの確認方法は [OpenLDAPでユーザー認証までやってみる - Qiita](https://qiita.com/toshihirock/items/fc6ba5b6be8040e69ac9) にあるので，詳細は省略します。


#### アカウント周りの設定を確認
```console
$ sudo slapcat
```


<details><summary>実行結果</summary><div>

```console
$ sudo slapcat
dn: dc=home,dc=local
objectClass: top
objectClass: dcObject
objectClass: organization
o: home.local
dc: home
structuralObjectClass: organization
entryUUID: da204d78-ee8a-1039-92d5-xxxxxxxxxxxx
creatorsName: cn=admin,dc=home,dc=local
createTimestamp: xxxxxx
entryCSN: xxxxxx#000000#000#000000
modifiersName: cn=admin,dc=home,dc=local
modifyTimestamp: xxxxxx

dn: cn=admin,dc=home,dc=local
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: LDAP administrator
userPassword:: xxxxxxxxxxxx
structuralObjectClass: organizationalRole
entryUUID: da242592-ee8a-1039-92d6-xxxxxxxxxxxx
creatorsName: cn=admin,dc=home,dc=local
createTimestamp: xxxxxx
entryCSN: xxxxxxZ#000000#000#000000
modifiersName: cn=admin,dc=home,dc=local
modifyTimestamp: xxxxxx
```
</div></details>


### LDAPS (Secure LDAP) 対応
まずはじめに，SSL/TLS サーバ証明書を取得します。
自分で作る自己証明書（いわゆるオレオレ証明書）でやる方法もありますが，今回は Let's Encrypt で証明書を取得します。

#### Let's Encrypt クライアントのインストール
今回は certbot という自動で証明書を取得するツールを用います。
この certbot は apt でインストール可能です。

```console
$ sudo apt install certbot
```

Let's Encrypt はドメイン保有の確認のために，HTTP-01 と DNS-01，TLS-ALPN-01 方式の 3 方式があります。
一般には HTTP-01 と呼ばれる，特定の URL に Let's Encrypt のボットがアクセスして指示した内容のファイルがあるか確認する方式を用いられることが多いですが，外部からアクセスできる環境にしたくないため DNS-01 と呼ばれる，DNS の TXT レコードに指示された文字列を埋め込む方式を用います。
（TLS-ALPN-01 はいまいち筆者が理解していないため，紹介は省略します。）

##### 使い方: DNS-01 チャレンジ
certbot を用いて DNS-01 チャレンジで証明書を取得します。

```console
$ sudo certbot certonly --manual -d ldap.home.local --preferred-challenges dns
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): xxxxxx@gmail.com ← メールアドレスを入力

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: a ← 規約等に同意する場合は 'a' を入力

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: n ← 'y' または 'n'
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for ldap.home.local

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y ← 'y'

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.ldap.home.local with the following value:

xxxxxx-XXXXXXXXXXXXX_xxxx-XXX_xxxxxx-XXXXXX ← ここの値を次で使う

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
```

ここまで表示されたら，エンターは押さずに以下の DNS 設定をします: 

* TXT レコード
* name: _acme-challenge.ldap.home.local
* value: xxxxxx-XXXXXXXXXXXXX_xxxx-XXX_xxxxxx-XXXXXX

設定が終わったらエンターを押します。

```plaintext
Waiting for verification...
Cleaning up challenges


IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/ldap.home.local/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/ldap.home.local/privkey.pem
   Your cert will expire on 2020-05-30. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

```

このような表示になれば DNS-01 チャレンジ成功です。
`/etc/letsencrypt/live/` ディレクトリ以下にファイルが生成されているはずです。


###### ファイルの閲覧権限を設定する
certbot で取得した証明書類のファイルは所有者が root となります。
また，秘密鍵は root のみ読み書きできる権限になります。
しかし，LDAP サーバの実行ユーザは openldap のため，これらのファイルの読み込みができないです。
そこで ACL（アクセス制御リスト）を用いて，通常のファイルアクセス制御に加えて，さらに高度な，詳細なアクセス制御を行います。

まずは ACL をインストールします:

```console
$ sudo apt install acl
```

ACL をインストールすると setfacl と getfacl コマンドが使えるようになります。
setfacl はアクセス制御の設定が、getfacl はアクセス制御の確認が行えます。

```console
$ getfacl /etc/letsencrypt/{archive,live}  # 現状の確認
getfacl: Removing leading '/' from absolute path names
# file: etc/letsencrypt/archive
# owner: root
# group: root
user::rwx
group::---
other::---

# file: etc/letsencrypt/live
# owner: root
# group: root
user::rwx
group::---
other::---

$ sudo setfacl -m d:u:openldap:rx /etc/letsencrypt/{archive,live}  # 設定を追加
$ sudo setfacl -R -m u:openldap:rx /etc/letsencrypt/{archive,live}  # 既存のファイルにも設定する
$ getfacl /etc/letsencrypt/{archive,live}  # 変更後の状態を確認
getfacl: Removing leading '/' from absolute path names
# file: etc/letsencrypt/archive
# owner: root
# group: root
user::rwx
user:openldap:r-x
user:pi:r-x
group::---
mask::r-x
other::---
default:user::rwx
default:user:openldap:r-x
default:group::---
default:mask::r-x
default:other::---

# file: etc/letsencrypt/live
# owner: root
# group: root
user::rwx
user:openldap:r-x
user:pi:r-x
group::---
mask::r-x
other::---
default:user::rwx
default:user:openldap:r-x
default:group::---
default:mask::r-x
default:other::---

```

これで openldap アカウントで読み込みと実行できるようになりました:clap:

それぞれのコマンドのオプションなどは　[【 getfacl 】コマンド――ファイルのアクセス制御リスト（ACL）を表示する：Linux基本コマンドTips（233） - ＠IT](https://www.atmarkit.co.jp/ait/articles/1808/17/news026.html)，[【 setfacl 】コマンド（基礎編）――ファイルのアクセス制御リスト（ACL）を設定／削除する：Linux基本コマンドTips（234） - ＠IT](https://www.atmarkit.co.jp/ait/articles/1808/23/news026.html) を確認してください。


###### leaps の設定追加
まずは起動時に LDAPS をリッスンするように設定を変更します。

```console
$ sudo vim /etc/default/slapd
```

```diff:/etc/default/slapd
- SLAPD_SERVICES="ldap:/// ldapi:///"
+ SLAPD_SERVICES="ldap:/// ldaps:/// ldapi:///"
```


設定を変更したら、サービスを再起動します。

```console
$ sudo systemctl restart slapd
```

その後、取得した証明書を読み込むように ldap の設定を変更するため、設定ファイルを作成します。

```conf:enable-ldaps.ldif
dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/letsencrypt/live/ldap.home.local/chain.pem
-
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/letsencrypt/live/ldap.home.local/cert.pem
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/letsencrypt/live/ldap.home.local/privkey.pem
```



```console
$ sudo ldapmodify -Y EXTERNAL -H ldapi:/// -f enable-ldaps.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "cn=config"

```

うまくいくとエラーはでない。

* slapd の実行ユーザ（openldap ユーザ）権限で証明書を読み取ることができない場合
* slapd が ldaps:/// を listen していない場合

```console
$ sudo ldapmodify -Y EXTERNAL -H ldapi:/// -f enable-ldaps.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "cn=config"
ldap_modify: Other (e.g., implementation specific) error (80)
```

###### 証明書の更新
Let’s Encrypt の証明書のは 3 ヶ月で有効期限が切れます。
なので，証明書の定期的な更新が必要です。
そして証明書が更新されたときは LDAP サーバのリロードが必要です。

そこで証明書が更新されたときはサーバがリロードされるように設定を行います。
certbot は証明書が更新されたとき `/etc/letsencrypt/renewal-hooks/deploy` 以下の実行ファイルを実行するため，この機能を利用します。

```sh:/etc/letsencrypt/renewal-hooks/deploy/slapd-reload
#!/bin/sh
systemctl force-reload slapd
```
参考: [Let's EncryptのSSL証明書更新時にサービスを再起動する - Qiita](https://qiita.com/kazuhidet/items/9d58a104f93d9ff7302d)


作成したファイルに実行権限を付与します:

```console
$ sudo chmod a+x /etc/letsencrypt/renewal-hooks/deploy/slapd-reload
```

実際に強制的に証明書を更新する事でうまく動作するかテストします:

```console
$ sudo certbot certonly --manual -d ldap.home.local --preferred-challenges dns
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Cert not yet due for renewal

You have an existing certificate that has exactly the same domains or certificate name you requested and isn't close to expiry.
(ref: /etc/letsencrypt/renewal/ldap.home.local.conf)

What would you like to do?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Keep the existing certificate for now
2: Renew & replace the cert (limit ~5 per 7 days)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2 ← 2 を入力して新規で作り直す
Renewing an existing certificate

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/ldap.home.local/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/ldap.home.local/privkey.pem
   Your cert will expire on 2020-06-02. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

```

リロードされたかはステータスから確認できます:

```console
$ sudo systemctl status slapd
```

## 参考
- [Raspberry Pi 3でLDAPサーバーを構築する - アジアの極東で魔法にかけられている。](http://xim0608.hateblo.jp/entry/2018/04/14/142049)
- [チャレンジのタイプ - Let's Encrypt - フリーな SSL/TLS 証明書](https://letsencrypt.org/ja/docs/challenge-types/)
- [CentOS7 に OpenLDAP をインストールし LDAPS を有効化する - らくがきちょう](http://sig9.hatenablog.com/entry/2017/11/14/000000)
- [Ubuntu 16.04 LTS : OpenLDAP : LDAP over TLS 設定 : Server World](https://www.server-world.info/query?os=Ubuntu_16.04&p=openldap&f=4)
- [Let's Encrypt の設定（Apache / OpenLDAP / Dovecot / Postfix on Debian9） - Qiita](https://qiita.com/tsuchm/items/cf3957f5413ca4f56317)
- [Let's EncryptのSSL証明書更新時にサービスを再起動する - Qiita](https://qiita.com/kazuhidet/items/9d58a104f93d9ff7302d)


## 次にやること
わざわざ定期的に証明書の更新を手作業でやるのは美しくないので，次は証明書の自動更新にチャレンジしてみると良いと思います。


## あとがき
これで NAS のログインも自作ウェブアプリのログインも共通の情報でできるようになります🙌

ちなみにアカウントの作成などには **LDAP Account Manager (LAM)** を用いました。
余裕があればそのうち記事にしたいです。