はいさい!しまぶだよ。
今日は、自分が管理しているサーバのLet's Encrypt証明書の自動更新が、気付いたらずっと失敗していたので慌てて直したという話です。せっかくなので、何が起こったのか、原因は何だったのか、どうやって解決したのか、という流れで書いていきます。
またいつ同じようなことが起こるかわからないので、備忘録も兼ねています。
TL;DR
blog.shimabox.netを Cloudflare Pages に引っ越したのに、旧サーバの証明書SANから外し忘れていた- HTTP-01 検証が旧サーバではなく Cloudflare Pages 側へ飛び、404 になってSAN一式での証明書更新に失敗していた
- 直そうにも
certbot-autoが動かせなかったので(OSが古くて)、acme.shに移行した
何が起こったのか
わたしはそれなりに昔から持っている shimabox.net を管理しているサーバで、Let's Encryptの証明書を運用していました。仕組みはシンプルで、週に1回cronで certbot-auto renew を走らせるだけ。Let's Encryptの有効期限は90日なので、30日以内になれば自動で更新される。長年これで何も考えずに済んでいました。
ところが今回、ひさびさに shimabox.net のほうを見に行ったら、ブラウザに警告が出ているじゃないですかー。証明書を確認してみると、案の定、証明書が失効してました。 20日以上も前に😇
そもそも自分が昔どうやって自動更新を仕込んだのかもうろ覚えだったので、当時書いていた記事を引っ張り出してきて読み返しました。
Let’s Encryptの自動更新をcron化したけど地味に苦労した話 | Shimabox Blog
2018年...。うおー、7年以上前のじぶん、ありがとう...。感謝感激雨嵐。書いておくって大事だな。なので今回も記事におこしています。
記事で構成を思い出しつつ、VPSに久々にSSHで入って、cronとletsencryptのログを回収。それを手元に持ってきてClaude Codeに投げて、一緒に調べることにしました。
何が原因だったのか
結論から書くと、原因は 2つ ありました。
原因1 SANから外すのを忘れていたサブドメイン
まず最初の犯人は blog.shimabox.net でした。
このブログ、先日 WordPressからCloudflare Pagesに引っ越し をしています。移行時に blog.shimabox.net のDNSレコードは当然Cloudflare側に向け直したのですが、旧サーバ側のLet's Encrypt証明書のSAN(複数ドメイン対応の対象リスト)から blog を外すのを忘れていました。
Claude Codeとのやり取りの一幕です。
Claude Code:ログを見ると、blog.shimabox.net の検証だけ失敗しているように見えます。
わたし:あ、blog はつい最近 Cloudflare Pages に引っ越したんだった。もしかしてそれが原因?
Claude Code:ビンゴかもしれません。念のためDNSを引いてみたら、blog.shimabox.net は CNAME で
xxxxxxxx.pages.devを指していました。旧サーバではなく Cloudflare Pages に到達しています。
Let's Encrypt で証明書を発行するときには、「本当にそのドメインの持ち主か?」を確認する工程があります(ドメイン所有確認)。確認方式は いくつか用意されている のですが、今回使われていたのは HTTP-01 と呼ばれるものです。
ざっくり言うと、こんな流れです。
- Let's Encrypt 「このトークンを
http://ドメイン/.well-known/acme-challenge/xxxで返せるようにしてね」 - certbot 「サーバ側に置いておきましたよ」
- Let's Encrypt がそのURLへGETしに行く
- 期待したトークンが返ってきたら「所有者OK」→ 証明書発行
ところが今回、blog.shimabox.netのDNSはCloudflareに向いています。Let's Encrypt は旧サーバではなく Cloudflare Pages に問い合わせに行く → Cloudflare は当然トークンを置いていないので 404 を返す → 検証失敗、という流れでした。
同じ証明書のSANに複数ドメインが入っている場合、通常はそのうち1つでも検証に失敗すると、そのSAN一式での証明書更新が失敗します。つまり blog.shimabox.net だけの問題に見えても、同じ証明書に入っていた shimabox.net 側まで巻き込まれて更新できなくなっていました。
ここでひとつ引っかかりました。blog を Cloudflare Pages に引っ越したのは 2026-01-11。証明書が失効したのは 2026-04-02。この3ヶ月弱の間、ずっと壊れていなかったように見えた のはなぜでしょう?
わたし:うーん、でもけっこう直近までは動いてたんですけどね。
Claude Code:certbot はデフォルトで「失効30日前」にならないと renew を実行しないんです。今回の証明書の失効日は 2026-04-02 なので、3月3日あたりまでは そもそも renew 自体が走っていなかった はずです。つまり「壊れていなかった」のではなく「試されてすらいなかった」だけ、ということかもしれません。
なるほど。ほんとうにそうなのか、サーバのログ (/var/log/letsencrypt/letsencrypt.log.*) で確認してみました。
ログファイルサイズの推移
| 日付(土曜cron) | ログサイズ | 状態 |
|---|---|---|
| 2026-01-03 | 71 KB | ✅ renew 成功、cert 発行 |
| 2026-01-10 〜 02-28(8週連続) | 1.3 KB | skip(失効まで30日以上あるので何もしない) |
| 2026-03-07 | 62 KB | ❌ 初めて renew 実行 → 失敗 |
| 2026-03-14 / 21 / 28 | 36 KB | ❌ 失敗 |
| 2026-04-02 | — | 証明書失効 |
| 2026-04-04 / 11 / 18 | 36〜56 KB | ❌ 失敗 |
| 2026-04-24 | — | わたしが気付く |
読み方としては、
1.3 KBの週 = 「起動してすぐ『まだ余裕あるよ』で終了」しているだけのログ30〜70 KBの週 = 実際に renew 処理が走って、成功 or 失敗している週
サイズの分布を見るだけで、1月〜2月末の8週間は certbot が何もしておらず、3月7日から突然動きはじめている のがわかります。まさに Claude Code の仮説どおりでした。
本当に skip / 失敗していたか、ログの中身でも裏取り
ファイルサイズだけで断定するのは強引なので、代表的な週のログの中身も確認します。
2026-01-10 のログ(1.3 KB、skip 週)
INFO:certbot._internal.renewal:Cert not yet due for renewal
DEBUG:certbot._internal.renewal:no renewal failures→ 「まだ失効日まで余裕あるから今日は何もしないよ」と確かに skip していました。
2026-03-07 のログ(62 KB、初めて失敗した週)
INFO:certbot._internal.renewal:Cert is due for renewal, auto-renewing...
INFO:certbot._internal.auth_handler:http-01 challenge for blog.shimabox.net
...
WARNING:certbot._internal.auth_handler:Challenge failed for domain blog.shimabox.net
Detail: 2606:4700:310c::ac42:2d2d: Invalid response from
http://blog.shimabox.net/.well-known/acme-challenge/9b1D...: 404→ 初めて renew を実行した週 に、予想どおりblog.shimabox.netの検証だけ失敗していました。しかも決定打がひとつ。レスポンスを返したIP 2606:4700:... はCloudflare の帯域です。Let's Encrypt の検証トラフィックは旧サーバには届いておらず、Cloudflare Pages 側が 404 を返していた ことまで見えました。これで、blogのDNS変更が原因だと完全に確定です🔥
分かったこと
一連の時系列をまとめると、こうなっていました。
- 2026-01-03
- 前回の renew が成功、新しい cert が発行される(このとき blog.shimabox.net はまだ旧サーバ向きだったので検証も通っていた)
- 2026-01-11
- blog を Cloudflare Pages に移設(
blog.shimabox.netの DNS は Cloudflare へ)
- blog を Cloudflare Pages に移設(
- 2026-01-11 〜 2026-03-03
- cron は毎週土曜に発火していたが、cert の失効日まで30日以上あるので certbot はそもそも renew を実行していない(= skip 期間)
- 2026-03-03 頃〜
- cert 失効まで30日を切り、certbot が renew を実行しはじめる → blog の検証が通らず 毎週失敗
- 2026-04-02
- 証明書失効
- 2026-04-24
- わたしが気付く
blog の引っ越しから気付くまでの 3ヶ月強、自動更新は静かに壊れていた わけです。失敗が始まった 3月7日から気付いた 4月24日まで 7週間 放置。これはこれは、少し恥ずかしい。。(公開しているコンテンツを持っている以上、ちょっと恥ずかしい)
原因2 直そうにも certbot-auto が動かない
原因1が見えたら、あとは「blog.shimabox.net を SAN から消して再発行すれば直る」はずです。
というわけで、実際に certonly を叩いてみると、
sudo certbot-auto certonly --standalone \
--cert-name shimabox.net \
-d shimabox.netYou have an ancient version of Python entombed in your operating system...
This isn't going to work; you'll need at least version 3.5.
お使いのオペレーティングシステムには、古いバージョンのPythonが埋め込まれています...
これでは動作しません。少なくともバージョン3.5が必要です。はい。このとおり、Pythonのバージョンが古いと言われて動作しませんでした。😇😇😇
このエラーの通り、certbot-auto は内部で Python 3.5+ を必要としています。しかし、その Python を入れようにも OS 側のリポジトリが古すぎて入らない。詰み。
要するに、blog を SAN から消したかったけど、消す手段(certbot-auto)が動かなかった。
ツール本体の入れ替えから考えないといけないことがここで確定しました。
ちなみに、cron で動いていた certbot-auto renew 自体はこれまで通り動作していました(2026-01-03 の renew は成功、4/18 のログでも certbot 本体は起動して blog の検証だけで落ちていた)。手で別のコマンドを叩いた瞬間はアウトなのに、なぜ renew は動いていたのか、詳しく追えずじまいでした。
推測ですが、certbot-auto のスクリプト は 自身のバージョンと既にインストール済みの certbot のバージョンが一致している限り virtualenv を作り直さない 実装になっているので、cron の renew は既存の venv をそのまま使って動いていた、ということかもしれません。手で別のコマンドを叩いた瞬間に venv 再作成フェーズに入って Python 3.5+ を要求された、のかなと。
詳しい方がいたら教えてください🙇
どうやって解決したのか
まずは選択肢を整理
ここまでをかんたんに言うと、「直接の原因は blog だが、直そうにも肝心のツールが使えない」というダブルパンチ佐藤状態。
ツールを延命するか、入れ替えるか。
なお、certbot-auto の 廃止アナウンス では 「snap での certbot インストールを推奨」 と明言されています(Certbot 公式の Instructions でも snap / pip などの選択肢が案内されています)。つまり正攻法は「certbot-auto をやめて、本家 certbot を snap などで入れ直す」です。
ただ、今回のわたしのサーバではその正攻法が素直には通りませんでした。
- snap 経由: snapd の対応OS に今回のディストリビューションが入っておらず、事実上まともに動かない
- pip 経由: Python 3.5+ が必要で、certbot-auto と同じ壁にぶつかる。Python を別ルートで調達すれば可能ではあるが、古いディストリビューションをいじる手間とリスクを追加で払うことになる
- Docker 経由: Certbot 公式 Docker イメージもある。ただし Docker 自体の導入にも古い環境ゆえのハードルあり
どれも「完全に不可能」ではないですが、素直に入れる手段が無く、どのルートにも延命工事が必要 になる状態でした。
では certbot そのものではなく、別のソリューションはないだろうか?となるのは自然な流れです。調べてみると、有名どころで acme.sh という、certbot の代わりになるツール がありました。pure shell で書かれていて依存がほぼないので、古い環境との相性も良さそう。
Claude Codeと話しながら、残った選択肢を以下に整理してみました。
| 選択肢A: certbot-auto延命 | 選択肢B: acme.shに乗り換え | |
|---|---|---|
| 延命コスト | 中(不安定) | 低(シェルスクリプトなので素直) |
| 将来性 | なし(廃止済み) | あり(開発継続中) |
| 必要な依存 | 古いPython環境の復活 | Python不要で依存が軽い |
| 今後の再発リスク | 高 | 低 |
これを見て、Bの acme.sh に乗り換える ことにしました。
acme.sh とは
acme.sh は、Let's Encryptなど「ACMEプロトコル」対応の認証局から証明書を取得・更新するためのシェルスクリプトです。certbotと同じレイヤーのツール、と思ってもらえればいいかと。
特徴を挙げると、
- Pure shell で書かれている(PythonもGoもRubyも不要)
- 依存がほぼない ので古い環境でも動く
- 広く使われている ので情報量が多い
- 自動更新のcronもインストール時に自動でセットアップ してくれる
- webroot・standalone・DNS APIなど 認証方法が豊富
今回のように「動かしたい環境がちょっと古い」状況との相性は抜群だなと思いました 💪
実際の移行
手順は以下の流れで進めました。
1. 旧cronを削除
acme.sh はインストール時に自動で cron を登録してくれるので、先に旧 cron を止めて 新旧並走を防ぎます。
sudo mv /etc/cron.d/letsencrypt /root/letsencrypt.cron.disabled.$(date +%Y%m%d)(ファイルごとリネームして退避。戻したくなっても mv で戻すだけ)
2. acme.sh をインストール
acme.sh は $HOME/.acme.sh/ にインストールされ、そのタイミングで 「証明書の自動更新」を定期実行するための cron を 実行したユーザの crontab に登録します。certbot-auto のときと同じく、期限が近づいたら勝手に証明書を更新してくれる、という仕組みです。
後のステップでやることになりますが、この自動更新時には service httpd stop/start を挟む 必要があります(acme.sh が検証のために一時的に :80 を使うため)。この操作には root 権限が要るので、自動更新の cron 自体が root の crontab に入っていてくれないと困ります。
なので、acme.sh 自体も root で実行してインストール します。そうすると /root/.acme.sh/ に置かれ、自動更新 cron も root の crontab に登録されます。
# 一度 root シェルに入って作業
sudo -i
# acme.sh をインストール
curl https://get.acme.sh | sh -s email=your-email@example.comインストール後、/root/.acme.sh/ にスクリプトが展開され、root の crontab に自動更新エントリが登録されます。
3. まず staging 環境で検証(これ大事)
Let's Encrypt には本番とは別の staging 環境 があって、本番と同じフローを低リスクで試せます。本番はレートリミットがそこそこ厳しいので、最初に staging で「そもそも検証が通るか」を確認してから本番で実行したほうが、余計な失敗でロックされずに済みます。
sudo /root/.acme.sh/acme.sh --issue --standalone \
-d shimabox.net \
--pre-hook "service httpd stop" \
--post-hook "service httpd start" \
--server letsencrypt_test-d は複数並べられるので、SANに含めたい他のドメインがあれば -d sub.example.com の形でいくらでも追加できます。たとえば example.com と www.example.com、api.example.com を同じ証明書に含めたい場合は以下のようになります。
sudo /root/.acme.sh/acme.sh --issue --standalone \
-d example.com \
-d www.example.com \
-d api.example.com \
--pre-hook "service httpd stop" \
--post-hook "service httpd start" \
--server letsencrypt_test--server letsencrypt_test が staging 指定のキモで、ここで「Cert success.」まで出れば検証フローは通っています。
4. 本番で証明書発行(SANから blog を除外)
staging が通ったら、同じコマンドの letsencrypt_test を letsencrypt に替えて本番発行します。-d には blog 以外 のドメインを並べます。
sudo /root/.acme.sh/acme.sh --issue --standalone \
-d shimabox.net \
--pre-hook "service httpd stop" \
--post-hook "service httpd start" \
--server letsencrypt --force5. 既存のApacheが読んでいるパスにインストール
certbot-auto 時代に Apache が見ていた /etc/letsencrypt/live/shimabox.net/ 配下のパスをそのまま使うので、Apache の設定変更は不要 です。
sudo /root/.acme.sh/acme.sh --install-cert -d shimabox.net \
--cert-file /etc/letsencrypt/live/shimabox.net/cert.pem \
--key-file /etc/letsencrypt/live/shimabox.net/privkey.pem \
--fullchain-file /etc/letsencrypt/live/shimabox.net/fullchain.pem \
--ca-file /etc/letsencrypt/live/shimabox.net/chain.pem \
--reloadcmd "service httpd restart"--reloadcmd を指定しておくと、以降の自動更新時にも指定したコマンドを実行してくれます。今回は service httpd restart なので、証明書更新後に httpd を再起動します。
6. 挙動確認
# 有効期限とSubject(CN)を確認
sudo openssl x509 -in /etc/letsencrypt/live/shimabox.net/cert.pem -noout -dates -subject
# SANに blog が含まれていないことを確認
sudo openssl x509 -in /etc/letsencrypt/live/shimabox.net/cert.pem -noout -text | grep -A1 "Subject Alternative Name"有効期限が 約90日先まで戻っている こと、SAN から blog が消えていることを確認。ブラウザからも実際に叩いて問題ないか見ておくと安心です。
結果
無事、新しい証明書が shimabox.net 系(blogを除く)のSANで発行されて、Apacheにも反映されました。有効期限も約90日先まで戻って一安心です。
acme.sh のインストール時にcronも自動で登録されるので、以降は何もしなくても毎日チェックして、期限が近ければ勝手に更新してくれます。便利すぎる。
まとめ
今回の件で個人的に感じたのは、主に以下です。
- 静かに失敗する自動化は、たちが悪い
- 引っ越しは「引っ越し元に残っている参照」まで含めて棚卸しする
- ツール自体にも寿命がある
とくに1つ目です。「動いている」と思っていたものが、実はずいぶん前から動いていなかった。エラーコードで終了していたし、ログにも失敗は残っていたけれど、誰もそこを見ていないなら無いのと同じで、誰も見ていないからこそ尚更たちが悪いです。ヘルスチェックや失敗時通知を仕込むべきだったなと反省しました。(これは別途ちゃんと仕込みます)
あとは、blog を Cloudflare Pages に引っ越したときに、旧サーバ側の設定も棚卸ししておけばよかった ということですね。これは「引っ越し先の設定を見直す」だけでなく、「引っ越し元の設定も見直す」ということです。DNSを切り替えるときは、DNSを切り替えた後に何が壊れる可能性があるか も考慮しておくべきでした。
今回の一連のやり取りは、ログを貼って、推測して、検証して、方針を決めて、実行して… というループをClaude Codeと一緒に回しました。一人で眺めていたら、DNSが原因だと気付くまでもっと時間がかかっていた気がします。AIと壁打ちしながら原因にたどり着くのは、ほんとうに効率がよい。
というわけで、自動化を仕込んだら 「動いているつもり」にならないよう定期的に見に行く、または通知を仕込む、これが今日の教訓です。
こういう時は「もうVPSじゃなくて全部サーバーレスに移行すればいいのでは?」と思ったりもします。Cloudflareみたいな基盤なら、証明書のことなんて考える必要すらなくて楽ですよね。
ただ、自前で管理するサーバが1つあるおかげで、こうして突然叩き起こされるたびに忘れていた仕組みを思い出したり、acme.sh みたいな新しいツールに出会えたりもします。これはこれで、お勉強になるし、楽しいなと思います。今はAIというやつもあるからな!
それでは。
