ある朝サイトを開くと「この接続ではプライバシーが保護されません」。原因は SSL 証明書の期限切れ──攻撃ではありませんが、フォームも EC も一斉に止まる、防げたはずの事故です。
この記事は、Let's Encrypt(certbot)で証明書を運用している Web 担当者・サイト運営者・制作会社向けに、「自動更新にしてあるはず」を実際に点検し、期限切れ前に必ず気づける監視を入れるまでの 5 つの対策をまとめたものです。nginx / Apache の設定例付きです。
なぜ今、点検が必要なのか
「自動更新にしているし、切れそうならメールが来る」という前提が崩れています。
1. 期限切れ通知メールは終了した。 Let's Encrypt は 2025 年 6 月 4 日で通知メールを終了しました(公式告知)。自動更新が黙って失敗していても、もう誰も教えてくれません。
2. 有効期間がどんどん短くなる。 CA/Browser Forum の Ballot SC-081v3 と Let's Encrypt の短縮計画により、次のように変わります。
| 時期 | 内容 |
|---|---|
| 2026-03-15 | 公開 TLS 証明書の最長有効期間 398 日 → 200 日(適用済み) |
| 2027-02-10 | Let's Encrypt の既定 90 日 → 64 日 |
| 2027-03-15 | 最長有効期間 → 100 日 |
| 2028-02-16 | Let's Encrypt の既定 → 45 日 |
| 2029-03-15 | 最長有効期間 → 47 日 |
期間が短いほど、更新失敗から期限切れまでの猶予も短くなります。有料証明書を年 1 回手で入れ替える運用も、200 日上限の今は成り立ちません。
対策1: 配信中の証明書の期限を外から確認する
サーバ上のファイルではなく、実際に配信されている証明書を見ます(ファイルは新しいのに配信は古い、という事故があるため)。
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -enddate
複数ドメインを同居させたサーバでは、-servername がないと別サイトの証明書が返ることがあります。certbot 管理下の一覧と残り日数はサーバ上で確認します。
sudo certbot certificates
ここに出てこないドメインは certbot の管理外です。手動発行やレンタルサーバのパネル経由の証明書が混ざっていないか、配信中の証明書と突き合わせておきます。
対策2: 自動更新が本当に動いているか確認する
# systemd タイマー / cron のどちらかに certbot renew があるか
systemctl list-timers --all | grep -i certbot
grep -r "certbot" /etc/crontab /etc/cron.d/ 2>/dev/null
# 本番の証明書は変えずに、更新が成功するかを試す
sudo certbot renew --dry-run
# 失敗時の理由
sudo tail -n 100 /var/log/letsencrypt/letsencrypt.log
タイマーにも cron にも何もなければ、自動更新は仕込まれていません。--dry-run はステージング環境で更新を試すだけでディスク上の証明書は書き換えないので、いつ実行しても安全です。
注意したいのが実行間隔です。certbot renew は期限が近い証明書だけを更新するコマンドで、判定は certbot 自身が行います。実行は毎日回しておくのが前提です。
- certbot 4.0.0 以降: 残り有効期間が 1/3 を切ったら更新(10 日以下の証明書は 1/2)。CA が ARI で更新時期を示せばそれにも従う
- 4.0.0 より前: 残り 30 日固定
「毎月 1 日に実行」「60 日ごとに更新」といった固定間隔は、64 日・45 日の証明書で更新の窓を踏み外します。certbot --version も確認しておきましょう。
対策3: 更新後に Web サーバへ確実に反映する
certbot がファイルを更新しても、nginx / Apache は再読み込みするまで古い証明書を配信し続けます。更新が成功したときだけ動く deploy hook に、構文チェック付きのリロードを仕込みます。
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-web.sh >/dev/null <<'EOF'
#!/bin/sh
# 構文チェックが通ったときだけ反映する(設定エラーでサイトを落とさない)
nginx -t -q && systemctl reload nginx
# Apache の場合は上の行の代わりに:
# apachectl configtest && systemctl reload apache2
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-web.sh
作ったら一度手で実行してエラーが出ないことを確認します。あわせて、Web サーバが参照するファイルも見直します。
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
-
cert.pem単体だと中間証明書が送られず、PC では見えるのに一部のスマホや API クライアントだけ接続エラーになる(Apache 2.4.8 以降もSSLCertificateFileにfullchain.pem) -
live/は更新のたびに新しい実体を指し直すシンボリックリンク。archive/配下の番号付きファイルを直接指定すると更新が反映されない
対策4: 更新を妨げる設定を塞がない
HTTP-01 認証では、Let's Encrypt が http://<ドメイン>/.well-known/acme-challenge/ にアクセスして管理権を確認します。セキュリティのために入れた設定がここを塞ぎ、更新だけが失敗し続けるのが典型です。
-
.gitや.envを守るためのドットファイル一律 deny - 公開前サイト向けの Basic 認証や IP 制限をサイト全体にかけた
- ドキュメントルートを移したのに
/etc/letsencrypt/renewal/<ドメイン>.confのwebroot_pathが古いまま
チャレンジ用パスだけを例外にし、ドットファイルの遮断そのものは残します。
# ^~ の前方一致は、以降の正規表現 location より優先される
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt; # certbot 側は --webroot -w /var/www/letsencrypt
auth_basic off;
allow all;
try_files $uri =404;
}
location ~ /\. {
deny all;
}
# .well-known 以外のドットファイルは遮断
<LocationMatch "/\.(?!well-known/)">
Require all denied
</LocationMatch>
# サイト全体に Basic 認証があっても、チャレンジ用パスだけは通す
<Location "/.well-known/acme-challenge/">
Require all granted
</Location>
反映後に sudo certbot renew --dry-run が通ることを確認します。
対策5: 期限を「外から」監視する
最後の砦は、更新の仕組みとは独立した場所から残り日数を見張ることです。更新と監視が同じサーバに乗っていると、サーバや cron の不調で両方同時に止まるので、別サーバや監視用マシンから回すのが理想です。
しきい値は固定日数ではなく有効期間の 1/4 にします。certbot は残り 1/3 で更新を始めるので、1/4 を切っていれば「しばらく更新に失敗し続けている」ことを意味し、期間が変わっても見直し不要です(90 日なら残り 22 日、45 日なら 11 日、200 日なら 50 日で通知)。
#!/usr/bin/env bash
# /usr/local/bin/check-cert-expiry.sh
set -u
DOMAINS=("example.com" "www.example.com")
NOTIFY_TO="web-admin@example.com"
alert=""
now=$(date +%s)
for d in "${DOMAINS[@]}"; do
pem=$(timeout 15 openssl s_client -connect "${d}:443" -servername "${d}" </dev/null 2>/dev/null \
| openssl x509 2>/dev/null)
if [ -z "${pem}" ]; then
alert+="${d}: 証明書を取得できません\n"
continue
fi
start=$(date -d "$(openssl x509 -noout -startdate <<<"${pem}" | cut -d= -f2)" +%s)
end=$(date -d "$(openssl x509 -noout -enddate <<<"${pem}" | cut -d= -f2)" +%s)
life=$(( (end - start) / 86400 ))
left=$(( (end - now) / 86400 ))
limit=$(( life / 4 )); [ "${limit}" -lt 3 ] && limit=3
if [ "${left}" -lt "${limit}" ]; then
alert+="${d}: 残り ${left} 日(有効期間 ${life} 日)\n"
fi
done
if [ -n "${alert}" ]; then
printf '%b' "${alert}" | mail -s "[要対応] SSL証明書の期限" "${NOTIFY_TO}"
fi
# 毎朝 9:00 に実行(crontab -e)
0 9 * * * /usr/local/bin/check-cert-expiry.sh
date -d は GNU coreutils 前提(Linux)です。mail は Slack / Teams の Webhook への curl に置き換えても構いません。証明書を取得できないときも通知するので、サイトごと落ちた場合にも気づけます。導入したら一度わざと鳴らし、通知が届くことを確認してください。
まとめ
- 配信中の証明書の期限を
openssl s_clientで外から確認する - タイマー / cron の存在と
certbot renew --dry-runで自動更新を点検する - deploy hook でリロードし、
fullchain.pem(live/配下)を指定する - ドットファイル遮断や Basic 認証から
/.well-known/acme-challenge/だけを外す - 有効期間の 1/4 をしきい値に、別の場所から期限を監視する
証明書の有効期間は 2029 年に向けて短くなり続けます。更新が動いていることを確かめる仕組みと、失敗に必ず気づける監視の 2 本立てにしておくことが、期限切れ事故を防ぐいちばん確実な方法です。
関連記事
本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp