2026/07/06(月)nginxのリバースプロキシが上手くいかない問題を解消した
投稿日:
ConoHa VPSをフロントエンドにし、自宅サーバーにリバースプロキシする構成を組んだ時のトラブルメモ。
起きていた条件
nginxからnginxへリバースプロキシする構成で前段がTLS終端で、後段へはHTTPで接続していた。前段はVPS、後段は物理サーバーで別の環境。
起きていた事象
lycolia.infoと*.lycolia.info全体で前段が後段に接続できない状態になっていた。
curl -iSsl4 https://blog.lycolia.info/
HTTP/2 000
server: nginx
date: Fri, 03 Jul 2026 15:24:44 GMT
前段のnginxにcurlを投げるとステータスコード000が返ってきて、curlが終了コード23で異常終了していた。
2026/07/04 00:11:33 [error] 2537#2537: *12468 upstream sent no valid HTTP/1.0 header while reading response header from upstream, client: xxx.xxx.xxx.xxx, server: lycolia.info, request: "GET /pub/lycolia/rss.xml HTTP/2.0", upstream: "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80/pub/lycolia/rss.xml", host: "blog.lycolia.info", referrer: "https://blog.lycolia.info/pub/lycolia/rss.xml"
nginxのエラーログには「アップストリームからのレスポンスヘッダーの読み取り中に、アップストリームが有効な HTTP/1.0 ヘッダーを送信しませんでした。」という意味のエラーを出力していた。
後段のnginxには何もログが出ていなかった。
curl -v --http1.1 -H 'Host: blog.lycolia.info' "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80/"
* Trying [2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80...
* Connected to 2400:4153:8f01:c800:c14b:3f7a:2b54:353a (2400:4153:8f01:c800:c14b:3f7a:2b54:353a) port 80
* using HTTP/1.x
> GET / HTTP/1.1
> Host: blog.lycolia.info
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
* Received HTTP/0.9 when not allowed
* closing connection #0
curl: (1) Received HTTP/0.9 when not allowed
前段のnginxのあるVPSから、後段のnginxに対してhttpバージョン1.1を明示して叩くと「HTTP/0.9 when not allowed」というエラーが出てきた。HTTP/0.9…?初めて聞く概念だ…。
curl -s --http0.9 'http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]/' | xxd
00000000: 0000 1204 0000 0000 0000 0300 0000 8000 ................
00000010: 0400 0100 0000 0500 ffff ff00 0004 0800 ................
00000020: 0000 0000 7fff 0000 0000 0807 0000 0000 ................
00000030: 0000 0000 0000 0000 01 .........
httpバージョン0.9を明示して叩き、帰ってきたレスポンスのバイナリ。端的に言うとプロトコルエラーで帰れと言われているが、理由は後述する。
解決した方法
+listen [::]:80;
-listen [::]:80 http2;
後段のmstdn.lycolia.infoの設定を上記に変更することで、他のドメインでも問題が解決した。
何故解決したかはわからないが、恐らく80 http2と書くと、他の80番ポートのリッスンにも影響が波及するのだと思う。
http2通信にしなくても問題ないのかでいうと、Mastodonは問題なさそうに見えたのでたぶん大丈夫なんだと思う。知らんけど。
何故解決したのか
使用しているnginxのバージョンがproxy_passするときにhttp 2.0をサポートしてなかったからだ。
nginx 1.29.4になるとproxy_passでhttp 2.0がサポートされるようで、2025年12月9日のリリースには以下の一文があった。
*) Feature: the ngx_http_proxy_module supports HTTP/2.
この時ばかりは今までnginxのバージョンに興味が微塵になかった私もバージョンを上げたくなった瞬間だった。
ただ後続のバージョンのバグフィックスを見るに結構バグが出てそうなので、今あげるのは時期尚早かもしれない。まぁ現状困ってないので上げなくてもいいのは幸いだ。
ところでv1.25.1になるとlisten単位のhttp2が非推奨になり、代わりにhttp2 onという全体単位?が推奨設定になるため、将来的に現状のポート単位にhttpバージョンを分ける技は使えなくなりそうである。
あとがき
バイナリ読解
前述したHTTP 2.0のバイナリレスポンスを読んでみようのコーナー。
curl -s --http0.9 'http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]/' | xxd
00000000: 0000 1204 0000 0000 0000 0300 0000 8000 ................
00000010: 0400 0100 0000 0500 ffff ff00 0004 0800 ................
00000020: 0000 0000 7fff 0000 0000 0807 0000 0000 ................
00000030: 0000 0000 0000 0000 01 .........
そのバイナリを見るといいという情報をやってみたので試してみた結果。
まず16進数なので1桁に0-Fが入る。これは0000~1111ということだ。つまり1桁が4bitであることが分かる。
さて、HTTP/0.9は一旦忘れて、先ほどやってきたHTTP/2.0として解釈してRFC 9113 - HTTP/2を読んでみる。
「4. HTTPフレーム」によればHTTPフレームの先頭24bitがLength、8bitがType、8bitがFlag、1bitがReserved、31bitがStream Identifierらしい。
つまり先頭を分解するとこうなる。
Length: 00 00 12
Type: 04
Flag: 00
Reserved+Stream Identifier: 00 00 00 00
Lengthだ。0x000012なので18個の8bitフィールドがある。
次の8bitがTypeで0x04となっている。0x04は6.5.1. 設定フォーマットだ。
Reserved+Streamは全て0なので無価値だろう。
設定フォーマットではそこから48bit単位が設定フィールドとなる。先頭16bitがIdentifier、後ろ32bitがValueだ。
18個の8bitフィールドが設定になるため18 * 8 = 144, 144 / 48 = 3で3つの設定が存在すると読める。
00 03 00 00 00 80
つまりこの部分になる。
Identifierは00 03なのでSETTINGS_MAX_CONCURRENT_STREAMS。最大同時ストリーム数だ。
Valueは00 00 00 80なので128。
次は00 00 00 00 10なのでSETTINGS_HEADER_TABLE_SIZE。ヘッダ圧縮テーブルの最大サイズ。
04 00 01 00なので67,109,120。オクテット単位なので単位はバイトと思われる。オクテットのオクはオクトパスのOctと同じなのでタコの脚は八本と考えると覚えやすい。Octoberも旧暦の8月だからOct。
次は00 00 05なので、SETTINGS_MAX_FRAME_SIZE。
00 ff ff ff 00なので4,294,967,040。これも単位なので単位はバイトと思われる。
これで設定が終わり、またフレームに戻る。
00 0004 0800
0000 0000 7fff 0000 0000 0807 0000 0000
0000 0000 0000 0000 01
これが残り。
Length: 00 00 04 → 4 * 8 = 32bit
Type: 08 → WINDOW_UPDATE
Unused Flags: 00
Reserved+Stream: 00 00 00 00
Reserved+Window Size Increment: 7f ff 00 00 → 2,147,418,112
次の残り
0000 0807 0000 0000
0000 0000 0000 0000 01
Length: 00 00 08 → 64bit
Type: 07 → 帰れ
ここまでくれば尻だけ読めばいいので中間は読み飛ばす。
最後の32bitがエラーコードらしいので00 00 00 01が恐らくエラーコード。意味合い的にはPROTOCOL_ERROR。
要するにHTTP 2に対してHTTP 1.1で接続していたのでプロトコルエラーが返却され、しかしnginxは理解できなかったのでHTTP 0.9として解釈していたのだろう。
今回組んだ構成
今回nginxからnginxにリバースプロキシをしたわけだが、結果として上図のような構成にした。前段にはConoHa VPSを利用している。
これをした理由としては、以前フレッツ光クロスでフルポート使えるIPv4が取れないことが判明したため、クロスに移行出来るようにするためというのと、IPv4のためにDDNSし続けるのが地味に面倒なのと、宅内からIPv4を排除したかったところによる。
ただこれはこれでVPS側にもTLS証明書が必要になるため、DDNSが消えてもIPv4保守のための呪いはまだ残る。DDNSよりはマシだが…。
また、この構成では前後でnginxのバージョンが異なり前段がv1.26.3、後段がv1.24.0となっているため、前段ではhttp2 onにしないと警告が出るなど、バージョン差異による違いが微妙にある。
結果的にできた構成
今回の施策によって自宅サーバーからIPv4が失われても、AレコードをConoHa VPSに向けることでIPv4を受けることが可能な体制の検証をすることができた。これによってフレッツ光クロスに乗り換えたときのIPv4不在問題を解消できる状態となった。
実運用に移すためにはVPS側にもTLS証明が必要になるので自宅サーバーからSCPで送るか、VPS側でも取得する必要が出てくるので、まだまだ対策が必要だ。後者は同一ドメインに二重に証明書が存在する状態になるのでなんか嫌だし、恐らく前者でやるだろう。TCP パススルーなる技術も健闘したが、真のクライアントIP転送問題など、いろいろ厄介そうなのでやめた。
SCPで送る場合はパスフレーズ付きの鍵認証を突破させ、更にnginxをリロードさせないといけないからこちらもやや頭が痛い。パスフレーズ付きのSSH鍵を無人突破させるにはどこかに平文でパスフレーズを持つ必要がある。暗号化もできるがどんどん複雑に…。
或いはDNS-PERSIST-01を使うことで証明書更新を不要にするのも一つの選択肢かもしれないので、こちらも検討していいかもしれない。
しかしVPSがあることで監視対象が増えるなど、なかなか複雑化してしまうので、これを軌道に乗せるかどうかは現時点では不透明だ。ただConoHa VPSは最低スペックで36ヶ月契約であれば月額293円のため、36ヶ月換算でも10,548円にしかならず、IPv4を保持する手段としてはお手頃である。安いISPでもv4を買おうとすると月2~3,000円はするし、まともなところだと一万円以上するので、IPv4にサーバーがついてこの価格は破格と言える。
帯域は100Mbpsだが、100Mbpsあれば十分だろう。
おまけConoHa VPSで帯域実測
自宅サーバーとConoHa VPS間でiperf3をやってみた結果。自宅サーバーへの穴あけが面倒だったので自宅サーバー→ConoHaという逆経路でしか試していない。
| Interval | Transfer | Bitrate | Retr | Cwnd |
|---|---|---|---|---|
| 0.00-1.00 sec | 24.4 MBytes | 204 Mbits/sec | 0 | 1.42 MBytes |
| 1.00-2.00 sec | 10.9 MBytes | 91.2 Mbits/sec | 0 | 1.43 MBytes |
| 2.00-3.00 sec | 12.2 MBytes | 103 Mbits/sec | 0 | 1.43 MBytes |
| 3.00-4.00 sec | 11.0 MBytes | 92.3 Mbits/sec | 0 | 1.43 MBytes |
| 4.00-5.00 sec | 10.9 MBytes | 91.2 Mbits/sec | 0 | 1.43 MBytes |
| 5.00-6.00 sec | 12.2 MBytes | 103 Mbits/sec | 0 | 1.43 MBytes |
| 6.00-7.00 sec | 11.0 MBytes | 92.3 Mbits/sec | 11 | 1.26 MBytes |
| 7.00-8.00 sec | 11.0 MBytes | 92.3 Mbits/sec | 169 | 762 KBytes |
| 8.00-9.00 sec | 11.0 MBytes | 92.3 Mbits/sec | 0 | 807 KBytes |
| 9.00-10.00 sec | 12.2 MBytes | 103 Mbits/sec | 0 | 838 KBytes |
- sender: 106 Mbits/sec
- receiver: 103 Mbits/sec
2026/06/01(月)フレッツ光クロスでフルポート使えるIPv4が取れるか調べた
投稿日:
フレッツ光クロスとフレッツ光ネクスト隼のコスト比較をしてみたの続きのような記事。
結論から言うと、現実的は難しい。金があれば取れる。
フレッツ光クロス x OCNでの対応状況
取り敢えず私はOCNユーザーなので、まずはOCNの対応状況から調べてみることにした。
OCN IPv6インターネット接続|光回線 | OCNお客さまサポートによると次のようにあり、使えないことが分かった。
(OCN 光 with フレッツ クロス / OCN インターネット 10ギガはPPPoE接続できません。)
またOCNサポートに問い合わせたところ、OCNインターネット10ギガはIPoEのみ対応で、方式はMAP-E、OCNバーチャルコネクトとのことだった。そしてPPPoEは使えず、方式上フルポート使えるIPv4の提供はないと言う話だった。
兵庫県下におけるフレッツ光クロスで従来型IPv4の提供があるISPについて
フレッツ光 対応プロバイダー検索|NTT西日本公式に一覧があるが、対応しているのはIIJのみ。
IIJ IPv6 FiberAccess/F サービス タイプIPoEであればIPv4アドレスの固定割り当てが取れるそうだが、月額14,000円~ということで、到底支払える値段ではない。
IIJのフレッツ光ネクストはIPv6 IPoE、動的IPv4アドレスの場合は2,200円とのことなので、恐らくフレッツ光クロスにするとIPv4が高くなるのだと思う。
あとがき
今回の調査では、前段に外部のリバースプロキシやそれに類するものを置かず自宅サーバーを運用する場合にIPv4を切る必要が出てくることが判明した。
前回の調査ではフレッツ光クロスにすると年3万円コスト増になることが分かっているため、これと合わせるとクロスに移行する場合、年3万の負担増とIPv4でのサーバーサービスを終了しないとならないことが分かった。
中々悩ましい結果だが、回線が逼迫しているわけではないので、OpenWrtのメトリクスなどをもっとちゃんととって、必要が出たあたりで移行できたらいいなとは思う。今でも偶に不審な速度になることはあるので、それの裏が取れたら移行してもいいだろうとは思った。
2026/05/28(木)Value-DomainのDNSを操作するユーティリティを更新した
投稿日:
value-domain-dns-utilを更新したので、その記録とか、その感想の怪文章とか。
更新内容
ワイルドカード証明を発行するときに失敗する問題の修正
これまでDNS更新のタイミングの関係で偶々成功していたが、何度もやってると失敗することが多かったので、失敗しないようにした。
変更前
vd-dcr.pl- APIからDNSレコードを取得→レコードにACME TXTレコードがなければ追加、あれば上書き→編集後のレコードをAPIに送る
変更後
vd-dcr-auth.pl- APIからDNSレコードを取得→レコードにACME TXTレコードを追加
vd-dcr-cleanup.pl- cleanup:PIからDNSレコードを取得→レコードから今回登録したACMEレコードを除去→編集後のレコードをAPIに送る
モジュール化
lib/VdDnsUtil.plをlib/VdDnsUtil.pmに変更し、shebangを削除。
並びに今回の変更で追加したユーティリティも.pmとして実装。
バージョニングの追加
スクリプトを叩いたときに依存モジュールと本体のバージョンが出るようにした。
対処が必要なこと
vd-dcr.plの改修に関わる影響のため、vd-dcr.plを使っている人だけに影響がある。
Net::DNS::Digの追加
rootユーザーで以下を流し、Net::DNS::Digを入れる。既にある場合は不要。
cpan Net::DNS::Dig
Certbotの走行コマンドの変更
certbotを以下の書式で再走行させる。具体的には--manual-cleanup-hookが増えてるので、そこを足す。これによってrenewが上書きされ、次回以降、正しく動作するようになる。
sudo certbot certonly --manual -n \
--preferred-challenges dns \
--agree-tos -m <your-email> \
--manual-auth-hook "/path/to/vd-dcr-auth.pl <value-domain-api-key> <root-domain> <optional:ttl>" \
+ --manual-cleanup-hook "/path/to/vd-dcr-cleanup.pl <value-domain-api-key> <root-domain> <optional:ttl>" \
-d <FQDN>
また、従来であればValue-DomainのAPIに更新リクエストを投げてから3秒しか待っていなかったところ、今回はDNSの伝播を待ち、更に5秒待つように直しているため、以前より実行時間が伸びているため、もし何かしらのタイミング処理がある場合は見直しが必要。
今回の対応をした理由
切欠としてはモジュール化の作業中に不具合が発覚したところに始まる。
これまで共通部品として.plを使っていたが、Perlの再利用性について考えていた時に.pmの概念を知ったので改修することにした。その動作確認をしていたところ、ワイルドカードドメインの更新がやたらと失敗することに気付き、本格的に調査を始めたのが発端だ。
そして以前は次の順序で処理していた。
- 初回走行時にルートドメインのTXTレコードを追加する
- 二回目走行時にこのレコードをワイルドカード用のTXTレコードで上書きする
しかし、本来これは正しくなかった。ワイルドカードドメインを指定する場合、プライマリとセカンダリのTXTレコードが両方とも登録されている必要があるようだった。
ではなぜ今まで動いていたのか。これはDNS更新のタイミングの関係で偶々成功していただけと思われる。そして、私がそのことに気付いていなかったわけだ。動作確認で何度も実行しているとやけに失敗するので、今回この不具合に対応することに決めた。
そして前述の更新内容に繋がるわけだが、これまでは--manual-auth-hook向けのスクリプトしか用意しておらず、その中で「既存ACMEレコードがあれば上書きする」という擬似的なクリーンアップ処理を行っていた。これが前述の不具合を引き起こす要因となっていたため、次の改修を施した。
- auth側からクリーンアップ処理(既存ACMEレコードの上書き)を除去し、純粋に「TXTレコードを追加する」だけの処理に変更
- 新たに
--manual-cleanup-hook用のスクリプトを追加し、authで登録したレコードをDNSから削除する処理を、こちらに新設した
これにより、プライマリ・セカンダリ両方のTXTレコードが正しく揃った状態でLet's Encryptの検証に渡され、検証完了後にきちんと後始末されるようになった。
参考までにコケていたときは次のエラーが出ていた。
Certbot failed to authenticate some domains (authenticator: manual). The Certificate Authority reported these problems:
Domain: hogezzz.lycolia.info
Type: dns
Detail: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.hogezzz.lycolia.info - check that a DNS record exists for this domainDomain: hogezzz.lycolia.info
Type: unauthorized
Detail: During secondary validation: Incorrect TXT record "tiHFPEdgZ24aejUpmRkkzNvzUoisQn6mDv3d3xqHBNo" found at _acme-challenge.hogezzz.lycolia.info
あとがき
二日で15コミット、変更行数は1,397行追加、217行削除という結構大規模な改修を入れたので、どこかにバグが潜んでいる可能性は捨てきれない。
何せ以前開発していたvalue-domain-dns-cert-registerから、このvalue-domain-dns-utilに移行するときにLLMにガっとやらせてから、今回に至るまでコードのメンテはほとんどLLMに任せている。
当然LLMが書いたコードはレビューして明らかに変なところは手直ししているが、精々コード全体の3-4割で、過半数はLLMが書き、LLMが直せる場所は私が直さずLLMに直させている。テストコードに至っては99%くらいLLMに書かせており、正直正しいコードが掛かれているのかまで見切れていない。以前なら血眼になってテストコードを書いていたはずなのに不思議なものである。
まぁでも実コードを手直ししたら落ちたので、たぶん機能していると思う。あとvalue-domain-dns-cert-registerから継承されたテストコードについては一通り見ているので、全く見ていないわけでもない。
LLMにコードを書かせると作るのは楽だがレビューが大変なのが困りものだ。あと私はAnthropicをBANされている関係で地味にお金がかかるのも困りものである。今回の改修には20USDほどかかっており、安くはない。もう少し手でコードを書くべきだとは思う。考える力も徐々に薄れていっている気がしており、人間として危機感を感じることもある。
ボケ防止のためにはAIに頼らず、自分の手を動かすのも大切なことだと思う。こういう話では、よくそろばん論を持ち出して、そろばんを使わなくなったが人類は劣化していない、電卓があるみたいなことを言う人がいるが、実際問題珠算ができる人は非常に速い速度で暗算ができ、頭の回転も速いとされているので、電卓があるから優位と言われれば、それは違うと思う。例えば電卓を取り出す時間で計算が終わっていたり、物事を予測しやすかったりするわけだ。
ある能力が減った分、別の能力が増えるみたいな考えもあるとは思うが、一人の人間としてAIにべったり過ぎるのもやや危険ではないかと思う。第一災害時どうするんだと思うし、登山などでは電波の届かない山の中ではそもそも使えないとかもあるし、ボケるボケないも寿命に影響するみたいな話は聞くので、考える力は大切だと思う。私は管理職とか企画職みたいなところよりは、現場人間でありたい気もしているし…。
なんだかオチが迷走してきたが、とりあえずvalue-domain-dns-utilはワイルドカード証明書も含めて安定して動くようになったはずなので、必要に応じて更新してもらえれば安定性が増すかもしれない。もしバグを踏んだら報告してもらえれば対応するかもしれません。
しかしこのスクリプトは動作確認が非常に面倒なので、できればもう手を入れたくないところではある…。
2026/02/26(木)Value-DomainでcertbotのDNS-01 challengeをやるスクリプトを書き直した中で知ったこと
投稿日:
前の記事で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)、つまり自動証明書管理環境とされているようだ。有志による日本語訳も存在し、日本語で読むこともできる。
CRONTABは書かなくていい
以下のコマンドで証明書を発行した場合、CRONTABにもsudoを抜いた同じコマンドを書く必要がありそうだが、実はしなくていい。
sudo certbot certonly --manual -n \
--preferred-challenges dns \
--agree-tos -m postmaster@example.com \
--manual-auth-hook "/path/to/vd-dcr.pl XXXXX example.com" \
-d hoge.example.com
何故かというとタイマーとサービスが/usr/lib/systemd/system/certbot.timerと/usr/lib/systemd/system/certbot.serviceとして登録されており、私の環境では毎日0時と12時に実行されるようになっていた。
サービスの生死は以下で確認できる。
systemctl status certbot.timer
systemctl status certbot.service
このサービスが何をしているかというと/etc/letsencrypt/renewal/*.confの設定ファイルをフェッチして証明書の更新をしてくれるようになっている。
つまりCRONTABを書く必要は、基本的にないのである。
なお/etc/letsencrypt/renewal/*.confの設定ファイルは証明書の発行コマンドを叩く度に増えるが、削除したい場合は、以下のコマンドで証明書など、諸々の関連ファイルごと削除できる。
# 例:sudo certbot delete --cert-name hoge.example.com
sudo certbot delete --cert-name <target-domain>
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アカウント鍵に有効期限を設けられるようだが、それだと本末転倒にも思う。勿論、そういうケースが役に立つ場合もあるだろうが、運用は大変そうだ。
2026/02/22(日)OpenWrtからValue-Domainに複数サブドメインのDDNSを行うツールを作った
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が変わったのに気づかないまま疎通できないインスタンスが出てくることがしばしばあり、手動で対応するのが手間なのと、毎回一日くらい気づくのに遅れるので今回自動化に踏み切った。前々からやり勝ったのだが、中々腰が重く進んでいなかった。
そもそもこの手のものは監視システムで検知できて然るべきなので、おいおい監視システムの構築もしていきたいところだ。



