投稿日:
前の記事で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が変わったのに気づかないまま疎通できないインスタンスが出てくることがしばしばあり、手動で対応するのが手間なのと、毎回一日くらい気づくのに遅れるので今回自動化に踏み切った。前々からやり勝ったのだが、中々腰が重く進んでいなかった。
そもそもこの手のものは監視システムで検知できて然るべきなので、おいおい監視システムの構築もしていきたいところだ。
日本国内からのアクセスを識別するときに、IPv4の頃はリモートホスト名が/\.jp$/であるかや、nifty.comみたいな例外を個別で見てやる程度で何とかなっていたが、IPv6は逆引きができないので、それをどうにか解決する方法。
確認環境
| Env | Ver |
|---|---|
| PHP | 8.3.8 |
| geoip2/geoip2 | 3.2.0 |
やり方
- ComposerでGeoIp2を入れる
php composer.phar require geoip2/geoip2 - dbipからIP to City Lite databaseのMMDBを落としてくる。
https://download.db-ip.com/free/dbip-city-lite-2025-11.mmdb.gzのURL形式なのでYYYYとMMのとこをいじっても落とせる。 こんな感じにコードを書く
<?php require 'vendor/autoload.php'; use GeoIp2\Database\Reader; // 落としてきたMMDBを指定 $cityDbReader = new Reader('dbip-city-lite-2025-11.mmdb'); // IPを渡すと結果が取れる $record = $cityDbReader->city('2403:6200:8860:e103:a4b2:ff66:9e68:6436'); print($record->country->isoCode . "\n"); // 'US' print($record->country->name . "\n"); // 'United States' print($record->country->names['zh-CN'] . "\n"); // '美国' print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota' print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN' print($record->city->name . "\n"); // 'Minneapolis' print($record->postal->code . "\n"); // '55455' print($record->location->latitude . "\n"); // 44.9733 print($record->location->longitude . "\n"); // -93.2323 print($record->traits->network . "\n"); // '128.101.101.101/32'
参考
- GeoIP extension Introduction - www.php.net
- GeoIP2 PHP API - maxmind.github.io
あとがき
国情報を利用してアクセスを弾く必要があったので雑に調べて導入したが、dbipの無料DBは正確性が低いので、個人の小規模利用向けだと思う。
エンタープライズ用途などで制度が必要な場合は有料DBの契約を検討したほうがいいだろう。
しかしGeoIp2はPHPのマニュアルにあるからPHPに組み込みで存在してるのかと思ったら、まさかのComposerで驚いた。PHPも変わったなぁ…とかしみじみ。
でも昔からPearはあったような気もするし、今更か…?
Google Analyticsを廃止してMatomoを導入してから集めていたIPv6のアクセス比率を集計した結果をメモしておく。
集計期間は2025/08/17~2025/10/21。
| サイト | IPv6ビジット | IPv4ビジット |
|---|---|---|
| lycolia.info | 56 (55.0%) | 77 (45.0%) |
| ブログ (blog.lycolia.info) | 2,018 (43.4%) | 2,644 (56.7%) |
| ECO-Wiki (eco.lycolia.info/wiki) | 7,946 (40.5%) | 11,291 (59.4%) |
| ツールサイト (tool.lycolia.info) | 13 (65.0%) | 7 (35.0%) |
| 合計 | 10,033 (41.7%) | 14,019 (58.2%) |
以上の結果からIPv6は約四割、v4は約六割ということが分かった。また最もアクセスがあるのはECO-Wikiで、ツールサイトにはほぼアクセスがないことも明確になった。まぁツールは私が使えればいいので仕方ないね。
lycolia.infoは突出してIPv6のアクセスが多いが、実数が少なすぎるので余りアテにならない。ただ恐らく、リンク元の特性的に、ITオタク周りのアクセスが多いことが想定されるため、この結果はある程度妥当かもしれない。ツールサイトもそういう人が興味を持って踏んでるのだとしたら納得だ。ブログも技術記事がある関係で比率が高い可能性がある。
逆にECO-Wikiは比較的一般向けのサイトなので比率が少なくなっていると思った。
あとがき
まだまだIPv4の環境は多く、IPv6シングルスタックへの移行はやや戸惑うレベルだ。GitHubのWebhookもIPv4だったりするので、しばらくはデュアルスタックで運用していくのが無難だろう。
現状さくらのレンタルサーバー上にある資源を自宅サーバーにフル移行する場合、DNS側の制約でマルチドメインのDDNSが面倒な問題があるため、専用のスクリプトを作る必要が出てきそうだ。
これは契約しているDNSサーバーのDDNSが1回に1レコードしか更新できず、レートリミットが60秒に1回という面倒な仕様なため、ドメインが複数あると厄介なのだ。現状Mastodonだけが自宅サーバーにある状態なのでどうにかなっているが、どうにかして対処しないといけない。
契約しているDNSサーバーにはDDNS API以外にも、DNSレコードを全部書き換えられるAPIがあり、こちらは1回に複数レコードを触れるため、比較的レートリミットを気にする必要がなくなる。なので、これを使って自力で書き換えるスクリプトを組めばDDNS APIのレートリミットは無視できるようになる。まぁ、こっちのAPIはレートリミットが緩いので何度でも叩けるのだが、バルク編集できるAPIを何度も叩く意味もないので…。
これとは関係なく、将来的には自宅サーバー側に権威DNSを立てて、レジストラのDNSサーバー側にはNSレコードだけというのもやってみたさがある。この場合、ドメインの追加や削除で一々レジストラの管理画面にアクセスする頻度が減らせ、管理が楽になるメリットがありそうだし、レジストラのDNSでは実現できないことが出来るようになる可能性もあるだろうから面白いかもしれない。
投稿日:
nginxでhttps://example.comを受けて、https非対応のApache2にリバプロし、Apache2側でもhttps://example.comでアクセスされているように振舞わせる方法。取り敢えず最低限こんだけあると無難だろうという内容。
確認環境
| Env | Ver |
|---|---|
| OS | Ubuntu 24.04.3 LTS |
| nginx | 1.26.1 |
| Apache2 | 2.4.58 |
Apache2(後段)の設定
/etc/apache2/ports.conf
80や443を開けるとnginxと衝突するため、適当にずらしておく。
Listen 8080
/etc/apache2/sites-enabled/001-example.conf
ServerNameは外側と同じものにする。
<VirtualHost *:8080>
ServerName example.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/example-error.log
CustomLog ${APACHE_LOG_DIR}/example-access.log combined
</VirtualHost>
nginx(前段)の設定
/etc/nginx/nginx.conf
httpセクションにupstream apacheを追記することで、conf.d/配下の設定ファイルから共通的に参照できるようにする。同じことを複数の設定ファイルに書くと構文エラーで起動しなくなる。
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
upstream apache {
server [::1]:8080;
}
include /etc/nginx/conf.d/*.conf;
}
/etc/nginx/conf.d/example.conf
ここでは/etc/nginx/nginx.confで設定したupstreamにプロキシする設定を作っている。
proxy_set_header Host $host;はnginxが受けたホストヘッダをそのままApacheに飛ばすことで、Apache側がバーチャルホストでルーティングできるようにしている。Apacheとnginxで別々のホスト名にすることもできるが、そうした場合、Apache側はApache側で定義したホスト名で動作するため、CGIなどのスクリプトを動かすときに支障があるうえ、Apache側のホスト名をhostsに書かないと疎通できないこともあり、手間なので基本的に前段のドメインに合わせておくのが無難だ。
proxy_set_header X-Real-IP $remote_addr;はnginxにアクセスしてきたクライアントのIPをApacheに渡すための設定。そのままだとnginxのIPが渡ってしまう。
proxy_set_header X-Forwarded-Proto $scheme;もホストヘッダの転送と似ていて、nginxが受けたプロトコルスキーマをApacheに飛ばすための設定。こうしておくとApache側はhttpで受けているのに、httpsでアクセスされたものとして認識させることが出来るため、プロトコルスキーマ付きでURLを生成するCGIがいるときに都合がいい。単にスキーマを誤魔化しているだけなので実際は暗号化されていない。
server {
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log;
location / {
proxy_pass http://apache/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
余談:HTTP通信に用いられるHOSTヘッダーについて
HTTP通信で利用されるドメインは単にHOSTヘッダーにドメインを書いているだけなので、以下のようなことをするとhostsを書かなくとも疎通できる。
curl -H "HOST: lycolia.info" http://"[2403:3a00:101:13:133:167:8:98]"
これは一般的なHTTPクライアントの仕組みとして、まずドメインをDNSに問い合わせ、IPを取得したのち、ドメインをホストヘッダーに載せてIPアドレス宛にHTTP要求を出しているからだと思われる。
hostsの書き換えができない環境で、任意のドメインに対してHTTP要求を出したい時にも重宝するので、覚えておくと何かと役に立つ。
