Let’s Encrypt の証明書の更新を依存関係の多い certbot から dehydrated に移行して、 DNS-01 でのドメイン確認を使うようにしてみました。
環境
- Ubuntu 18.04.2 LTS (bionic)
- certbot-auto 0.34.2
- dehydrated 0.6.1-2
- bind9 1:9.11.3+dfsg-1ubuntu1.8
参考
Examples for DNS 01 hooks の example dns 01 nsupdate script を参考にしました。
例示環境
bind9 を動かしている ns.example.jp
に example.jp
のマスターがあって、 target.example.jp
と ns.example.jp
に証明書を発行したい、とします。
bind9 設定
NS の委譲設定
親ゾーンに _acme-challenge.target.example.jp
の NS の委譲設定を追加します。 一時的に TXT レコードを追加するだけのゾーンなので、 slave サーバーは追加しない方が問題が起きにくくて良いと思います。
_acme-challenge.target IN NS ns.example.jp.
_acme-challenge.ns IN NS ns.example.jp.
dynamic zone 設定
_acme-challenge
用のゾーンファイルの雛形として以下のような _acme-challenge.zone
を用意します。
$TTL 10m
@ IN SOA ns.example.jp. hostmaster.example.jp. (
1 ; Serial
1h ; Refresh
15m ; Retry
1w ; Expire
2h ; Nagative Cache TTL
);
IN NS ns.example.jp.
;; first zone file of dynamic DNS
;; see /var/cache/bind/*.zone
sudo install -o bind -g bind -m 644 zone/_acme-challenge.zone /var/cache/bind/_acme-challenge.target.example.jp.zone
sudo install -o bind -g bind -m 644 zone/_acme-challenge.zone /var/cache/bind/_acme-challenge.ns.example.jp.zone
のように Dynamic DNS 用のディレクトリに設置します。 この後は nsupdate
経由で扱うため、直接は触りません。 nsupdate
での変更はジャーナルファイル (*.zone.jnl
) に先に記録されるため、 dig
で見える最新の情報が *.zone
ファイルにあるとは限らないようです。
nsupdate 用の鍵作成
dnssec-keygen -r /dev/urandom -a hmac-sha512 -b 128 -n HOST <keyname>
で作成するということなので、 以下のように作成します。 この例だと Kdehydrated-example.+165+33269.key
と Kdehydrated-example.+165+33269.private
ができています。
% dnssec-keygen -r /dev/urandom -a hmac-sha512 -b 128 -n HOST dehydrated-example
Kdehydrated-example.+165+33269
% cat Kdehydrated-example.+165+33269.key
dehydrated-example. IN KEY 512 3 165 8PvYT0pDeQs0kuCBiOVRvA==
% cat Kdehydrated-example.+165+33269.private
Private-key-format: v1.3
Algorithm: 165 (HMAC_SHA512)
Key: 8PvYT0pDeQs0kuCBiOVRvA==
Bits: AAA=
Created: 20190628004245
Publish: 20190628004245
Activate: 20190628004245
鍵ファイル作成
bind9
と nsupdate
で共通で使うため、 /etc/bind/conf/_acme-challenge.key.conf
を作成します。
key
は dnssec-keygen
の <keyname>
と合わせる必要はなく、あとで指定する許可設定で使います。 secret
は key ファイルの最後のところか、 private ファイルの Key:
の行からコピーします。 このファイルを作れば dnssec-keygen
で作成したファイルは不要なので消してしまって構いません。
% sudo cat /etc/bind/conf/_acme-challenge.key.conf
key "dehydrated-example" {
algorithm hmac-sha512;
secret "8PvYT0pDeQs0kuCBiOVRvA==";
};
鍵が含まれるので、パーミッション: 640, owner: root, group: bind などにして、アクセス制限をしておいた方が良いです。
ゾーン作成
/etc/bind/conf/named.conf._acme-challenge.conf
として以下のように zone を作成して、 TXT レコードの変更だけ許可しました。
zone "_acme-challenge.target.example.jp" {
type master;
file "/var/cache/bind/_acme-challenge.target.example.jp.zone";
allow-query { any; };
update-policy {
grant dehydrated-example name _acme-challenge.target.example.jp TXT;
};
};
zone "_acme-challenge.ns.example.jp" {
type master;
file "/var/cache/bind/_acme-challenge.ns.example.jp.zone";
allow-query { any; };
update-policy {
grant dehydrated-example name _acme-challenge.ns.example.jp TXT;
};
};
テスト
以下のようにエラーが出てこなければ正常です。
% printf "server %s\nupdate add _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" 127.0.0.1 target.example.jp 300 "test" | sudo nsupdate -k /etc/bind/conf/_acme-challenge.key.conf
% dig +short @127.0.0.1 _acme-challenge.target.example.jp txt
"test"
% printf "server %s\nupdate delete _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" 127.0.0.1 target.example.jp 300 "test" | sudo nsupdate -k /etc/bind/conf/_acme-challenge.key.conf
bind9 の設定がちゃんとできていないと response to SOA query was unsuccessful
と出てきて失敗しました。 dehydrated コマンドで update failed: REFUSED
と出てきたこともあったので、これも nsupdate
の失敗メッセージかもしれません。
dehydrated 設定
domains.txt
対象のドメインを並べて書いておきます。 1行1証明書で、 SANs で複数入れたい場合は横に並べるようです。
% cat /etc/dehydrated/domains.txt
target.example.jp
ns.example.jp
hook
/etc/dehydrated/hook.sh
として以下の内容の hook を作成しました。
#!/bin/bash
set -euo pipefail
NSUPDATE="nsupdate -k /etc/bind/conf/_acme-challenge.key.conf"
DNSSERVER="127.0.0.1"
TTL=300
ruby -e 'printf "%s %p\n", Time.now.strftime("%Y-%m-%d %H:%M:%S"), ARGV' -- "$@" >>/var/log/dehydrated-hook.log
case "$1" in
"deploy_challenge")
printf "server %s\nupdate add _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${2}" "${TTL}" "${4}" | $NSUPDATE
;;
"clean_challenge")
printf "server %s\nupdate delete _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${2}" "${TTL}" "${4}" | $NSUPDATE
;;
"deploy_cert")
shift
/etc/dehydrated/deploy_cert.sh "$@"
;;
"unchanged_cert")
# do nothing for now
;;
"startup_hook")
# do nothing for now
;;
"exit_hook")
# do nothing for now
;;
esac
exit 0
deploy_cert.sh
hook.sh から呼び出している /etc/dehydrated/deploy_cert.sh
は以下のような内容にしました。
HTTP-01 を使っているときは常に apache2 にも証明書を設定していたので、 apache2 の reload は必ず実行していて、 DNS-01 を使うようになって不要なドメインもありますが、 restart に比べて reload はそんなに重くないので、 必要なドメインで reload を忘れた時の影響の方が大きいかと思い、 入れたままにしています。
今回の例には入っていませんが、 mx や ldap のドメインでの reload の例も入れています。
#!/bin/bash
DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
install -v -o root -g root -m 600 "${KEYFILE}" "/etc/ssl/private/${DOMAIN}.key"
install -v -o root -g root -m 644 "${FULLCHAINFILE}" "/etc/ssl/certs/${DOMAIN}.crt"
systemctl reload apache2.service
case "${DOMAIN}" in
mx*)
systemctl reload postfix.service
systemctl reload dovecot.service
;;
ldap*)
install -v -o root -g openldap -m 640 "${KEYFILE}" /etc/ssl/private/slapd.pem
install -v -o root -g root -m 644 "${FULLCHAINFILE}" /etc/ssl/certs/slapd.crt
systemctl restart slapd.service
;;
esac
最後に
自前の DNS サーバーで権威サーバーを運用している場合の DNS-01 を使った dehydrated での letsencrypt の証明書の発行方法を紹介しました。
API に対応している他の DNS サービスで DNS-01 を使う場合は、 dehydrated の wiki から各種 hook へのリンクがあるので、参考にすれば良さそうです。
ワイルドカード証明書を発行したかったり、外から HTTP(S) アクセスできないサーバーに証明書を入れたかったりする場合に HTTP-01 ではなく DNS-01 が必須になってくるので、そういうものもそのうち試してみたいと思っています。