acme-dns で Let's Encrypt のワイルドカード証明書を発行・更新する

# はじめに

無料で SSL 証明書を発行してくれる [Let's Encrypt](https://letsencrypt.org/) では、証明書の発行・更新の際にドメインの所有者であることを証明する必要があるが、その方法として次の2つがよく使用される。

* HTTP-01 チャレンジ
* DNS-01 チャレンジ

(参考: [チャレンジのタイプ - Let's Encrypt](https://letsencrypt.org/ja/docs/challenge-types/))

ワイルドカード証明書 (「*.example.com」のようなすべてのサブドメインで有効な証明書) を発行してもらう場合は、DNS-01 チャレンジを使用する必要がある。

## DNS-01 チャレンジとは

自身が所有するドメインの権威 DNS サーバにおいて、Let's Encrypt から指定された特定の値を TXT レコードに設定することにより、ドメインの所有権を証明する。

```:例
_acme-challenge.example.com. IN TXT 82QiAOKu66D70Jm-Q3nAijh29d2587F3HiuhEgWGFf8
```

Let's Encrypt の証明書の有効期限は90日なので、約3ヶ月ごとに DNS サーバのレコードを変更する必要があり、手間がかかる。そこで acme-dns という仕組みを利用する。

## acme-dns とは

[acme-dns](https://github.com/joohoi/acme-dns) とは、DNS-01 チャレンジ認証を実施するための

* 簡易 DNS サーバ
* Web API サーバ

がセットになった、Go 言語で書かれたオープンソースのソフトウェアである。

# 用意するもの

## acme-dns 用サーバ

今回は Ubuntu Server 18.04 をインストールした適当な VPS を使う。グローバル IP アドレスが必要。

## Let's Encrypt 用クライアント

適当な Linux 環境。今回は発行した SSL 証明書を使用する Web サーバを想定。

# 文中のパラメータ

文中の下記の値は、環境に応じて適宜読み替えること。

|key|value|
|---|---|
|acme-dns サーバのホスト名|acme-dns.example.net|
|acme-dns 用のゾーン|acme-dns.example.net|
|acme-dns サーバの IP アドレス|203.0.113.10|
|SSL 証明書を発行するドメイン|example.com|


# 構築手順

## acme-dns サーバ用の DNS レコードの登録

acme-dns で使用するドメイン (例: example.net) の権威 DNS に、次のレコードを登録する (SSL 証明書の発行は、このドメインに限られないのでご安心を)。

* acme-dns サーバの A レコード
* DNS-01 チャレンジで使用する NS レコード

```:例
acme-dns.example.net. IN A 203.0.113.10
acme-dns.example.net. IN NS acme-dns.example.net. 
```

なお今回は前記「文中のパラメータ」の通り、「acme-dns サーバのホスト名」と「acme-dns 用のゾーン」で同じ名前を使うので、A レコードはグルーレコードの役割も果たす。このゾーンの権威サーバも acme-dns が担う。

## acme-dns サーバ

今回は Docker で acme-dns サーバを動かすことにする。

まず用意した Ubuntu Server 18.04 に

* Docker 環境
* docker-compose コマンド

をインストールしておく。

Ubuntu で systemd-resolved が動いている場合、acme-dns が Port 53 を listen するのに邪魔になるので無効化する (下記「1.1.1.1」等の部分は、適当な DNS リゾルバを指定)。

```
$ sudo systemctl stop systemd-resolved
$ sudo systemctl disable systemd-resolved
$ cat << EOM | sudo tee /etc/resolv.conf
nameserver 1.1.1.1
nameserver 1.0.0.1
EOM
```

acme-dns のソースを GitHub から持ってくる。

```
$ git clone https://github.com/joohoi/acme-dns
```

設定ファイルやデータベース用のディレクトリを作成する。

```
$ cd acme-dns
$ mkdir data db
```

設定ファイルを作成する。

```
$ cp config.cfg config/
```

```ini:config/config.cfg
[general]
listen = "0.0.0.0:53"
protocol = "both"
domain = "acme-dns.example.net"
nsname = "acme-dns.example.net"
nsadmin = "admin.example.net"
records = [
    "acme-dns.example.net. A 203.0.113.10",
    "acme-dns.example.net. NS acme-dns.example.net.",
]
debug = true

[database]
engine = "sqlite3"
connection = "/var/lib/acme-dns/acme-dns.db"

[api]
ip = "0.0.0.0"
disable_registration = false
port = "443"
tls = "letsencrypt"
acme_cache_dir = "api-certs"
corsorigins = [
    "*"
]
use_header = false
header_name = "X-Forwarded-For"

[logconfig]
loglevel = "debug"
logtype = "stdout"
logformat = "text"
```

なお acme-dns の API は HTTPS なので、自身にも SSL 証明書が必要になるが、上記の `tls` オプションで `letsencrypt` を指定することで、自動的に Let's Encrypt で証明書を発行して適用してくれる。

設定ができたので、docker-compose コマンドで acme-dns を起動する (`-d` はバックグラウンドで起動するオプション)。

```
$ docker-compose build
$ docker-compose up -d
```

以上で acme-dns サーバが構築できた。

# SSL 証明書の発行

以後は、適当な Linux 環境 (例: 証明書を使用する Web サーバ) で作業する。

あらかじめ以下のコマンドをインストールしておく。

* curl
* jq

## acme-dns のユーザ登録 (初回のみ)

構築した acme-dns に対して、証明書を発行するドメイン (ゾーン) ごとにユーザ登録が必要になる。

ユーザ登録は Web API で行える。なお下記の `203.0.113.0/24` の部分は、このユーザが acme-dns を使用できる IP アドレス (つまり Web サーバ等) の範囲を指定する。必須パラメータではないが、セキュリティ上指定しておいた方が良い。

```
$ curl \
  -s \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "allowfrom": [
        "203.0.113.0/24"
    ]
}' https://acme-dns.example.com/register | jq .
```

すると以下のような結果が返ってくるので、これをメモしておく。

```json
{
  "username": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
  "password": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
  "fulldomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.acme-dns.example.net",
  "subdomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "allowfrom": [
    "203.0.113.0/24"
  ]
}
```

## DNS レコードの登録 (初回のみ)

SSL 証明書を発行するドメイン (例: example.com) の権威 DNS サーバに、acme-dns を使用するための CNAME レコードを登録する。下記の `xxx...` の部分は、上記結果の `fulldomain` の値に置き換えること。

```:例
_acme-challenge.example.com. IN CNAME xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.acme-dns.example.net.
```

## SSL 証明書の発行

いよいよ acme-dns を使って Let's Encrypt の SSL 証明書を発行する。

acme-dns に対応した[クライアントの実装はいくつかある](https://github.com/joohoi/acme-dns#clients)。また、よく Let's Encrypt で使われる certbot コマンドの hook スクリプト機能を使用する方法もあるが、今回は [acme.sh](https://github.com/acmesh-official/acme.sh) というとても便利なクライアントを見つけたので、これを使用する。

まず acme.sh をインストールする。

```
$ curl https://get.acme.sh | sh
$ . ~/.bashrc
```

インストールできたら、`-h` オプションでヘルプを見てみる。

```
$ acme.sh -h | head -20
https://github.com/acmesh-official/acme.sh
v2.8.6
Usage: acme.sh  command ...[parameters]....
Commands:
  --help, -h               Show this help message.
  --version, -v            Show version info.
  --install                Install acme.sh to your system.
  --uninstall              Uninstall acme.sh, and uninstall the cron job.
  --upgrade                Upgrade acme.sh to the latest code from https://github.com/acmesh-official/acme.sh.
  --issue                  Issue a cert.
  --signcsr                Issue a cert from an existing csr.
  --deploy                 Deploy the cert to your server.
  --install-cert           Install the issued cert to apache/nginx or any other server.
  --renew, -r              Renew a cert.
  --renew-all              Renew all the certs.
  --revoke                 Revoke a cert.
  --remove                 Remove the cert from list of certs known to acme.sh.
  --list                   List all the certs.
  --showcsr                Show the content of a csr.
  --install-cronjob        Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job.
```

証明書を発行する用のシェルスクリプト (例: `letsencrypt.sh`) を作成する。

```bash:letsencrypt.sh
#!/usr/bin/env bash

OPT=$1

DOMAIN="example.com"
export ACMEDNS_BASE_URL="https://acme-dns.example.net"
export ACMEDNS_USERNAME="zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"
export ACMEDNS_PASSWORD="yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
export ACMEDNS_SUBDOMAIN="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

~/.acme.sh/acme.sh \
  $OPT \
  --debug \
  --dns dns_acmedns \
  -d "$DOMAIN" \
  -d "*.$DOMAIN"
```

シェルスクリプトを実行する。

```
$ chmod +x letsencrypt.sh
$ ./letsencrypt.sh --issue
```

うまくいくと、`$HOME/.acme.sh/example.com/` ディレクトリ以下に、SSL 証明書や秘密鍵が発行されるので、これらのファイルを Web サーバ等から参照する。

```
$ ls -1 $HOME/.acme.sh/example.com
ca.cer
fullchain.cer
example.com.cer
example.com.conf
example.com.csr
example.com.csr.conf
example.com.key
```

dig コマンドを使用すると、acme-dns が TXT レコードを応答していることが確認できる。

```
$ dig _acme-challenge.example.com. txt +short
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.acme-dns.example.net.
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
```

なお acme.sh の `--list` オプションで、発行済証明書の一覧が確認できる。

```:例
$ acme.sh --list
Main_Domain     KeyLength  SAN_Domains       Created                       Renew
example.com  ""         *.example.com  Wed Jun 17 11:02:23 UTC 2020  Sun Aug 16 11:02:23 UTC 2020
```

発行した証明書を更新するには、以下のコマンドを実行すれば良い。

```
$ ./letsencrypt.sh --renew
```

acme.sh には、cron による証明書の自動更新など便利な機能がいろいろあるので、研究してみよう。