acme-dnsを使ってLet's Encryptのワイルドカード証明書を更新する

API で TXT レコードを変更できない DNS を利用しているドメインの証明書を dns-01 で更新できないかと思ってやってたのでメモ
[Let's Encryptのフォーラムのコメント](https://community.letsencrypt.org/t/google-domains-dns-api-support-not-google-cloud-dns/55480/11)で acme-dns が紹介されていたので

この記事の1年ぐらい前に同じことをする別の方法の記事がQiitaにすでにあったのでリンクしておく
[VALUE-DOMAIN でも DNS認証がしたい！ (Let's Encryptの証明書の更新の自動化)](https://qiita.com/binzume/items/698d12779b8ad5cda423)

# 状況・条件
- DNS
  - API で TXT レコードを変更できない
  - NS レコードを設定できる
- ワイルドカード証明書を使う
- ネームサーバーを変えない
- `systemd-resolved` などのリゾルバを動かしたまま使う

## この方法が適さない状況
acme-dns に使うドメインは NS レコードを設定できるのが前提。例えば無料で Dot TK とかのドメインが使える Freenom の DNS では NS レコードを設定できないので使うことができない。ネームサーバーの変更ができないようなレジストラは~~ないと思うが~~諦めるしかない。
ネームサーバーを変更するならば Cloudflare DNS などの TXT レコードの API での変更がサポートされているようなところを最初から選べば良い。

## サンプル環境
- ドメイン: example.org
- グローバル IP アドレス: 203.0.113.1

# 手順
1. https://github.com/joohoi/acme-dns#installation に従ってインストール
2. `config.cfg` の編集
3. `certbot` の renewal pre/post hook と `--manual-auth-hook` を用意する
4. `53/udp` を解放

## 用意
acme-dns は https化されていることが前提なので、あらかじめ証明書を取得しておく必要がある。これは TXT レコードの手動変更とかで取ればいいと思うのでこの辺とか参考にしてワイルドカード証明書を発行する
[Let's Encrypt (certbot) でワイルドカード証明書できた！](https://qiita.com/chamaharun/items/566c78bb8a13b4c2bc16)

## systemd で管理する
`$GOBIN/acme-dns` を `/usr/local/bin/acme-dns` へ symlink しておくので `$GOPATH/src/github.com/joohoi/acme-dns/acme-dns.service` を書き換える必要もなくなる。なので symlink で systemd の管理下へ置く
Installation では `/etc/systemd/system/acme-dns.service` へ移せとあるけど、`/usr/lib/systemd/system/acme-dns.service` にしておく
こうすると `systemctl <enable|disable> acme-dns` をしても困らない[^systemd symlink]

## config.cfg
- DNS のリゾルバと共存させるにはサーバーのグローバルIPを利用する [^dns global ip]
- ~~更新の時に Nginx を止めるようにしているので~~今回はリバースプロキシは利用しないが、~~止めなければ~~リバースプロキシを使うことで acme-dns で証明書を設定する必要がなくなる [^reverse Nginx proxy]
  - ただ、常に acme-dns を動かしておかないといけない状況はあまりないと思うので好みで
  - 更新の時に Nginx を止めているとかなり長い時間ダウンタイムがある事になるので 8080 番ポートで受ける形にしている

```conf:config.cfg
[general]
# DNS interface. Note that systemd-resolved may reserve port 53 on 127.0.0.53
# In this case acme-dns will error out and you will need to define the listening interface
# for example: listen = "127.0.0.1:53"
#listen = ":53"
listen = "203.0.113.1:53" # グローバルip 203.0.113.1 を設定する

[api]
# 0.0.0.0 を設定して直接公開
# Nginx などでリバースプロキシを使うなら 127.0.0.1 でもいい
ip = "0.0.0.0"

# 直接公開するので 443 番ポートを選択
# リバースプロキシを使うなら HTTP alternative の 8008 番とか 8080 番とか
#port = "443"
port = "8080" # Nginx を止めない形にしたので alternative で受ける

# すでに取得済みの証明書を使うので "cert"
# "letsencrypt" の挙動は確かめてない
# リバースプロキシ使うなら "none" で
tls = "cert"

# only used if tls = "cert"
# 既存の証明書を指定
tls_cert_privkey = "/etc/letsencrypt/live/auth.example.org/privkey.pem"
tls_cert_fullchain = "/etc/letsencrypt/live/auth.example.org/fullchain.pem"
```

## フックの設定
`renewal-hooks/{pre,post}` に acme-dns と nginx の起動停止の処理を書く

```sh:/etc/letsencrypt/renewal-hooks/pre/acme-dns-start.sh
#!/bin/sh
#systemctl stop nginx
systemctl start acme-dns
```
```sh:/etc/letsencrypt/renewal-hooks/post/acme-dns-finish.sh
#!/bin/sh
systemctl stop acme-dns
#systemctl start nginx
systemctl reload nginx # restart しなくても reload で証明書を読んでくれる
```

acme-dns に TXT レコードを設定するフックは `--manual-auth-hook` で指定する。*[取得/更新する の項を参照](#取得/更新する)*

## DNSサーバーの設定
acme-dns を置くサーバーを例えば `auth.example.org` の NS レコードに設定する。 NS レコードは IP アドレスを受け付けないので適当に `ns.example.org` などで acme-dns を置いたサーバーの IP アドレスを A/AAAA レコードに入れる

```dns
ns.example.org A 203.0.113.1
auth.example.org NS ns.example.org
```

こうすることで任意のドメインで `_acme-challenge` に CNAME レコードで `<uuid>.auth.example.org` とした時に acme-dns の TXT レコードを取りに来る

## 取得/更新する
acme-dns 用の認証スクリプトは [joohoi/acme-dns-certbot-joohoi](https://github.com/joohoi/acme-dns-certbot-joohoi) や [koesie10/acme-dns-certbot-hook](https://github.com/koesie10/acme-dns-certbot-hook) などがある。`acme-dns-certbot-joohoi` は acme-dns に未登録のドメインだった時に自動でアカウントを作って `_acme_challange.example.org. CNAME <uuid>.auth.example.org.` をプリントしてくれるので、事前に `curl -sX POST https://auth.example.org/register` する手間が省ける

スクリプトを適当な場所にインストールして `--manual-auth-hook` で指定する

```bash:取得
#!/bin/bash

sudo systemctl start acme-dns

sudo certbot certonly --manual \
  --manual-auth-hook /etc/letsencrypt/renewal-hooks/acme-dns-auth.py \
  --manual-public-ip-logging-ok \
  --agree-tos -m <メールアドレス> \
  -d example.org -d \*.example.org \
  --preferred-challenges dns-01 \
  --server https://acme-v2.api.letsencrypt.org/directory

sudo systemctl stop acme-dns
sudo systemctl reload nginx
```

`--force-renewal` オプションをつけて全てのドメインの更新するときのオプションを変更させる

```bash:更新
#!/bin/bash

sudo certbot renew --force-renewal --manual \
  --manual-auth-hook /etc/letsencrypt/renewal-hooks/acme-dns-auth.py \
  --manual-public-ip-logging-ok
```

# 終わり
Cloudflare に DNS のためだけにネームサーバーを向けていたので、是非はともかくこれで Google Domains DNS に戻せる

もし不明なところ等あればコメントどうぞ

[^dns global ip]: [joohoi/acme-dns Issue #118](https://github.com/joohoi/acme-dns/issues/118#issuecomment-431744983)

[^reverse Nginx proxy]: _example use a reverse Nginx proxy and set up basic authentication_ https://community.letsencrypt.org/t/help-me-understand-acme-dns/58892/22

[^systemd symlink]: symlink を `/etc/systemd/system` に作ると `systemctl disable acme-dns` した時に消えてしまうので `/lib/systemd/system` や `/usr/lib/systemd/system` で管理するのが望ましいと思う