Certbotの自動更新、実はsystemd timerが標準だった件

目次
どうも、キタです。
なんかラノベのタイトルみたいになりましたが、今回はLet's Encryptについての記事です。
先日、自社で運営しているサイトのSSL証明書が期限切れになりました。
インフラエンジニアとして普通に恥ずかしい話なんですが、そのおかげで長年の勘違いに気付くことができました。
自分はずっと「Let's Encryptの自動更新 = cron」だと思っていました。
ところが調査してみると、AlmaLinux 9 + Certbot 3.xではcronではなくsystemd timerが標準になっていました。
また、今回の期限切れの原因はそのどちらでもない、別のところにありました。
今回は実際の障害対応で調査した内容と、新しく学んだことを備忘録としてまとめます。
先に結論(急いでいる人向け)
- AlmaLinux 9(EPEL版Certbot)の自動更新はcronではなくsystemd timer(
certbot-renew.timer)が標準 - timerが動いていても、認証プラグインの設定次第で更新は失敗し続ける
- 期限切れが起きたら、
systemctl list-timers→journalctl -u certbot-renew.serviceの順に確認する - 更新後のWebサーバ反映は deploy hook で自動化しておく
以下、実際の対応の流れに沿って解説します。
ある日突然発生したトラブル
ある日、直近リリースした自社サービスHPにアクセスすると、SSL証明書の期限切れでブラウザに警告画面が表示されていました。

これはマズいです。
Let's Encryptの証明書は有効期限が90日と短く、自動更新を組むことが運用の大前提です。
裏を返せば、期限切れが起きたということは、どこかで自動化ができていないということになります。
まず最初に思ったことは「あれ、cron設定してなかったっけ?」でした。
そこで早速確認したのは、みんな大好きcronです。
$ sudo crontab -l $ sudo cat /etc/crontab $ sudo ls /etc/cron.d/
……あれ、何も書かれていない。
「設定漏れか...」と思いかけたんですが、それだと辻褄が合いません。
cronが空なのに、これまでの更新は成功していたからです。
調べていくと、こんな情報を見つけました。
最近のCertbotはsystemd timerを使う
え、そうなの?初耳でした。
Certbotの自動更新はcronとは限らない
これ、実は公式ドキュメントに明記されていました。
Certbot公式User Guideの「Automated Renewals」セクションには、ほとんどのCertbotインストールには自動更新があらかじめ設定されていること、
そしてその確認方法として、crontab(/etc/crontab や /etc/cron.*/*)またはsystemd timer(systemctl list-timers)を見るように書かれています。
"look for the certbot renew command in either your system's crontab (typically /etc/crontab or /etc/cron.*/*) or systemd timers (systemctl list-timers)"
(certbot renewコマンドを、システムのcrontab、またはsystemd timerの中から探してください)
つまり自動更新の実現方法はcronとは限らず、どちらが使われるかはディストリビューションやパッケージ次第のようです。
今回のAlmaLinux 9のEPEL版Certbotは、systemd timer方式でした。
systemd timerを確認してみる
$ sudo systemctl list-timers | grep certbot
結果はこちら。
なんとcertbot-renew.timerが動いていました。
つまり、cronを1行も書かなくても、自動更新は最初から仕込まれていたわけです。
cronとsystemd timerの違い
せっかくなので、一旦両者の違いも整理しておきます。
運用目線だと、ログとステータスの見やすさが段違いです。
| 項目 | cron | systemd timer |
|---|---|---|
| 実行ログの確認 | syslog等を自分で追う | journalctl -u <service> で一発 |
| 次回実行時刻 | 直接見る手段がない(自分で計算) | systemctl list-timers で見える |
| サーバ停止中の実行漏れ | 実行されずスキップ | Persistent=true なら起動後にリカバリ |
| AlmaLinux 9のCertbot | 使われない | 標準(certbot-renew.timer) |
ただ、ここで疑問が残ります。自動更新は動いていた。
なのに、なぜ期限切れになったのか。
なぜ期限切れになったのか:原因は認証方式
timerが動いているのに証明書が切れたということは、
更新処理は実行されたが、失敗し続けていたと考えるのが自然です。
ログを確認します。
$ sudo journalctl -u certbot-renew.service --since "yesterday" | grep -i error
Jul 01 11:12:29 ip-172-26-9-○○.ap-northeast-1.compute.internal certbot[355188]: Failed to renew certificate www.server-camp.com with error: The manual plugin is not working; there may be problems with your existing configuration.
Jul 01 11:12:29 ip-172-26-9-○○.ap-northeast-1.compute.internal certbot[355188]: The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.')
あっ...
「The manual plugin is not working」
「An authentication script must be provided」
ここで原因が出ました。manualプラグインの認証エラーです。
自動更新自体は実行されていた。でも、証明書の更新処理が失敗していた。
cronの有無ではなく、認証方式の問題だったわけです。
manualプラグインだと自動更新できない理由
Certbot公式User Guideの「Manual」セクション「Renewal with the manual plugin」の項に、
答えがそのまま書いてあります。
"Certificates created using
--manualdo not support automatic renewal"
(--manualで作成された証明書は自動更新をサポートしない)
例外は、--manual-auth-hook で認証フックのスクリプトを用意した場合のみ。
--manual は、DNSレコードの追加といった人間の手作業を前提とした認証方式なので、フックによる自動化なしでは、無人で走るtimerからは更新できません。
さらにもう一つ押さえておきたい仕様があります。
公式User Guideの「Renewing certificates」セクションに、更新時のプラグインの扱いが明記されています。
"The same plugin and options that were used at the time the certificate was originally issued will be used for the renewal attempt, unless you specify other plugins or options."
(更新の試行には、証明書を最初に発行されたときと同じプラグインとオプションが使われる。別のプラグインやオプションを指定しない限り)
つまり、過去にmanualで取得された証明書は、その後もずっとmanualで更新しようとし続ける、ということです。
timerは定期実行のたびに失敗を繰り返し、そのまま90日が経過して期限切れ──というのが今回の全容でした。
対処:webroot認証に切り替えて更新テスト
原因がわかれば、あとは対処するだけです。
期限切れの緊急対応として certbot certonly --webroot で手動更新した結果、renewal設定(/etc/letsencrypt/renewal/<ドメイン名>.conf)は書き換わり、
現在は authenticator = webroot になっています。
$ sudo cat /etc/letsencrypt/renewal/www.server-camp.com.conf | grep authenticator authenticator = webroot
webroot認証はHTTP-01チャレンジを利用する認証方式で、人間の介入が不要になります。
つまりtimerからの無人更新が可能な認証方式です。
設定を直したら、必ず--dry-runで更新テストをします。
--dry-runの仕様は、公式のコマンドリファレンスに次のように書かれています。
"Perform a test run against the Let's Encrypt staging server, obtaining test (invalid) certificates but not saving them to disk."
(Let's Encryptのステージングサーバに対してテスト実行を行い、テスト用(無効)の証明書を取得するが、ディスクには保存しない)
つまり、本番のレート制限を消費せずに更新経路を確認できます。
$ sudo certbot renew --dry-run Saving debug log to /var/log/letsencrypt/letsencrypt.log - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Processing /etc/letsencrypt/renewal/www.server-camp.com.conf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Simulating renewal of an existing certificate for www.server-camp.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Congratulations, all simulated renewals succeeded: /etc/letsencrypt/live/www.server-camp.com/fullchain.pem (success) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
「Congratulations, all simulated renewals succeeded」と出ているのでこれで更新経路は正常。続けてサービス側の状態も確認しておきます。
$ sudo systemctl status certbot-renew.service ○ certbot-renew.service - This service automatically renews any certbot certificates found Loaded: loaded (/usr/lib/systemd/system/certbot-renew.service; static) Active: inactive (dead) since Mon 2026-07-06 07:16:35 JST; 4h 10min ago TriggeredBy: ● certbot-renew.timer Process: 392173 ExecStart=/usr/bin/certbot renew --noninteractive --no-random-sleep-on-renew $PRE_HOOK $POST_HOOK $RENEW_HOOK $DEPLOY_HOOK $CERTBOT_ARGS (code=exited, status=0/SUCCESS) Main PID: 392173 (code=exited, status=0/SUCCESS) CPU: 333ms
直近の実行が status=0/SUCCESS で正常終了していることが確認できました。
※Active: inactive (dead) と表示されていますが、このサービスは更新チェック実行時のみ動くタイプなので、実行時以外はこの表示で正常です。
「TriggeredBy: certbot-renew.timer」とある通り、timerから起動されていることもここで確認できます。
これで現在は問題なく自動更新できる状態になりました。
ただ、実は最後にもう一つ落とし穴が残ります。
更新後のApache反映も自動化する(deploy hook)
意外と見落としがちなんですが、証明書ファイルが更新されても、
Apacheは新しい証明書を自動では読み込まないため、reloadまたはrestartが必要です。
せっかく自動更新を直しても、reloadが手動なら詰めが甘いです。
ここもCertbotの仕組みで自動化できます。
公式User Guideの「Renewing certificates」セクションによると、/etc/letsencrypt/renewal-hooks/ 配下の
pre・deploy・post 各ディレクトリに置いた実行可能ファイルが、それぞれpre・deploy・postフックとして自動実行されます。(複数ある場合はファイル名のアルファベット順になります)
| フック | 実行タイミング |
|---|---|
| pre | 更新試行の前(失敗しても実行) |
| deploy | 更新に成功したときだけ |
| post | 更新試行の後(失敗しても実行) |
Webサーバのreloadは「更新に成功したときだけ」実行できればいいので、deployのhookが適切です。
公式ドキュメントにも、こう書かれています。
"If you want your hook to run only after a successful renewal, use --deploy-hook"
(更新が成功したときにだけフックを実行したい場合は、--deploy-hook を使う)
やることは2ステップだけです。まずスクリプトを作成。
$ sudo vi /etc/letsencrypt/renewal-hooks/deploy/reload-httpd.sh
中身はこれだけです。
#!/bin/bash systemctl reload httpd
実行権限も付与しておきます。
$ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-httpd.sh
これで、
証明書更新(成功時)
↓
deploy hook実行
↓
Apache reload
↓
新証明書が即時反映
まで完全に自動化できます。
なお、反映後の証明書が正しく適用されているかはopensslコマンドでも確認できます。
確認方法はopensslコマンドでの証明書の整合性・検証結果の確認方法で解説しています。
今回学んだこと:期限切れが起きたら見る順番
今回一番勉強になったのは、
「cronの設定がない = 自動更新されない」ではないこと。そして
「timerが動いている = 更新できている」でもない、ということでした。
もしLet's Encryptの期限切れに遭遇したら、この順番で確認するのがおすすめです。
- timerが動いているか
$ sudo systemctl status certbot-renew.timer
- 更新処理が失敗していないか(原因がだいたいここに出る)
$ sudo journalctl -u certbot-renew.service
- 認証方式は無人更新可能なものか
$ sudo cat /etc/letsencrypt/renewal/<ドメイン名>.conf
- 修正後のテスト
$ sudo certbot renew --dry-run
この流れを知っていれば、今回のような調査はたぶん15分ほどで終わります。
よくある質問(FAQ)
Q. Certbotの自動更新はcronとsystemd timerのどちらで動いていますか?
A. パッケージとディストリビューションによります。Certbot公式User Guideの「Automated Renewals」では、ほとんどのインストールに自動更新があらかじめ設定されているとした上で、確認方法としてcrontabまたはsystemd timer(systemctl list-timers)を挙げています。AlmaLinux 9(EPEL版)ではsystemd timer(certbot-renew.timer)が標準です。
Q. certbot-renew.timerが動いているのに証明書が期限切れになるのはなぜですか?
A. timerは「更新コマンドの定期実行」を保証するだけで、更新処理の成功までは保証しません。
manualプラグインで取得した証明書は認証フックなしでは自動更新できないため、実行はされても失敗し続けます。journalctl -u certbot-renew.service でエラー内容を確認してください。
Q. 「The manual plugin is not working」エラーの対処法は?
A. 証明書の取得方式をwebroot認証など無人更新可能な方式に切り替えます。certbot certonly --webroot -w <ドキュメントルート> -d <ドメイン> で再取得すると、renewal設定も更新され、以降は自動更新が通るようになります。
Q. 証明書を更新してもブラウザで古い証明書が表示されるのはなぜですか?
A. Webサーバが起動時に読み込んだ古い証明書を使い続けているためです。/etc/letsencrypt/renewal-hooks/deploy/ にreloadスクリプトを置いておくと、更新成功時に自動でWebサーバへ反映されます。
Q. cronとsystemd timerは両方設定してもいいですか?
A. おすすめしません。
certbot renew 自体は期限が近い証明書しか更新しないので二重実行の実害は小さいですが、実行元が2系統あるとログの確認先が分散して、障害時の調査が煩雑になります。
ディストリビューション標準の方式に一本化しましょう。
Q. certbot.timerとcertbot-renew.timerは何が違いますか?
A. 中身は同じCertbotの自動更新用systemd timerで、パッケージによって名前が違うだけです。
AlmaLinux 9などRHEL系(EPEL版)は certbot-renew.timer、Debian/Ubuntu(apt版)は certbot.timer、snap版は snap.certbot.renew.timer という名前になっています。
systemctl list-timers | grep certbot ならどの環境でも確認できます。
まとめ
今回の対応で分かったことをまとめます。
- AlmaLinux 9ではCertbotの自動更新はsystemd timer(
certbot-renew.timer)で動作する - cronを書かなくても自動更新される場合がある(公式ドキュメントにも明記あり)
- 自動更新が動いていても、manualプラグイン等の認証エラーで失敗し続けることがある
- 原因特定の最短ルートは
journalctl -u certbot-renew.service - 更新後はdeploy hookでWebサーバのreloadまで自動化しておくと確実
今回の障害対応は正直ヒヤッとしましたが、その分cronだけ見ていては原因を見誤るという良い学びになりました。
同じようにLet's Encryptを運用している方の参考になれば幸いです。
ではでは~
参考資料(公式ドキュメントとユーザーガイド)
- Certbot User Guide - Automated Renewals … 自動更新の仕組みと確認方法
- Certbot User Guide - Manual … manualプラグインが自動更新非対応である旨の記載
- Certbot User Guide - Renewing certificates … 更新時のプラグイン引き継ぎ、フック(pre/deploy/post)の仕様
- certbot コマンドリファレンス …
--dry-runの仕様 - Let's Encrypt 公式サイト
1
