VALUE-DOMAIN でも DNS認証がしたい! (Let's Encryptの証明書の更新の自動化)


Let's Encryptでワイルドカード証明書を作るためにはDNSでの認証が必須なのと,SSL証明書取得のためだけに80番ポートが開いているのも微妙なのでDNS認証を使うことにしました.ただ,ネームサーバに管理用のAPIが無い場合は証明書更新の自動化のため工夫が必要だったのでその話.

SSL や Let's Encrypt や ACMEv2 については知っている前提です.あと acme.sh を使います.
例えば @tenforward さんの Let's Encrypt で acme.sh を使って ACME v2 でワイルドカード証明書を取得する とかを参照してください.

DNS認証のメリット

  • インターネットから接続できないホストの証明書も取得できる
  • 証明書の取得を行う環境とデプロイ先を別にできる
  • ワイルドカード証明書を取得できる (一番のモチベーション)

他にも色々あると思いますが,個人的にhttpを使った認証に比べて,このあたりが解決できるのが嬉しいです.

VALUE-DOMAINだと更新がつらい

  • TXTレコードを設定しても反映に時間がかかる
  • そもそも設定するためのAPIがない

VALUE-DOMAINというより,まともなAPIが無いネームサービス全般の話ですが,ACMEのDNS認証では証明書の取得や更新の要求時に指定された文字列をDNSのTXTレコードに設定する必要があるので,APIは必須です.

(もちろん,手動でTXTレコードを設定すれば使えますが,証明書の更新のたびに作業が発生するのは現実的じゃないので...)

その上,反映されるまで数十分かかったりするようなので,管理画面をスクレイピングして操作したとしても使いにくそうだし確実じゃなさそうなので却下.

TXTレコードを返答するネームサーバを用意

基本的には,APIが提供されているネームサーバを利用するのが良いと思いますが,認証のためだけに移行するのも面倒なので _acme-challenge の問い合わせだけ手元に用意したDNSに向けることにしました.

例としてexample.comのゾーンを以下のような感じにしてみます(雰囲気で察してください)

tmpdns IN A  証明書取得環境のIPアドレス
_acme-challenge IN NS tmpdns.example.com.

こうしておけば,_acme-challenge.example.com のTXTレコードの取得時に証明書取得用の環境にリクエストが来るので,指定されたTXTレコードを返してあげれば良いです.(つまり外部からUDPのport53へのアクセスが可能な場所である必要があります)

上記の設定を最初にDNSに登録するときは数十分待つ必要がありますが,一度設定してしまえば更新時に設定するのは手元のネームサーバーだけなので待つ必要はありません.
(普段は応答しないNSが登録されている気持ち悪さはありますが...)

DNSサーバを作る

BINDとかは,二度と触りたくないアプリケーション・ベスト5に入ってるので,適当な簡易DNSサーバを書きました.Golang なら https://github.com/miekg/dns とか使うと一瞬でできるので良い時代になった.

作った簡易DNSサーバ: https://github.com/binzume/tmpdns (TmpDNS)

起動時に引数に指定したレコードを返すだけの簡単なDNSですが,更新中にバックグラウンドで動かしやすいようにDockerイメージを作りました.

docker pull binzume/tmpdns
docker run --rm -p 53:53/udp --name tmpdns binzume/tmpdns -z example.com. "hoge:txt:hello" "fuga:a:192.168.0.1"
dig fuga.example.com @localhost  # 192.168.0.1
dig txt hoge.example.com @localhost  # "hello"

証明書を取得する

acme.shを使ってるなら,更新のタイミングでシェルスクリプトを実行してくれるので,そこにDNSを操作するコマンド書いておくと自動化できます.

上で説明したNSレコード等が登録した上で,リポジトリ内のdns_tmpdns.sh というスクリプトを,~/.acme.sh/dnsapi/ あたりに置いておくと,以下のようなコマンドで証明書を取得したときに裏で起動されます.(内部でDokcerコンテナを起動するのでDockerが必要です. Docker使わない版も同じリポジトリに追加しましたが,実運用はしてないので挙動は怪しいかもです)

acme.sh --issue --dnssleep 60 --dns dns_tmpdns -d example.jp -d *.example.jp

dnssleep は10秒とかでも大丈夫そうですが,しばらく運用してダメだったら調整します.

証明書の更新は以下のように--cronをつけて定期的に実行するようにしておけば,期限が近づいた証明書を自動的に更新してくれます.

acme.sh --cron

とりあえずこれで VALUE-DOMAIN使っててもワイルドカード証明書が使えるようになったので一旦満足.

  • 面倒臭がらずAPIのある真っ当なネームサービスに移行するほうが良い気もした
  • acme.shでワイルドカードを先頭にして証明書作るとファイル名に,"*"が入ってて一瞬ぎょっとする
  • example.jp と *.example.jp は同じ _acme-challenge に対して別々のTXTレコードが必要なの気づかず少しハマった
  • いつの間にかIPv6対応されていた.以前AAAAレコードしか無いドメインで困ったことがあったのでこれで安心
  • 証明書がほしい(サブ)ドメインごとに_acme-challengeに対するNSレコードが出来てしまう...

https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode を見ると,CNAMEを見てくれるようなことが書いてあるので,実際は以下のようにしています.ドメインやサブドメインごとにNSレコードが増えなくなります.(NSがCNAMEになるだけですが...)

_acme-challenge IN CNAME _acme-challenge.sub.example.com.
tmpdns IN A  証明書作成環境のIPアドレス
sub    IN NS tmpdns.example.com.