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-timersjournalctl -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の中から探してください)

出典: Certbot User Guide「Automated Renewals」

つまり自動更新の実現方法は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 --manual do not support automatic renewal"
(--manualで作成された証明書は自動更新をサポートしない)

出典: Certbot User Guide「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."
(更新の試行には、証明書を最初に発行されたときと同じプラグインとオプションが使われる。別のプラグインやオプションを指定しない限り)

出典: Certbot User Guide「Renewing certificates」

つまり、過去に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のステージングサーバに対してテスト実行を行い、テスト用(無効)の証明書を取得するが、ディスクには保存しない)

出典: certbot コマンドリファレンス「--dry-run」

つまり、本番のレート制限を消費せずに更新経路を確認できます。

$ 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/ 配下の
predeploypost 各ディレクトリに置いた実行可能ファイルが、それぞれ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 を使う)

出典: Certbot User Guide「Renewing certificates」

やることは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の期限切れに遭遇したら、この順番で確認するのがおすすめです。

  1. timerが動いているか
    $ sudo systemctl status certbot-renew.timer
  2. 更新処理が失敗していないか(原因がだいたいここに出る)
    $ sudo journalctl -u certbot-renew.service
  3. 認証方式は無人更新可能なものか
    $ sudo cat /etc/letsencrypt/renewal/<ドメイン名>.conf
  4. 修正後のテスト
    $ 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を運用している方の参考になれば幸いです。

ではでは~

参考資料(公式ドキュメントとユーザーガイド)

この記事がお役に立てば【 いいね 】のご協力をお願いいたします!
1
読み込み中...
1 票, 平均: 1.00 / 11
7
X facebook はてなブックマーク pocket

この記事をかいた人

About the author

キタ

アルバイトからビヨンドに拾っていただきました。
人事部 教育課 インフラエンジニア
飛ぶタイプの虫が嫌いです。