Let's Encrypt の設定（Apache / OpenLDAP / Dovecot / Postfix on Debian9）

## 証明書を取得

最初に Let's Encrypt の証明書を取得するためのプログラムをインストールする。[^getssl]

[^getssl]: Let's Encrypt の証明書を取得するためのプログラムとしては [getssl](https://github.com/srvrco/getssl) もある。Debian9 では、certbot はパッケージ化されていたのに対して、getssl はパッケージ化されていなかったという点が、本稿で certbot を採用した理由である。他に理由がなければ、依存ライブラリが少ない getssl の方が良いかも知れない。

```shell-session
# apt install certbot
```

certbot は Let's Encrypt のサーバと通信して、証明書を取得する。この時、利用者が本当に example.jp ドメインの管理者であることを確認するために、Let's Encrypt のサーバは `http://example.jp/.well-known/acme-challenge/` にアクセスする。したがって、この URL がアクセス可能であることを確認しておく必要がある。[^standalone]

[^standalone]: standalone モードで certbot を利用すれば、この確認や設定は省略可能。しかし、証明書の更新を自動化するためには、Apache を起動したままで証明書を更新できるように設定する必要があるため、今回は、最初の証明書取得時から Apache を起動したままで取得する手順を検討した。

以下のようなテストファイルを用意して、`http://example.jp/.well-known/acme-challenge/` にアクセス出来ることを確認する。

```shell-session
# mkdir /var/www/html/.well-known/acme-challenge
# echo HELLO > /var/www/html/.well-known/acme-challenge/index.txt
```

筆者の環境では、`http://example.jp/` は基本的にアクセスを禁止しているため、以下のように明示的に `/.well-known/acme-challenge/` に対するアクセスを許可した。

```conf:/etc/apache2/sites-available/default.conf
<VirtualHost *:80>
        ServerName example.jp
        ServerAdmin webmaster@example.jp
        DocumentRoot /var/www/html
        <Location /.well-known/acme-challenge/>
                Require all granted
        </Location>
        （略）
</VirtualHost>
```

確認が完了したら、テスト用のディレクトリを丸ごと消しておく。

```shell-session
# rm -rf /var/www/html/.well-known
```

以下のコマンドで、SSL 証明書を取得する。

```shell-session
# certbot certonly --webroot -m yourname@example.jp -w /var/www/html -d example.jp
```

すると、`/etc/letsencrypt/live/example.jp/` 配下に証明書が保存される。

## Apache の設定

以下のように証明書の場所を設定する。

```conf:/etc/apache2/sites-available/default-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
        ServerName example.jp:443
        ServerAdmin webmaster@example.jp
        （略）
        SSLCertificateFile      /etc/letsencrypt/live/example.jp/cert.pem
        SSLCertificateKeyFile   /etc/letsencrypt/live/example.jp/privkey.pem
        SSLCertificateChainFile /etc/letsencrypt/live/example.jp/chain.pem
        （略）
</VirtualHost>
</IfModule>
```

その上で、証明書を再読込するように指示する。


```shell-session
# service apache2 restart
```

## OpenLDAP の設定

証明書の場所を指定する LDIF ファイルを用意する。

```conf:sslconfig.ldif
dn: cn=config
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/letsencrypt/live/example.jp/cert.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/letsencrypt/live/example.jp/privkey.pem
-
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/letsencrypt/live/example.jp/chain.pem
```

なお、新規にサーバ証明書を設定する場合は、上記のように add 操作を指定するが、既存のサーバ証明書を変更する場合は、下記のように replace 操作を指定する。

```conf:sslconfig-replace.ldif
dn: cn=config
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/letsencrypt/live/example.jp/cert.pem
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/letsencrypt/live/example.jp/privkey.pem
-
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/letsencrypt/live/example.jp/chain.pem
```

この LDIF ファイルを、以下のコマンドで読み込ませる。

```shell-session
# ldapmodify -Y EXTERNAL -H ldapi:/// -f sslconfig.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 の実行ユーザ（Debian の場合は openldap ユーザ）権限で証明書を読み取ることができない場合
* slapd が ldaps:/// を listen していない場合

のどちらかのトラブルがあると、以下のようなエラーになる。

```shell-session
# ldapmodify -Y EXTERNAL -H ldapi:/// -f replace.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)
```

slapd の実行ユーザが証明書を読み取れるようにするには、以下のコマンドを実行する。

```shell-session
# setfacl -m u:openldap:rx /etc/letsencrypt/live
# setfacl -m u:openldap:rx /etc/letsencrypt/archive
```

slapd が ldaps:/// を listen していない場合は、以下のように `/etc/default/slapd` を編集する。

```conf:/etc/default/slapd
SLAPD_SERVICES="ldap:/// ldaps:/// ldapi:///"
```

なお、設定内容を確認するには slapcat コマンド。

```shell-session
# slapcat -b cn=config
```

## Dovecot の設定

```conf:/etc/dovecot/conf.d/10-ssl.conf
ssl = yes
ssl_cert = </etc/letsencrypt/live/example.jp/fullchain.pem
ssl_key = </etc/letsencrypt/live/example.jp/privkey.pem
```

等号 `=` 直後の不等号 `<` は省略できないので注意。

```conf:/etc/dovecot/dovecot-ldap.conf.ext
tls_ca_cert_file = /etc/letsencrypt/live/example.jp/chain.pem
```

```shell-session
# setfacl -m u:dovecot:rx /etc/letsencrypt/archive
# setfacl -m u:dovecot:rx /etc/letsencrypt/live
```

IMAP over SSL が正常に接続できているかどうかは、以下のコマンドでチェック。

```shell-session
openssl s_client -connect example.jp:993 -verify 4
```

## Postfix の設定

```conf:/etc/postfix/main.cf
smtpd_tls_security_level = may
smtpd_tls_cert_file = /etc/letsencrypt/live/example.jp/cert.pem
smtpd_tls_key_file = /etc/letsencrypt/live/example.jp/privkey.pem
smtpd_use_tls = yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
```

なお、この設定では 25/tcp で待ち受けて、必要な場合に STARTTLS で暗号化を開始するという挙動になる。そのため、動作確認には、以下のように openssl コマンドに `-starttls smtp` オプションを指定する。

```shell-session
$ openssl s_client -connect mail.example.jp:25 -starttls smtp
```

465/tcp で待ち受ける SMTP over SSL サーバを立てる必要がある場合は、`/etc/postfix/master.cf` に設定を加える必要がある。

## 証明書の自動更新

```sh:/etc/letsencrypt/renewal-hooks/post/apache2-reload
#!/bin/sh
service apache2 reload
```

```sh:/etc/letsencrypt/renewal-hooks/post/slapd-reload
#!/bin/sh
service slapd force-reload
```

```sh:/etc/letsencrypt/renewal-hooks/post/dovecot-reload
#!/bin/sh
service dovecot reload
```

```sh:/etc/letsencrypt/renewal-hooks/post/postfix-reload
#!/bin/sh
service postfix reload
```

以下のコマンドを実行して、証明書が更新されること、かつ、証明書更新後にその証明書を読み込むスクリプトが実行されること、を確認しておく。

```shell-session
# certbot renew --force-renewal
```

## DNS を利用して証明書を取得

certbot は Let's Encrypt のサーバと通信して、証明書を取得する。この時、利用者が本当に example.jp ドメインの管理者であることを確認するために、Let's Encrypt のサーバは `http://example.jp/.well-known/acme-challenge/` にアクセスする。ところが、プライベートIPアドレスを利用している閉鎖ネットワークであるとか、セキュリティ上の要件から 80/tcp が公開できない、等の各種の理由で、HTTP 経由で認証できない場合がある。

そのような場合は、[DNS-01 Challenge](https://letsencrypt.org/docs/challenge-types/) が必要になる。DNS-01 Challenge は、大雑把に言うと以下のような処理の流れになっている。

1. Certbot は Let's Encrypt に共有鍵の発行を申請する。
2. 管理者は `_acme-challenge.example.jp` というホスト名の TXT レコードに、共有鍵を設定し、設定完了を通知する。
3. Let's Encrypt は上記の TXT レコードに共有鍵が設定されていることを確認し、確認できたらサーバ証明書を発行する。

サーバ証明書の更新を自動化するには、TXT レコードの設定を自動化しなければならないという点がネックになる。DNS サーバとして Route53 を利用している場合には、[専用のプラグイン（certbot-dns-route53）を利用する方法](https://qiita.com/komazarari/items/88c1ed18765fb7cab6c1)があるらしい。筆者の場合は、DNS サーバとして BIND を使っているので、Dynamic DNS の設定を行うことにした。

ただ、主要サービスに利用している example.jp ゾーンを直接 Dynamic DNS 管理下に置くことはセキュリティ上ためらわれたので、Dynamic DNS 専用のゾーンとして acme-challenge.example.jp ゾーンを別に[用意した](https://www.atmarkit.co.jp/ait/articles/0307/08/news001.html)。その上で、example.jp ゾーンは以下のように設定し、_acme-challenge.example.jp に対する CNAME レコードを用意しておく。

```conf:example-jp-zonefile
example.jp                  IN  A      198.51.100.11
_acme-challenge.example.jp  IN  CNAME  example.jp.acme-challenge.example.jp.
```

次に、Certbot と Dynamic DNS を連携するスクリプトを用意する。Certbot と連携スクリプトは [CERTBOT_DOMAIN と CERTBOT_VALIDATION という環境変数を使って情報を連携](https://certbot.eff.org/docs/using.html)するので、必要な TXT レコードを登録するためのスクリプトは以下のようになる。

```sh:/etc/letsencrypt/dns-auth.sh
#!/bin/sh

[ -z "${CERTBOT_DOMAIN}" ] && exit 1
[ -z "${CERTBOT_VALIDATION}" ] && exit 1

# 実際には Dynamic DNS の設定を行ったサーバの IP アドレス
server=198.51.100.10
dynzone=acme-challenge.example.jp

cat <<EOF | nsupdate
server ${server}
update add ${CERTBOT_DOMAIN}.${dynzone}. 300 IN TXT "${CERTBOT_VALIDATION}"
send
EOF

# DNS情報の反映に要する時間．筆者の環境では，待機時間をゼロにすると認証失敗が多発した．
sleep 3
```

利用が終わった TXT レコードを抹消するスクリプトも用意しておく。

```sh:/etc/letsencrypt/dns-cleanup.sh
#!/bin/sh

[ -z "${CERTBOT_DOMAIN}" ] && exit 1

# 実際には Dynamic DNS の設定を行ったサーバの IP アドレス
server=198.51.100.10
dynzone=acme-challenge.example.jp

cat <<EOF | nsupdate
server ${server}
update delete ${CERTBOT_DOMAIN}.${dynzone}. 300 IN TXT
send
EOF
```

以下のように、上記スクリプトを使って DNS-01 Challenge を行うように指定して、証明書取得を行う。

```shell-session
# certbot certonly -m yourname@example.jp -d example.jp --preferred-challenges=dns --manual --manual-auth-hook /etc/letsencrypt/dns-auth.sh --manual-cleanup-hook /etc/letsencrypt/dns-cleanup.sh
```

証明書が無事に取得できたら、残る設定作業は HTTP 経由で証明書を取得した場合と同じである。