投稿日:
前の記事でvalue-domain-dns-utilを作り、かつてのvalue-domain-dns-cert-register (vddcr)を焼き直したことを書いたが、あれからいくらか直し、学びも得たので、その記録として残す。
なお、今回行った調査によりvalue-domain-dns-utilのvd-dcr.plでは、複数ドメイン指定(-d hoge.example.com -d fuga.example.com)や、ワイルドカードドメイン指定(-d example.com -d *.example.com)が動作することを確認している。
certbotがauth-hookに渡す環境変数
ドメインを一つだけ指定したとき
--manual-auth-hookに指定したスクリプトが一回だけ走る。
入力
sudo certbot certonly --manual -n --preferred-challenges dns --agree-tos -m postmaster@example.com --manual-auth-hook "/path/to/script" -d hoge.example.com
環境変数
| 変数名 | 値 |
|---|---|
| CERTBOT_DOMAIN | hoge.example.com |
| CERTBOT_VALIDATION | xxxxx |
| CERTBOT_ALL_DOMAINS | hoge.example.com |
ドメインを複数指定したとき
指定した回数分、--manual-auth-hookに指定したスクリプトが走る。
入力
sudo certbot certonly --manual -n --preferred-challenges dns --agree-tos -m postmaster@example.com --manual-auth-hook "/path/to/script" -d hoge.example.com -d fuga.example.com
環境変数
走行一回目
| 変数名 | 値 |
|---|---|
| CERTBOT_DOMAIN | hoge.example.com |
| CERTBOT_VALIDATION | xxxxx |
| CERTBOT_ALL_DOMAINS | hoge.example.com,fuga.example.com |
走行二回目
| 変数名 | 値 |
|---|---|
| CERTBOT_DOMAIN | hoge.example.com |
| CERTBOT_VALIDATION | yyyyy |
| CERTBOT_ALL_DOMAINS | hoge.example.com,fuga.example.com |
ワイルドカードドメインを指定したとき
ドメインを複数指定したときと似た挙動をするが、CERTBOT_DOMAINは二回とも同じものが入り、CERTBOT_ALL_DOMAINSも同じドメインが二個入り、CERTBOT_VALIDATIONだけ異なる値になる。
入力
sudo certbot certonly --manual -n --preferred-challenges dns --agree-tos -m postmaster@example.com --manual-auth-hook "/path/to/script" -d example.com -d *.example.com
環境変数
走行一回目
| 変数名 | 値 |
|---|---|
| CERTBOT_DOMAIN | example.com |
| CERTBOT_VALIDATION | xxxxx |
| CERTBOT_ALL_DOMAINS | example.com,example.com |
走行二回目
| 変数名 | 値 |
|---|---|
| CERTBOT_DOMAIN | example.com |
| CERTBOT_VALIDATION | yyyyy |
| CERTBOT_ALL_DOMAINS | example.com,example.com |
TXTレコードに永続性は不要で、証明書を作るときにだけあればよいということが分かった
私は長らくDNSレコードにtxt _acme-challenge.bts "gfj9Xq...Rg85nM"のような値が存在し続けることに意味があると考えていたが、実際にはそうではなかった。
Challenge Types - Let's EncryptのDNS-01 challengeの仕様には次のようにある。
原文
After Let’s Encrypt gives your ACME client a token, your client will create a TXT record derived from that token and your account key, and put that record at _acme-challenge.<YOUR_DOMAIN>. Then Let’s Encrypt will query the DNS system for that record. If it finds a match, you can proceed to issue a certificate!
日本語訳
Let’s EncryptがACMEクライアントにトークンを渡すと、クライアントはそのトークンとアカウントキーから派生したTXTレコードを作成し、_acme-challenge.<YOUR_DOMAIN>に格納します。その後、Let's EncryptはDNSシステムにそのレコードを照会します。一致するレコードが見つかった場合、証明書の発行に進むことができます。
つまり、DNS-01 challengeにおけるTXTレコードはドメインの所有権を確認するための一時的な検証値に過ぎず、証明書の発行が完了すれば不要になる。
この仕様を知ったことで、ワイルドカードドメイン指定時の懸念が解消された。ワイルドカードでは_acme-challenge.example.comに対して--manual-auth-hookに指定したスクリプトが二度走り、そのままでは二回目で一回目のTXTレコードが上書きされるが、永続する必要がないなら問題にならない。
以前はワイルドカードドメインに対してTXTレコードを二個維持する必要があると考え、「_acme-challengeのTXTレコードがDNS上に二行以上あればワイルドカード用とみなして全削除してから追加、二行未満なら単純に追加」という判別ロジックを検討していた。
しかしこの方法では、前回のレコードが一行だけ残っている場合に破綻する。ワイルドカード指定では--manual-auth-hookに指定したスクリプトが二回走るので、次のようなことが起きる。
- 一回目:既存レコードは一行(前回の残り)なので「二行未満→追加」が選ばれ、計二行になる
- 二回目:既存レコードが二行になったため「二行以上→全削除して追加」が選ばれ、一回目で登録したばかりのレコードごと消えてしまう
このように既存レコード数という外部状態に依存した判別では、初期状態の違いによって正しく動かないケースが生じ、運用でカバーせざるを得なかった。
だが、TXTレコードに永続性は不要で、証明書を作るときにだけあればよいということが分かり、結果としてレコードを永続させる必要がないと分かったことで、このような判別ロジック自体が不要になった。
これによって例外処理をするスクリプトの作成も、それを動かすための運用も考慮しなくてよくなり、vd-dcr.plはシンプルな状態に保てるということが分かったので、とても良かったし、何よりcertbotへの理解が深まったのもよかった。
余談だがCertbotの振る舞いはRFC 8555に定められており、Automatic Certificate Management Environment (ACME)、つまり自動証明書管理環境とされているようだ。有志による日本語訳も存在し、日本語で読むこともできる。
DNS-01 challengeの運用負荷を減らす、新しいDNS-PERSIST-01という方式の登場について
DNS-01 challengeについて調べる中で、DNS-PERSIST-01という新しいACMEチャレンジタイプの存在を知った。これはDNS-01の代替ではなく、DNS-01と併存する別の選択肢として提案されているものだ。
これはまだドラフトだが、今後導入される予定のDNSを利用した証明書の発行方式で、Let's Encryptでは2026年Q2に本番導入が予定されている。
DNS-PERSIST-01の利点は一度DNSに検証文字列を書けば、以降はDNSレコードを触らなくてよいというものである。DNSのAPIを都度叩かなくてよいため、運用面で非常に便利だといえる。当然、セキュリティ面では劣化がある。
欠点はACMEアカウント鍵が漏洩すると、第三者に証明書を発行されるリスクがあることだ。DNS-01 challengeでは更新の都度、ランダムな検証文字列を設定して検証するのでこの問題は起きない。
要するにDNSレコードを弄らない代わりに鍵を使う方式と、DNSレコードを弄る代わりに盗まれる物がない方式の二つになるということだ。
DNS-PERSIST-01でもセキュリティのために、ACMEアカウント鍵に有効期限を設けられるようだが、それだと本末転倒にも思う。勿論、そういうケースが役に立つ場合もあるだろうが、運用は大変そうだ。
Value-DomainのダイナミックDNSエンドポイントは60秒以内に叩くとエラーが返ってくるので、これを回避するためのDDNSの仕組みを作った話。
やったこと
まず、Value-DomainのDNS APIに対し、既存のaレコードをバルクで差し替えるためのツールとしてvd-ddns_v4.plを作った。
そしてhotplug.dにPPPoEインターフェースのIPが変わったときに、このスクリプトを蹴る処理を書いた。
この記事の前提構成
OpenWrtにIPv4用のPPPoEインターフェースがある。
やったこと
- 今回利用するのに必要なPerl周りをセットアップする。ストレージの空きが3.2MBほど必要
- value-domain-dns-utilを
/root配下とか適当な場所に放り込むopkg install openssh-sftp-serverでSFTPを導入しておくとファイル移動に便利
vi /etc/hotplug.d/iface/40-pppoeとかして、OpenWrtのインターフェースが変化したときのHookを作る#!/bin/sh # デバイスが存在しなければ終了 [ -n "$DEVICE" ] || exit 0 # リンクアップでなければ終了 [ "$ACTION" = ifup ] || exit 0 # インターフェース名がPPPoEのものでなければ終了 [ "$INTERFACE" = wanppp ] || exit 0 # pppoe-wanpppのIPv4アドレスを取得 pppoeaddr=$(ip -4 addr show pppoe-wanppp | head -2 | tail -1 | awk '{print $2}') # ログに吐く logger -t "DDNS - PPPoE IP" $pppoeaddr # DDNSもどきを叩く /root/vd-dns-util/vd-ddns_v4.pl 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' example.com $pppoeaddr hoge fuga piyo # 結果をログに吐きたいが何故か動いていない if [ $? -eq 0 ]; then logger -t "DDNS - UPDATE SUCCEED" else logger -t "DDNS - UPDATE FAIL" fiPPPoEインターフェスをRestart
- Value-Domainのコンパネで指定したドメインのaレコードが更新されていることを確認
備考
一般的にDNSレコードの浸透時間は更新前に浸透していたTTLに依存し、Value DomainのAPI経由で普段から操作している場合は120秒以下にならないため、最大120秒のダウンタイムが発生するが、理論上は公式のDDNS機能と大差ないはずと思われる。
aレコードのみの対応にしているのは私の環境だとIPv6は変動しないが、v4はそれなりの頻度で変わるためだ。
運用しているMastodonのv4が変わったのに気づかないまま疎通できないインスタンスが出てくることがしばしばあり、手動で対応するのが手間なのと、毎回一日くらい気づくのに遅れるので今回自動化に踏み切った。前々からやり勝ったのだが、中々腰が重く進んでいなかった。
そもそもこの手のものは監視システムで検知できて然るべきなので、おいおい監視システムの構築もしていきたいところだ。
投稿日:
私は2020年の末あたりからLet's EncryptのDNS-01 challengeをValue-Domainで達成するためのスクリプトであるvalue-domain-dns-cert-register (vddcr)の開発を行ってきていたのだが、メンテが辛くなってきたので、今回Shellscript(Bash)Perlでvalue-domain-dns-utilとして作り直した。その経緯と、開発中の学び、そして最後にどうやって作っていったかについて書いていく。
背景
一年半ほど前からTypeScriptに限界を感じてきたことや、当時の誤った単体テストへの偏執により、vddcrはメンテしたくない状態になってしまっていた。
そこで昨夏、つまり2025年の8月に式年遷宮、つまり全体的な書き直しを行ったのだが、個々の処理をする部品を作ったまではいいものの、それらを繋ぎ合わせるところで飽きてしまい、放置していた。vddcrはこの時点で事実上の放棄状態だった。
しかし、自宅サーバーの運用をしていく中で別の目的でValue-DomainのDNS APIと関わらなければならない必要が出てきたため、今回、vddcrの置き換えも兼ねて新しく作り直すことにしたのだ。
具体的には複数ドメインに対するDDNSをバルクでやりたい要求が生まれたが、Value-DomainにあるDDNS APIは一度に1ドメインしか処理できない上にレートリミットが60秒に1回と極めてキツく、複数ドメインに対して行うには余りにも遅延が大きかった。
例えば10ドメイン更新しようとすると10分もかかるわけで、これでは使い物にならないというので、DNS APIを直に叩いて一気に書き換える必要性が出てきたのだ。どうせ作るならvddcrが担っていたDNS challengeの機能も包含できる形にしたかったので、Value-DomainのDNS APIを便利に叩くためのツールキットとしてvalue-domain-dns-utilを作ることにした。
つまりvddcrの純粋な後継ツールというよりは、vddcrの役目もこなせるツールキットを開発し、vddcrと同等の機能を有するツールを実装サンプルとして同梱したわけだ。
Shellscriptにした理由
ものの数時間でPerlに直したので過去の話となった。移植性の関係でbashはイマイチだった。
ぶっちゃけそんなに大した理由はなくて、単に目の前にターミナルとテキストエディタがあってパッと組めたからという部分が大きい。実際ベースはパッと作れた。まぁ作って半年も放置していたのが…。ただまぁPerlの方が保守性は高いだろうし、移植性もあるだろうとは思うから、気が向いたら書き直すかもしれない。
ちなみに別言語に移植することの検討はvddcrでも行っていたが、最終的には候補になかったShellscript(Bash)での実装に落ち着いた。そもそもこの課題の存在を忘れていたので、検討すらしていなかったのである。
value-domain-dns-utilについて
今回作ったvalue-domain-dns-utilは、vddcrの単純な代替ではなく、Value-DomainのDNS APIを叩くのに便利なShellscriptの関数群だ。DNS challenge以外にも用が生まれたので、このような汎用的な形になっている。
DNS APIから現在のレコードを取得する関数や、取得したレコードから先頭一致のパターンマッチでレコードを取得する関数、レコードリストへの追加や置換をする関数、そして編集したレコードをDNS APIに送り戻す関数を備えており、DNSに関する一連の編集処理ができるように便利に作っている。
結果として、Value-DomainのDNS APIを叩く汎用機能があれば、それを流用してLet's EncryptのDNS-01 challengeもできるし、当然複数ドメインに対するDDNSをバルクでやることも可能になる。
学び
開発中に発見も多くあったので記していく。なんかこういうのまとめた記事かページ作りたいね。
echoでドルマーク始まりのシングルクォート文字列を作ると、その中の改行コードが展開される
例えば以下のようになる。これは一行で改行展開を書けて変数に入れられるので、インデントされているコードなどで物理的にへし折りたくないときなどに有用だし、文字列としてのコードが見えるのでわかりやすい部分もあると思う。
入力
#!/bin/bash
# ドルマーク始まりのシングルクォート文字列は改行が展開される
echo $'1:aaa\nbbb'
# つまりこれと同じ
echo "2:ccc
ddd"
# ダブルクォートの中の\nは展開されない
echo "3:eee\nfff"
# ドルマーク始まりのダブルクォートも展開されない
echo $"4:ggg\nhhh"
# 一行で改行展開を書けて変数に入れられるので便利
hoge=$(echo $'5:iii\njjj')
echo "$hoge"
出力
1:aaa
bbb
2:ccc
ddd
3:eee\nfff
4:ggg\nhhh
5:iii
jjj
変数の最後の一文字を削る
これは後方一致除去(${parameter%word})と呼ばれるものらしく、詳しくは調べていないが"hoge"に対してfugaを繋げ、"hogefuga"としたい場合に便利だ。
例えば以下のように書ける。
入力
#!/bin/bash
hoge='"hoge"'
fuga='fuga'
result="${hoge%?}$fuga"'"'
echo $result
出力
"hogefuga"
sedで改行を改行コードに置換(\n→\\n)
これは見つけた当時sedの記事にも書いているが、以下のようになる。
入力
cat <<'EOF' | sed ':a;N;$!ba;s/\n/\\n/g'
aaa
bbb
ccc
ddd
EOF
出力
aaa\nbbb\nccc\nddd
[]ではGlob展開できないが、[[]]では出来る
今のところこういった形式でGlobを書くことはないのだが、Sonnet 4.6にテストコードを書かせている中で発見したので書き留めておく。
入力
#!/bin/bash
hoge='aaabbbccc'
[ $hoge = *"bbb"* ]
result1=$?
[[ $hoge = *"bbb"* ]]
result2=$?
echo $result1
echo $result2
出力
1
0
あとがき
LLMとの作業分担
今回の開発でもLLMを活用したのでその辺り。
今回の開発では、8ada152~93d58ccのコミットを行った。最も古いコミットである8ada152ではテストコードを除き、私がフルスクラッチで書き、テストコードはClaude Code(Sonnet 4.6)に書かせた。
次の029a951では、私が書いたコードをClaude Code(Sonnet 4.6)に全体的に直してもらって私がレビュー・微修正するという開発形態をとった。それ以降のコミットはコメントなどの微修正なので、再び私が手動で書いている。
こうした分担により、設計の根底にある信条のようなものには私の色が濃く出ているが、実装上の問題点はClaude Codeによって大きく改善されている。特に029a951では、typoや単純な実装ミスの修正に加えて、bashの文字列処理に不慣れなことで生まれたエスケープシーケンスの山が整理された。jqの-rオプションの有無がまちまちだった部分も統一され、可読性がだいぶ向上したと思う。
とはいえ、Claude Codeの言うことを鵜呑みにはしていない。こちらから明示的に指示して直させた箇所もある。例えばテストコードのappend_recordやreplace_recordのアサートは元々Globで書かれていたのだが、より厳密にするために全文マッチに直させた。別にレコードの順序がどうなっていようとValue-Domain APIは気にしないどころか勝手にソートされるので実用上の意味はないのだが、こういうのは人としての拘りである。
昨今は過去の記事でもちょいちょい言及している通り、LLMを使った開発をそこそこしていて徐々に馴染めてきているので、その点も含めてよい経験になった。
抽象化した今回の設計と、過去の歪な設計を振り返って
抽象化して再利用性を高めた今回のツールキット方式と、単体テスト大正義とばかりに変な作りになってしまった前回の方式の振り返り。
過去のvddcrでは単体テストへの誤った偏執によりモジュールが具象的な単位で分割され、非常にメンテナンス性が悪く、再利用性もないものになっていたが、今回はちゃんと抽象化して、責務ごとに機能が切れたと思っている。このことによって再利用性が高く、使いやすいものが作れたと考えている。大した規模ではないとはいえ、部品を組み合わせて処理を組むには程よくいい感じだろう。
先ほどのことや、単純に機能を削ったこと、TypeScriptの複雑すぎる型管理や非同期処理から解放されたこともあり、コードもだいぶ見通しがすっきりした。ツールではなく、ツールキットという形で作ったことも奏功し、Value-DomainのDNS API周りのスクリプトは大分作りやすくなったと思う。将来的にバルクのDDNSスクリプトを書くのも楽になるはずだし、重い腰を上げて作れてよかった。
式年遷宮で起きる変遷について
式年遷宮をする以上、元の状態には戻れないという話があり、それが何故起きるのかみたいなところが分ったような、分からないような、そんな話。
しかし世の中には式年遷宮で全く違う姿になるものがある。今回作ったのはまさにそんな感じで、なんというか作者の気持ちがわかった気がする。
分かりづらい例えだが、FF14の外部ツールであるConcept Matrix(CMTool)がAnamnesisに変わったときは、全く別物になった気さえしたし、こういった式年遷宮によって元あったことは一応できるにはできるけど、複雑になったり、なんかかゆいところに手が届かなくなったり…みたいなのは他でもちょいちょいあると思っていて、今回vddcrをvalue-domain-dns-utilにしたのもまさにそうだと思う。
という背景があるのもあり、完全互換を目指したvd-dcr.shを同梱することで、この不便さを少しはなくそうとした。いやまぁ自分のためにやってるので、自分のためのことではあるのだが…。とはいえ、以前のと比べるとプラットフォーム別の互換性は落ちていると思うので、現時点だと動かない環境は増えている気がする。
あとがき2:結局Perlに変えた
動かそうとしてた環境の一つのシェルがashで、bashが使えないことに気が付いたのでSonnet 4.6に頼んでサッとPerlに変えてもらった。
