TechNow!
ITテクノロジー関連の記事を中心に発信しています
CertbotとPorkbun APIの安全な設定

Porkbunでワイルドカード証明書を自動更新する方法

By Author
Porkbunでワイルドカード証明書を自動更新する方法
`*.example.com` などのワイルドカード証明書は Let's Encrypt で無料で発行できます。ドメインを購入したレジストラにかかわらず、DNS を Cloudflare へ委任して、Certbot から DNS-01 認証を自動化し、ワイルドカード証明書を自動的に更新し続ける方法について解説します。

ここではAlmaLinuxへCertbotとCloudflare用プラグインを導入し、DNS APIトークンを保護したうえで、更新後にWebサーバを再読み込みするところまで設定します。ドメイン登録をPorkbun、Cloudflare Registrar、お名前.com、ムームードメインなどのどこで行っていても、DNSをCloudflareで管理している構成なら手順の考え方は同じです。

ワイルドカード証明書ではDNS-01認証を使う

Let's Encryptがドメインの管理権限を確認する方法には、Webサーバへ検証用ファイルを置くHTTP-01と、DNSへ検証用TXTレコードを追加するDNS-01があります。ワイルドカード証明書の発行にはDNS-01が必要であり、_acme-challenge.example.comへ一時的なTXTレコードを登録しなければなりません。認証の詳しい仕組みは、Let's Encryptのチャレンジ方式の解説でも確認できます。

TXTレコードをCloudflareの画面から手動で追加することもできますが、更新のたびに同じ作業を繰り返すことになります。証明書の有効期限が近づくたびにログインし、Certbotが示した値を登録してDNS反映を待つ運用では、担当者が不在のときに更新を逃しかねません。Cloudflare DNS APIをCertbotから呼び出し、認証用TXTレコードの作成と削除を自動で処理する構成にします。

なお、*.example.comが保護するのはwww.example.comやapi.example.comのような一階層のサブドメインです。ドメイン直下のexample.comや、dev.api.example.comのように二階層深い名前までは対象になりません。たとえばトップページをexample.comで公開する場合は、証明書要求にexample.comと*.example.comを併記します。

DNS-01認証では、AlmaLinuxサーバの80番ポートを外部へ公開する必要がありません。社内ネットワーク内のサーバや、ロードバランサーの配下にあるWebサーバでも、Cloudflare APIへのHTTPS通信とDNS名前解決が可能なら発行できます。HTTP公開前の検証環境へ、あらかじめ本番用の証明書設定を用意したい場面でも使いやすい方式です。

作業前に、対象ドメインの権威DNSがCloudflareになっているかを確認します。ドメインをどのレジストラで取得したかではなく、レジストラ側でCloudflareから指定されたネームサーバーを設定済みかどうかが認証の成否を左右します。次の例では、返されたネームサーバーからDNS管理先を調べます。

dig +short NS example.com    

Cloudflareでゾーンを作成しただけでは、DNS委任が完了していない場合があります。digの結果がCloudflareのネームサーバーではなく、レジストラや別のDNS事業者を指しているなら、CloudflareへTXTレコードを追加してもLet's Encryptは確認できません。レジストラのネームサーバー設定画面と、Cloudflareダッシュボードのゾーン状態を照合してください[要確認]。

CAAレコードを設定している場合は、Let's Encryptによる証明書発行が許可されているかも確認します。CAAが存在しなければ、通常は特定の認証局だけに発行元を制限していない状態です。CAAを使っているドメインでは、letsencrypt.orgを許可する必要があるため、既存のDNSポリシーを管理者と確認します。

dig +short CAA example.com    

CloudflareのAPIトークンを安全に用意する

Cloudflareへログインし、プロフィール画面のAPI Tokensから証明書更新専用のAPIトークンを作成します。権限は対象ゾーンのDNS編集を許可するものに絞り、可能であればゾーンリソースもexample.comだけへ限定します。必要な権限や画面の名称は変わる可能性があるため、CloudflareのAPIトークンに関する案内とCertbotのCloudflareプラグイン情報を作成前に確認してください[要確認]。

Certbotが必要とするのは、認証用TXTレコードを追加・削除できるAPIトークンです。トークンが漏えいすると、攻撃者にDNSレコードを改ざんされ、Webサイトの転送先やメール配送先を変えられるおそれがあります。Cloudflare APIトークンは、証明書の秘密鍵と同じようにroot以外が読めないファイルへ保存します。

ここでは認証情報を/etc/letsencrypt/cloudflare.iniへ保存します。トークンをコマンドライン引数へ渡すと、シェル履歴やプロセス一覧に残る可能性があるため避けてください。次の項目名はcertbot-dns-cloudflareの仕様に基づくものですが、導入するパッケージのドキュメントも確認します[要確認]。

sudo install -d -m 700 -o root -g root /etc/letsencrypt    
    
sudo tee /etc/letsencrypt/cloudflare.ini > /dev/null <<'EOF'    
dns_cloudflare_api_token = ここをCloudflareのAPIトークンへ置き換える    
EOF    
    
sudo chown root:root /etc/letsencrypt/cloudflare.ini    
sudo chmod 600 /etc/letsencrypt/cloudflare.ini    

権限はstatで確かめられます。所有者がrootであり、root以外に読み取り権限が付いていない状態が必要です。たとえば-rw-------と表示されれば、パーミッションは600です。

sudo stat -c '%A %a %U:%G %n' /etc/letsencrypt/cloudflare.ini    

APIトークンをGitリポジトリへ登録したり、構成管理ツールの平文変数として保存したりしてはいけません。バックアップへ含める場合も、保管先で暗号化し、復元可能な管理者を必要最小限にします。私なら本番用と検証用でトークンを分け、不要になったトークンはCloudflare側でただちに失効させます。

CloudflareのグローバルAPIキーを使う方法もありますが、アカウント全体へ強い権限を与えることになりがちです[要確認]。証明書の更新だけが目的なら、対象ゾーンのDNS編集に限定したAPIトークンを作るほうが被害範囲を狭められます。サーバへログインできる利用者、バックアップの保存場所、監視ツールが収集するファイルも、トークンを置く前に確認しておきたいところです。

AlmaLinuxへCertbotとCloudflareプラグインを導入する

Cloudflare用のDNSプラグインは、Certbot本体とは別のPythonパッケージとして提供されています。AlmaLinuxのRPM版Certbotと、pipで導入したプラグインを異なるPython環境へ入れると、Certbotからプラグインが認識されないことがあります。そのため、専用のPython仮想環境へCertbotとCloudflareプラグインをまとめて導入します。

以下は主にAlmaLinux 9系を想定した例です。利用中のAlmaLinuxとPythonのバージョンが、最新版のCertbotおよびcertbot-dns-cloudflareの要件を満たすかは、導入時にCertbotのドキュメントとPyPIのパッケージ情報で確認してください[要確認]。AlmaLinux 8系などで標準Pythonのバージョンが足りない場合は、OSが提供する新しいPythonパッケージを選ぶ必要があります。

最初にPythonと仮想環境の作成に必要なパッケージを導入します。続いて/opt/certbotへ仮想環境を作成し、その中のpipでCertbotとCloudflareプラグインをインストールします。OSが管理するPython環境へsudo pip installを直接実行すると、RPMパッケージとの衝突につながるため採用しません。

sudo dnf install -y python3 python3-pip    
    
sudo python3 -m venv /opt/certbot    
sudo /opt/certbot/bin/python -m pip install --upgrade pip    
sudo /opt/certbot/bin/pip install certbot certbot-dns-cloudflare    

python3 -m venvが使えないときは、AlmaLinuxのバージョンに対応した仮想環境用パッケージが不足していないか調べます。必要なパッケージ名はPythonの版によって異なる場合があります[要確認]。別のPythonを導入した環境では、その実行ファイルを明示して仮想環境を作成してください。

python3 --version    
sudo dnf search python | grep -i venv    

毎回フルパスを入力しないようにするなら、仮想環境内のCertbotへシンボリックリンクを作成します。すでに/usr/local/bin/certbotが存在する場合は上書きせず、どのCertbotが動いているかを先に確認してください。RPM版やSnap版が入っている環境では、更新ジョブの二重起動を防ぐため、一つの導入方法へ統一します。

command -v certbot || true    
sudo ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot    
certbot --version    

インストール後は、CertbotがCloudflareプラグインを検出できているかを確認します。一覧にdns-cloudflareが表示されなければ、証明書発行の前にPython環境の混在を解消します。たとえばwhich certbotが/usr/bin/certbotを示しているのに、プラグインを/opt/certbotへ入れた場合は、別環境のCertbotを実行しています。

/opt/certbot/bin/certbot plugins    

プラグインはroot権限で実行される第三者コードです。公開元、更新履歴、依存パッケージを確認し、いきなり本番サーバへ最新版を導入するのではなく、検証環境で発行試験を行います。再現性を優先するなら、動作確認済みのバージョンを固定し、更新後に改めてrenew --dry-runを実行する運用が適しています。

証明書を試験発行してWebサーバへ設定する

設定ミスを直すために本番の認証局へ何度も要求すると、Let's Encryptの発行制限に達する可能性があります。最初は--dry-runを指定し、ステージング環境でCloudflareへのTXTレコード追加からDNS検証まで通るかを確認します。メールアドレス、ドメイン名、Cloudflare認証情報ファイルのパスは実際の値へ置き換えてください。

sudo /opt/certbot/bin/certbot certonly \    
  --dry-run \    
  --non-interactive \    
  --agree-tos \    
  --email admin@example.com \    
  --authenticator dns-cloudflare \    
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \    
  --dns-cloudflare-propagation-seconds 120 \    
  --preferred-challenges dns \    
  --cert-name example.com \    
  -d example.com \    
  -d '*.example.com'    

--dns-cloudflare-propagation-seconds 120は、CertbotがTXTレコードを作成してからLet's Encryptの検証を開始するまで待つ秒数です。120秒はあくまで出発点であり、DNS反映が間に合わない場合は長くします。複数回の試験で短時間に安定して反映されることを確認できたなら、更新時間を短縮する目的で調整できます。

シェルが*をファイル名として解釈しないよう、'*.example.com'は引用符で囲みます。たとえば作業ディレクトリ内にapi.example.comというファイルがあると、引用符のないワイルドカードが別の引数へ展開される場合があります。意図しない名前を証明書へ含めないためにも、引用符は省略しないでください。

プラグインのオプションが認識されない場合は、インストール済みバージョンのヘルプを確認します。認証情報ファイルの記述形式も、プラグインのREADMEやヘルプに記載された内容と一致させます。パッケージ更新により、本記事の例と差が生じる可能性があるためです[要確認]。

sudo /opt/certbot/bin/certbot --help dns-cloudflare    

API認証エラーが出た場合は、トークンの転記ミスだけでなく、Cloudflare側で対象ゾーンへのDNS編集権限を付与しているかを確認します。DNS検証がタイムアウトした場合は、レジストラ側のネームサーバー設定、Cloudflareのゾーン状態、TXTレコードの反映時間を調べます。認証中に別の端末から次のコマンドを繰り返すと、公開DNSで値が見えるまでの時間を確認できます。

dig +short TXT _acme-challenge.example.com    
dig @1.1.1.1 +short TXT _acme-challenge.example.com    
dig @8.8.8.8 +short TXT _acme-challenge.example.com    

試験が成功したら、同じコマンドから--dry-runだけを外して本番証明書を取得します。**example.comと*.example.comを同じ要求に含めれば、ドメイン直下と一階層のサブドメインを一組の証明書で扱えます**。利用予定のサブドメインを個別に-dで列挙する必要はありません。

sudo /opt/certbot/bin/certbot certonly \    
  --non-interactive \    
  --agree-tos \    
  --email admin@example.com \    
  --authenticator dns-cloudflare \    
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \    
  --dns-cloudflare-propagation-seconds 120 \    
  --preferred-challenges dns \    
  --cert-name example.com \    
  -d example.com \    
  -d '*.example.com'    

発行に成功すると、通常は/etc/letsencrypt/live/example.com/以下から証明書を参照できます。fullchain.pemにはサーバー証明書と中間証明書が含まれ、privkey.pemは外部へ出してはいけない秘密鍵です。これらは実体ファイルへのシンボリックリンクなので、Webサーバにはarchive配下の番号付きファイルではなく、live配下のパスを設定します。

sudo ls -l /etc/letsencrypt/live/example.com/    
sudo /opt/certbot/bin/certbot certificates    

Nginxでは、対象となるserverブロックへ次のように設定します。親プロセスをrootで動かす一般的なNginx構成なら、/etc/letsencrypt内の秘密鍵を直接参照できます。設定変更後は、構文検査が成功した場合だけ再読み込みしてください。

server {    
    listen 443 ssl;    
    server_name example.com *.example.com;    
    
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;    
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;    
    
    root /var/www/example.com;    
}    
sudo nginx -t    
sudo systemctl reload nginx    

Apache HTTP Serverを利用する場合は、SSL対応のVirtualHostへ証明書と秘密鍵のパスを指定します。設定ファイルの場所やモジュール構成は、AlmaLinuxに導入しているApacheの状態に合わせます。変更後はapachectl configtestで検査してから、httpdを再読み込みしてください。

<VirtualHost *:443>    
    ServerName example.com    
    ServerAlias *.example.com    
    
    SSLEngine on    
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem    
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem    
    
    DocumentRoot /var/www/example.com    
</VirtualHost>    
sudo apachectl configtest    
sudo systemctl reload httpd    

秘密鍵をアプリケーション専用ディレクトリへコピーする構成もありますが、コピー先のファイルはCertbotの更新だけでは差し替わりません。その場合は更新後のdeploy hookで、コピー、所有者変更、SELinuxコンテキストの復元、サービス再読み込みまで実行する必要があります。NginxやApacheがlive配下を直接読めるなら、コピーを作らないほうが更新経路を単純に保てます。

systemdで更新と再読み込みを自動化する

Certbotは初回発行時のドメイン名、認証方式、認証情報ファイルのパスなどを/etc/letsencrypt/renewal/へ保存します。そのため、通常の更新で発行時の長いコマンドを毎回入力する必要はありません。まずは保存済みの更新設定に、Cloudflareプラグインと認証情報ファイルのパスが記録されていることを確認します。

sudo sed -n '1,200p' /etc/letsencrypt/renewal/example.com.conf    

このファイルにはAPIトークンそのものではなく、認証情報ファイルへのパスだけが保存される構成が一般的です。更新設定はCertbotが管理するため、理由なく手編集すると次回の更新を壊すことがあります。ドメイン構成を変更する場合は発行コマンドを再実行し、その後にrenew --dry-runで確かめます。

次に、保存された設定だけで更新試験を通せるか確認します。初回発行に成功していても、認証情報ファイルを移動したり、Cloudflareプラグインを削除したりすると、自動更新の実行時に失敗します。**renew --dry-runが成功し、Webサーバの再読み込みまで完了する状態を運用開始の基準にします**。

Nginxを使う場合は、次のようにdeploy hookを付けて試験できます。deploy hookは証明書が実際に更新された後に実行されるため、期限が十分に残っていて更新不要と判断された場合は呼び出されません。--dry-run時のhook実行挙動はCertbotのバージョンによって差があり得るため、ログでも確認してください[要確認]。

sudo /opt/certbot/bin/certbot renew \    
  --dry-run \    
  --deploy-hook "/usr/bin/systemctl reload nginx"    

Apacheを使う場合は、hook内のサービス名をhttpdへ変更します。証明書を複数のサービスで利用するなら、構文検査と再読み込みをまとめたスクリプトを作成し、そのスクリプトをdeploy hookから呼び出すほうが管理しやすくなります。たとえばNginxの設定検査に失敗したとき、再読み込みを実行しない構成にできます。

sudo tee /usr/local/sbin/reload-after-certbot > /dev/null <<'EOF'    
#!/bin/bash    
set -euo pipefail    
    
/usr/sbin/nginx -t    
/usr/bin/systemctl reload nginx    
EOF    
    
sudo chown root:root /usr/local/sbin/reload-after-certbot    
sudo chmod 750 /usr/local/sbin/reload-after-certbot    
sudo /opt/certbot/bin/certbot renew \    
  --dry-run \    
  --deploy-hook /usr/local/sbin/reload-after-certbot    

仮想環境へCertbotを導入しただけでは、OS側に自動更新用のsystemd timerが作成されないことがあります。そこで、更新を実行するoneshotサービスと、1日に複数回起動するtimerを用意します。証明書の期限直前だけに処理を走らせるのではなく、定期的にCertbotへ更新の要否を判定させれば、一時的にCloudflare APIで障害が起きても次の実行で再試行できます。

sudo tee /etc/systemd/system/certbot-renew.service > /dev/null <<'EOF'    
[Unit]    
Description=Renew Let's Encrypt certificates with Certbot    
After=network-online.target    
Wants=network-online.target    
    
[Service]    
Type=oneshot    
ExecStart=/opt/certbot/bin/certbot renew --quiet --deploy-hook /usr/local/sbin/reload-after-certbot    
PrivateTmp=true    
EOF    
sudo tee /etc/systemd/system/certbot-renew.timer > /dev/null <<'EOF'    
[Unit]    
Description=Run Certbot renewal periodically    
    
[Timer]    
OnCalendar=*-*-* 03,15:20:00    
RandomizedDelaySec=1h    
Persistent=true    
    
[Install]    
WantedBy=timers.target    
EOF    

RandomizedDelaySecを指定すると、同じ時刻に多数のサーバが認証局へ接続する状況を避けられます。Persistent=trueは、予定時刻にサーバが停止していた場合、次回起動後に未実行分を動かす設定です。たとえば夜間だけ停止する検証サーバでも、起動後に更新可否を確認できます。

systemdへ新しい定義を読み込ませ、timerを有効化します。その後、次回実行時刻が一覧に表示されることを確認してください。既存のcertbot.timerが同時に存在する場合は、RPM版など別のCertbotジョブが残っていないか調べ、重複実行を解消します。

sudo systemctl daemon-reload    
sudo systemctl enable --now certbot-renew.timer    
    
sudo systemctl status certbot-renew.timer    
sudo systemctl list-timers --all | grep certbot    

サービス単体も一度起動し、終了状態とログを確認します。証明書がまだ更新時期でなければ、更新を行わず正常終了するのが通常です。失敗した場合は、Cloudflare API、DNS反映、Pythonプラグイン、Webサーバの再読み込みのどこで止まったかをログから切り分けます。

sudo systemctl start certbot-renew.service    
sudo systemctl status certbot-renew.service    
sudo journalctl -u certbot-renew.service --since today    

自動更新を設定しても、プラグインの破損やAPIトークンの失効は、放置すると気付きにくいものです。systemdサービスの失敗を監視へ通知し、月に一度程度はrenew --dry-runの結果も確認します。CloudflareでAPIトークンを再発行した場合はcloudflare.iniを更新し、ファイル権限を600へ戻してから試験してください。

CertbotとCloudflareプラグインの更新は、証明書更新とは別に管理します。仮想環境内のパッケージを更新した直後は、プラグイン一覧、発行設定、dry-runの順に検査し、問題があれば確認済みバージョンへ戻せるよう記録を残します。たとえば次のコマンドを無条件の自動ジョブにせず、検証日を決めて実行するほうが、依存関係の変更によって更新処理が突然止まる事態を減らせます。

sudo /opt/certbot/bin/pip list --outdated    
sudo /opt/certbot/bin/pip install --upgrade certbot certbot-dns-cloudflare    
sudo /opt/certbot/bin/certbot plugins    
sudo /opt/certbot/bin/certbot renew --dry-run \    
  --deploy-hook /usr/local/sbin/reload-after-certbot    

レジストラを問わず、CloudflareへDNSを委任し、APIトークンをroot専用ファイルへ隔離したうえで、DNS-01の試験発行、deploy hook、systemd timer、失敗監視まで設定すれば、ワイルドカード証明書を人手に頼らず更新し続けられます。

おすすめの記事