2026年6月7日
2026年6月7日
DoQ (DNS over QUIC) の鍵を更新する手順|AdGuard Home・Unbound で証明書を自動更新し、DoQ 側の反映漏れを監視する
DoQ(DNS over QUIC)サーバーの「鍵の更新」は、実際には TLS 証明書と秘密鍵を新しいものに入れ替え、DoQ の待ち受けにそれを読み込ませる作業です。つまずきやすいのはファイルの入れ替えそのものではなく、「ファイルは新しくなったのに、UDP 853 番の DoQ だけが古い証明書を返し続ける」という反映漏れです。この記事では、AdGuard Home と Unbound を例に、certbot の更新に合わせて鍵と証明書を自動で差し替える仕組みと、DoQ 側で実際に返っている証明書を定期的に確かめる監視の作り方を説明します。
dnsdist での入れ替えと、公開鍵をピン留めしているクライアントへの事前告知は別の記事で扱っているため、ここでは「毎回の更新を人手に頼らず回す」ことに絞ります。
更新の対象と、更新の頻度を決める
DoQ は QUIC の上で DNS をやり取りする方式(RFC 9250)で、QUIC は TLS 1.3 を組み込んでいます。運用者が入れ替えるのは、クライアントがサーバーを確かめるためのサーバー証明書と、その秘密鍵です。通信を暗号化する鍵は接続ごとに作られ、接続中にも自動で更新されるため、設定で触ることはありません。
更新の頻度は、使う証明書で決まります。
- 公的な CA(Let's Encrypt など)の証明書。 有効期間が短く、期限の前に自動で更新するのが前提です。CA 側では有効期間をさらに短くする方針も示されているため(2026年10月現在)、手作業での更新はいずれ回らなくなると考えて、最初から自動化しておく方が楽です。
- 社内 CA や自己署名の証明書。 期限は自分で決められますが、長くしすぎると秘密鍵が漏れたときの影響期間も長くなります。年に一度など、決まった周期で入れ替える運用にしておくと、手順を忘れずに済みます。
秘密鍵を毎回作り直すかどうかも決めておきます。certbot は既定で更新のたびに新しい秘密鍵を作ります。--reuse-key を付けると同じ鍵を使い続けます。公開鍵のピン留めをしていなければ、毎回作り直す既定の動きのままで問題ありません。ピン留めがある場合は鍵を固定し、計画したときだけ変えます(オプションの細部は certbot --help all で確認してください)。
事前に確認すること
作業の前に、次の3点を確かめておきます。
- DoQ の待ち受けがどの証明書ファイルを読んでいるか。 設定ファイルに書かれたパスを控えます。DoT や DoH と同じファイルを共有している構成が多いはずです。
- 実行ユーザーがそのファイルを読めるか。 certbot の
/etc/letsencrypt/live/とarchive/は root 以外から読めない権限になっているのが一般的です。DNS サーバーが専用ユーザーで動く場合は、ファイルを別の場所へコピーし、そのユーザーのグループに読み取りだけを許すのが扱いやすい方法です。 - 証明書を読み直す方法。 稼働中に読み直せるのか、再起動が要るのかは実装とバージョンで異なります。分からなければ再起動を前提にします。DNS の問い合わせは短いので、再起動で QUIC の接続が切れても、クライアントはすぐにつなぎ直します。
作業前には、現在の証明書と鍵をコピーして切り戻せるようにし、設定ファイルもバックアップします。可能ならステージングのサーバーで一度通してから本番に入れます。
AdGuard Home の場合
AdGuard Home は暗号化の設定画面で、証明書と秘密鍵を「内容を貼り付ける」か「ファイルのパスを指定する」かのどちらかで登録できます。自動更新するなら、ファイルのパスを指定する方式を選びます。貼り付けた内容は設定ファイルに保存されるため、更新のたびに画面で貼り直すことになるからです。
設定ファイル(AdGuardHome.yaml)では、tls の項目に次のような値が入ります。キー名はバージョンで変わる可能性があるため、手元のファイルで確かめてから編集してください。編集は AdGuard Home を止めてから行います。
tls:
enabled: true
server_name: dns.example.com
port_dns_over_tls: 853
port_dns_over_quic: 853
certificate_path: /etc/adguardhome/tls/fullchain.pem
private_key_path: /etc/adguardhome/tls/privkey.pem
certbot の更新が成功したときに動く deploy フックで、証明書をこの場所へコピーし、AdGuard Home を再起動します。
#!/bin/sh
# /etc/letsencrypt/renewal-hooks/deploy/adguardhome.sh(例)
set -e
case " $RENEWED_DOMAINS " in
*" dns.example.com "*) ;;
*) exit 0 ;;
esac
install -d -m 0750 /etc/adguardhome/tls
install -m 0644 "$RENEWED_LINEAGE/fullchain.pem" /etc/adguardhome/tls/fullchain.pem
install -m 0600 "$RENEWED_LINEAGE/privkey.pem" /etc/adguardhome/tls/privkey.pem
systemctl restart AdGuardHome
フックには chmod 755 で実行権限を付けます。サービス名の AdGuardHome は、AdGuard Home 自身の -s install で登録した場合の名前です。Docker で動かしている場合は、証明書のディレクトリをコンテナにマウントし、最後の行をコンテナの再起動に置き換えます。AdGuard Home を root 以外で動かしているなら、秘密鍵の所有者をその実行ユーザーに合わせてください。
再起動のあとに画面の暗号化設定を開くと、読み込んだ証明書の有効期限やホスト名が表示されます。表示の内容や項目名はバージョンで異なるため、画面だけで判断せず、後述の外部からの確認も行います。
Unbound の場合
Unbound は 1.22 系から DoQ の待ち受けに対応したとされていますが、ngtcp2 などのライブラリを組み込んでビルドされている必要があり、ディストリビューションのパッケージでは無効なこともあります(2026年10月現在)。unbound -V の出力でビルドオプションを確かめてください。DoT と DoQ は、同じ証明書の設定を共有します。
# /etc/unbound/unbound.conf.d/encrypted.conf(例)
server:
interface: 0.0.0.0@853
tls-port: 853
quic-port: 853
tls-service-key: "/etc/unbound/tls/privkey.pem"
tls-service-pem: "/etc/unbound/tls/fullchain.pem"
deploy フックは AdGuard Home と同じ形で、コピー先と最後の行を変えます。
install -m 0640 -o root -g unbound "$RENEWED_LINEAGE/fullchain.pem" /etc/unbound/tls/fullchain.pem
install -m 0640 -o root -g unbound "$RENEWED_LINEAGE/privkey.pem" /etc/unbound/tls/privkey.pem
unbound-checkconf && systemctl restart unbound
unbound-control reload は設定を読み直しますが、証明書の扱いはバージョンによって違いがあり、キャッシュも消えます。どちらにしてもキャッシュが消えるなら、挙動がはっきりしている再起動の方が確認しやすいと考えて、例では restart にしています。グループ名 unbound はディストリビューションで異なるため、ps -o user,group -C unbound で確かめてください。
自動更新を試す
フックを置いたら、本番の更新を待たずに動作を確かめます。
# 証明書は更新せず、更新処理が通るかだけを試す
sudo certbot renew --dry-run
# フックそのものを手で動かして、コピーと再起動まで通るか確かめる
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/dns.example.com \
RENEWED_DOMAINS="dns.example.com" \
sh /etc/letsencrypt/renewal-hooks/deploy/adguardhome.sh
--dry-run ではテスト用の CA から仮の証明書を取得する動きになり、deploy フックは通常は実行されません。フックの中身は、上のように環境変数を渡して手で動かして確かめます。DNS サーバーのログ(journalctl -u AdGuardHome や journalctl -u unbound)に、証明書を読めないといったエラーが出ていないかも見ておきます。
DoQ 側で返っている証明書を監視する
自動更新を入れても、「フックがエラーで止まった」「DoT は新しいが DoQ は古い」といった漏れは起こり得ます。そこで、ファイルではなく外から実際に返ってくる証明書を、DoT と DoQ の両方で定期的に確かめます。
#!/bin/sh
# /usr/local/bin/check-doq-cert.sh(例)期限まで14日を切ったら終了コード1
HOST=dns.example.com
SEC=1209600
echo | openssl s_client -connect "$HOST:853" -servername "$HOST" 2>/dev/null \
| openssl x509 -noout -serial -enddate -checkend "$SEC" || exit 1
echo | openssl s_client -quic -alpn doq -connect "$HOST:853" -servername "$HOST" 2>/dev/null \
| openssl x509 -noout -serial -enddate -checkend "$SEC" || exit 1
1つめは TCP の DoT、2つめは UDP の DoQ に接続します。-quic は OpenSSL 3.2 以降の s_client で使えるオプションで、古い OpenSSL では使えません。その場合は、kdig(Knot DNS の新しい版)の +quic で問い合わせが通るかを見る、q などの DoQ 対応ツールを使う、といった代わりの方法を取ります。
この出力で見るポイントは2つです。
- 期限(notAfter)。 期限までの日数が閾値を下回ると、
-checkendで終了コードが1になります。cron や監視ツールから実行し、失敗したら通知するようにします。 - シリアル番号。 サーバー上のファイルを
openssl x509 -in /etc/adguardhome/tls/fullchain.pem -noout -serialで表示し、DoT と DoQ で返るシリアルと一致するか比べます。DoQ だけ違えば、DoQ の待ち受けが古い証明書を持ったままです。
監視はサーバー自身からではなく、別のネットワークから実行するのが理想です。サーバー内からだとファイアウォールの設定ミスで UDP 853 番が外から閉じていても気づけないためです。
鍵が漏れた疑いがあるとき
秘密鍵が第三者に読まれた疑いがある場合は、期限を待たずに入れ替えます。新しい鍵で証明書を取り直し(certbot なら --reuse-key を使わずに更新)、反映を確かめたうえで、古い証明書を失効させます。certbot では certbot revoke に --reason keycompromise を付けて失効理由を伝えられます。失効させる前に新しい証明書が DoQ と DoT の両方で返っていることを確かめておかないと、失効した証明書でクライアントが接続できなくなります。
まとめ
- DoQ の鍵の更新は、TLS 証明書と秘密鍵を入れ替えて DoQ の待ち受けに読み込ませる作業です。通信用の鍵は自動で更新されます。
- 自動化は certbot の deploy フックで行い、DNS サーバーが読める場所へコピーしてから、再起動で確実性を優先して反映します。AdGuard Home ならファイルのパス指定、Unbound なら
tls-service-keyとtls-service-pemを使います。 - フックは
--dry-runだけでなく、環境変数を渡して手で動かして確かめます。 - 監視は外から行い、DoT と DoQ それぞれで返る証明書の期限とシリアル番号を確かめると、DoQ 側だけの反映漏れに気づけます。