CertbotとBINDの組み合わせでLet's Encryptのワイルドカード証明書を取得・更新する

# 初めに

Let's Encryptが[ワイルドカード証明書のサポートの開始](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579 "ワイルドカード証明書のサポートの開始")をアナウンスしてから5か月近くが経過しました。そろそろいろいろな情報が出回っている頃だし、私も自宅環境でワイルドカード証明書を使ってみようと思い具体的なやり方を調べてみたのですが、

* クライアントとして[Certbot](https://certbot.eff.org/ "Certbot")を使う。
* DNSサーバとして[BIND](https://www.isc.org/downloads/bind/ "BIND")を使う。
* 証明書の取得や更新をコマンド一発で完了するようにする(i.e. 取得や更新に設定ファイルの編集などの手作業を伴わない)。

という条件を付けると、意外なことにあまり情報がありませんでした。それでも試行錯誤を繰り返して、何とか上記の条件でワイルドカード証明書を取得・更新できるようになったので、これから同じことをやる人の参考になるかと思い、設定の手順をこちらに投稿してみることにしました。

# 前提

以下では証明書のsubjectのCNが`*.example.org`であるようなワイルドカード証明書を取得する方法を説明しますが、そのためには以下の4点が必要となります。

1. BINDを動かすマシン(DNSサーバ)に対して外部からDNSの問い合わせをすることが可能なこと
2. example.orgの権威DNSサーバが`_acme-challenge.example.org`というサブドメインを1のDNSサーバに権限移譲していること
3. Certbotを動かすマシン(クライアント)から1のDNSサーバに対してDNSの問い合わせをすることが可能なこと
4. クライアントからLet's Encryptのサーバに対してHTTPSでのアクセスが可能なこと

1は要するにDNSサーバはパブリックであることが必要で、外部からアクセスできない企業の内部ネットワークにあるDNSサーバとかでは駄目です。一方クライアントは必ずしもパブリックな環境にある必要はなく、3と4の条件を満たせば内部ネットワークにあるマシンでも構いません。

2に関しては、1のDNSサーバ自体がexample.orgの権威DNSサーバであるなら何も問題はないですし、そうでなくてもexample.orgのDNSサーバがBINDとかNSDとかPowerDNSを使っているのなら、単純にそのDNSサーバの管理者に設定をお願いできるかという問題になるのですが、レジストラのドメイン管理に付随するDNSサービスを利用しているとかだと話は難しくなるかもしれません。ユーザがゾーンデータをまったく編集できないというのはさすがにないと思いますが、サブドメインを認めていないサービスというのはありうるかもしれませんし、また認めていても`_acme-challenge`というのがドメイン名としては不正なので、指定できなかったりするかもしれません。この辺りは各サービスの仕様をご確認ください。以下の説明では、1のDNSサーバがexample.orgの権威DNSサーバであるとします。

# 環境

私が普段使っているのが[FreeBSD](https://www.FreeBSD.org/ "FreeBSD")なので、CertbotやBINDのインストールや設定の手順もFreeBSDを前提としたものになります。Linuxなど他のOSを使っている方は、インストールに関してはそれぞれのOSでの方法を別途調べていただく必要がありますが、設定に関してはファイルやディレクトリのパスを適宜読み替えていただけば他のOSでも通用すると思います。

# ソフトウェアのインストール

## BINDのインストール

FreeBSDのportsには複数のバージョンのBINDがありますが、現時点での最新の安定版は9.14系列なので、これをインストールします。
Portsを使ってインストールするなら、rootで以下のコマンドを実行します

```shell
make -C /usr/ports/dns/bind914 install
```

またpackageを使ってインストールするなら、同じくrootで以下のコマンドを実行します。

```shell
pkg install dns/bind914
```

インストールが完了したら、`/etc/rc.conf.local`に以下の一行を追加します。

```
named_enable="YES"
```

その後rootで

```shell
service named start
```

とすれば`named`が起動されます。またマシンを再起動した場合などには、自動的に`named`が起動されます。

## Certbotのインストール

CertobotとBINDの組み合わせでワイルドカード証明書を取得するには、Certbot本体のほかに`dns-rfc2136`というプラグインをインストールする必要があります。
FreeBSDでは

```shell
make -C /usr/ports/security/py-certbot-dns-rfc2136 install
```

または

```shell
pkg install security/py-certbot-dns-rfc2136
```

とすればCertbot本体とdns-rfc2136プラグインが両方インストールされます。

# 設定

## 認証鍵の作成

CertbotとBINDの組み合わせではRFC2136に基づくゾーンデータの動的更新を行うので、更新を認証する鍵を作成する必要があります。
BINDをインストールすると`tsig-keygen`というコマンドが`/usr/local/sbin`の下にインストールされますが、これを

```shell
tsig-keygen certbot-key
```

のように実行すると

```
key "certbot-key" {
	algorithm hmac-sha256;
	secret "nWvUxOpLj0YvptnyvkzmAm271P3jEXzUgXV+qGYAci8=";
};
```
このような内容のデータが標準出力に出力されるので、これをDNSサーバの`/usr/local/etc/namedb/certbot-key.key`というファイルに書き出し、所有者を`bind`、パーミッションを0600に設定します。

## BINDの設定

ここでは本題であるワイルドカード証明書の取得・更新に関係する設定のみを説明します。その他の一般的な設定についてはBIND 9 Administrator Reference Manualなどを参考にして適切に行ってください。

Certbotは`*.example.org`のワイルドカード証明書を取得するにあたり、次のような処理を行います。

1. _acme-challenge.example.orgのSOAレコードが存在するか確認する
2. _acme-challenge.example.orgのTXTレコードに認証用のデータを追加する
3. 2で追加したTXTレコードを取得することによってドメインを認証する
4. 証明書発行の処理を行う
5. 2で追加したTXTレコードを削除する

従ってBINDについてはそれに対応した設定を行う必要があります。

### named.conf

`/user/local/etc/namedb/named.conf`を以下のように設定します。

```
include "/usr/local/etc/namedb/certbot-key.key"; // 認証鍵の読み込み

zone "example.org" {
	type master;
	file "/usr/local/etc/namedb/master/example.org.zone";
	masters {
		10.100.200.1; // slaveのアドレス
	};
	check-names ignore;
};

zone "_acme-challenge.example.org" {
	type master;
	file "/usr/local/etc/namedb/dynamic/_acme-challenge.example.org.zone";
	check-names ignore;
	update-policy {
		grant certbot-key. name _acme-challenge.example.org. TXT;
	};
};
```

example.orgゾーンに関しては一般的なmasterの設定です。ただ`_acme-challenge`という不正なサブドメインを利用するために`check-names ignore`の設定が必要です。

_acme-challenge.example.orgゾーンに関しては

```
	update-policy {
		grant certbot-key. name _acme-challenge.example.org. TXT;
	};
```

この設定で_acme-challenge.example.orgのTXTレコードの動的な編集を許可しています。grantの直後に指定されているのは、認証に用いる鍵の名前です。

### example.orgのゾーンファイル

以下のような内容で`/usr/local/etc/namedb/master/example.org.zone`を作成します。

```
$TTL 1h
@		IN	SOA	ns1.example.org.	admin.example.org. (
					2018080300	; Serial
					1h		; Reflesh
					15m		; Retry
					30d		; Expire
					1h		; Neg. cache TTL
                                )
		IN	NS	ns1.example.org.
		IN	NS	ns2.example.org.
ns1		IN	A	10.100.100.1
ns2		IN	A	10.100.200.1
_acme-challenge	IN	NS	ns1.example.org.
```
重要なのは_acme-challengeサブドメインの権限をns1.example.orgに委譲していることでしょうか。

このファイルは読み出し専用なので、所有者をrootにしてパーミッションを0444とかにしておきましょう。

### _acme-challenge.example.orgのゾーンファイル

以下のような内容で`/usr/local/etc/namedb/dynamic/_acme-challenge.example.org.zone`を作成します。

```
$TTL 1h
@		IN	SOA	ns1.example.org.	admin.example.org. (
					2018080300	; Serial
					1h		; Reflesh
					15m		; Retry
					30d		; Expire
					1h		; Neg. cache TTL
				)
		IN	NS	ns1.example.org.
```

このファイルはnamedによって更新されるので、所有者をbindにしてパーミッションを0644にしておきます。またどこか他の場所にこのファイルをバックアップしておいたほうが良いかもしれません。

さらに親ディレクトリである`/usr/local/etc/namedb/dynamic`も所有者をbindに、パーミッションを0755にしておく必要があります。これはゾーンデータが動的更新されると、namedがこのディレクトリの下に`_acme-challenge.examle.org.zone.jnl`という名前のファイルを作成するからです。FreeBSDでportsやpackageを使ってBINDをインストールした場合には、このディレクトリが所有者bind、グループbind、パーミッション0755で自動的に作成されます。

## Certbotの設定

以下のような内容で`/usr/local/etc/letsencrypt/dns-rfc2136.ini`を作成します

```INI
# _acme-challenge.example.orgゾーンのmasterサーバのアドレス
dns_rfc2136_server = 10.100.100.1
# アクセスするポート番号
dns_rfc2136_port = 53
# 認証鍵の名前
dns_rfc2136_name = certbot-key.
# 認証鍵の値
dns_rfc2136_secret = (生成した鍵の値)
# 鍵の生成に用いたアルゴリズム
dns_rfc2136_algorithm = HMAC-SHA256
```

認証鍵に関する情報を含むため、このファイルは所有者をrootに、パーミッションを600に設定します。

# アカウントの作成

証明書を取得する前にアカウントの作成を行う必要があります。
アカウントを作成するにはrootで以下のコマンドを実行します。

```shell
certbot register
```

実行すると

* 連絡用アドレスの入力
* 利用条件を受諾するか否かの確認
* 連絡用アドレスをCertbotの開発元である[電子フロンティア財団](https://www.eff.org/ "電子フロンティア財団")とシェアしてよいかの確認

が行われます。これらをすべてコマンド一発で完了させたければ、アドレスをシェアする場合には

```shell
certbot register --email admin@example.org --agree-tos --eff-email
```

シェアしない場合には

```shell
certbot register --email admin@example.org --agree-tos --no-eff-email
```

とすればよいです。

# 証明書の取得

証明書を取得するにはrootで以下のコマンドを実行します。

```shell
certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /usr/local/etc/letsencrypt/dns-rfc2136.ini -d '*.example.org'
```

BINDの設定などに問題がなければ、証明書の取得が正常に終了します。
取得した証明書や秘密鍵は` /usr/local/etc/letsencrypt/live/example.org`の下にあります。

# 証明書の更新

(2019年4月2日：証明書を利用しているアプリケーションのリロード処理をCertbotのdeploy-hookを用いる方式に変更)
(2019年10月12日：Port/packageに追加されたperiodic scriptを用いてCertbotを定期的に実行する方法を追記)

証明書を更新するにはrootで以下のコマンドを実行します。

```shell
certbot renew
```

このコマンドを実行するとCertbotは証明書の期限を確認して期限が迫っていれば証明書の更新処理を行います。

証明書が更新された場合、証明書を利用しているアプリケーション(Apache HTTP Server. NGINX, Sendmail, Postfix, Dovecot, etc.)に更新された証明書を読み込ませる必要があります。Certbotのrenewサブコマンドには証明書が更新されたときにコマンドを実行する仕組みがあるので、それを利用します。

まず以下のようなシェルスクリプトを`/usr/local/sbin/certbot-deploy-hook`として作成します。

```shell
#!/bin/sh
 
LANG=C
PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/sbin:/usr/sbin
services="apache24 postfix dovecot"
 
if [ $(id -u) != 0 ]
then
    echo "This command requires root previlege." 1>&2
    exit 1
fi
 
for service in ${services}
do
    service ${service} reload || exit $?
done
exit 0
```

スクリプトの変数`services`の値は証明書を利用しているアプリケーションにあわせて変更してください。なお`PATH`の値を適切に設定すれば、このスクリプトはLinux系のOSでも利用できるはずです。ただ`systemd`を使っているのならば、

```shell
    service ${service} reload || exit $?
```

のところは

```shell
    systemctl reload ${service} || exit $?
```

とした方がより自然かもしれません。

次に以下の一行を`/usr/local/etc/letsencrypt/cli.ini`に追加(なければ作成)します。

```INI
deploy-hook = /usr/local/sbin/certbot-deploy-hook
```

これで証明書が更新された時に`/usr/local/sbin/certbot-deploy-hook`が実行されるようになります。

あとはrootのcrontabに

```crontab
45 3 * * 6 /usr/local/bin/certbot renew
```

というエントリを追加しておけば、毎週土曜日の3時45分に更新処理が自動的に実行されます。

(2019年10月12日追記)

2019年9月9日の[リビジョン511693のコミット](https://svnweb.freebsd.org/ports?view=revision&revision=511693)でsecurity/py-certbotのportに更新処理を実行するweekly periodic scriptが追加されました。Quarterly branchにも2019Q4からこのコミットが含まれるようになったので、port/packageいずれを使っている場合でも、`/etc/periodic.conf`に

```shell
weekly_certbot_enable="YES"
```
の一行を追加すれば、毎週土曜日の4時15分から実行されるweekly periodic scriptの実行処理の中で、更新処理が実行されるようになります。

# 参考文献

* [BIND 9 Administrator Reference Manual](https://www.home.utahime.org/BIND9Documents/Bv9ARM.html "BIND 9 Administrator Reference Manual")
* [Welcome to certbot-dns-rfc2136’s documentation!](https://certbot-dns-rfc2136.readthedocs.io/en/latest/ "Welcome to certbot-dns-rfc2136’s documentation!")
* [Documentation about DNS credential files extremly hard to find... even for 0.22 · Issue #5663 · certbot/certbot](https://github.com/certbot/certbot/issues/5663 "Documentation about DNS credential files extremly hard to find... even for 0.22 · Issue #5663 · certbot/certbot")