SSL 証明書の期限切れアラート:もう誰もメールでは教えてくれない

Let's Encrypt は期限切れメールを停止し、証明書の有効期間は 200 日に。実測済みの openssl チェック、短命証明書に合うしきい値、通知の組み方をまとめました。

更新日

目次

誰の証明書まわりでも、足元で 2 つのことが変わりました。そして、そのどちらにも対応できていないチームがほとんどです。

1 つめ、セーフティネットが外れました。 Let's Encrypt は 2025 年 6 月 4 日に期限切れ通知サービスを停止しました。自動化が静かに壊れたときに、何千ものサイトを救ってきたあのメールです。理由自体は妥当でした(大半の利用者はすでに更新を自動化している、数百万件のメールアドレスを保持するのはプライバシー上の負債、そして運用コストが「年に数万ドル」)。そして告知の最後は「代わりにサードパーティの監視を探してください」で終わっています(Let's Encrypt)。多くの人はそこまで読んで納得し、後半をやらないままになりました。

2 つめ、余裕が消えました。 2026 年 3 月 15 日以降、公開 TLS 証明書の有効期間は最長 200 日です。CA/Browser Forum の投票 SC-081v3 により、2027 年 3 月 15 日に 100 日、2029 年 3 月 15 日に 47 日へと下がります。Let's Encrypt は上限より速く動いていて、6 日間証明書は 2026 年 1 月 15 日に一般提供となり、既定プロファイルは 2027 年 2 月に 64 日、2028 年 2 月に 45 日になる計画です(Let's Encrypt)。

この 2 つは同じ方向に働きます。更新の回数が増えれば壊れる機会も増え、壊れても誰もメールをくれません。この記事はその隙間を塞ぐための「点検」です。動作確認済みのシェルスクリプト、短命証明書に合うしきい値、そして最後の一線を Echobell で電話として鳴らす方法を扱います。

証明書起因の障害は、実際どれくらい深刻なのか?

昨年、3 分の 1 を超える組織が実際に経験する程度には深刻です。 DigiCert の「2026 Global Certificate Management Outlook」——2026 年 5 月に Propeller Insights が米・英・豪の IT/セキュリティ意思決定者 1,001 人を対象に実施——では、過去 1 年で3 分の 1 超の組織が証明書の期限切れによるサービス停止を報告しました。4 分の 3 近くが証明書関連のダウンタイムを合計 5 時間以上経験し、5 分の 1 が 25 時間以上、そして4 分の 1 近くが最も重大な証明書インシデントの損害が 25 万ドルを超えたと回答しています(DigiCert)。

この数字で興味深いのは所要時間のほうです。5 時間は証明書の更新にかかる時間ではありません。更新は数秒です。5 時間は、誰かが気づくまでにかかった時間です。

自動更新だけではなぜ足りないのか?

更新の自動化は静かに失敗し、動かなくなった cron は何の出力も出さないからです。 以下はどれも実在するよくある故障で、しかもどれも音を立てません。

  • タイマーがもう動いていない。 ディストリビューションのアップグレード、コンテナの再作成、あるいは 3 か月前に誰かがやった systemctl disable。発火していない certbot.timer は、正常に発火している certbot.timer と見分けがつきません。
  • 更新は成功したが、サービスがリロードされていない。 新しい証明書はディスク上にあるのに、nginx・HAProxy・Postfix はメモリ上の古いものを掴んだままです。「完全自動」の構成がそれでも期限切れになる、いちばんよくある経路です。
  • チャレンジの経路が壊れた。 誰かが /.well-known/acme-challenge/ の手前にリダイレクトや WAF ルール、Deny を入れて HTTP-01 が失敗する。あるいは DNS-01 で使っている DNS プロバイダーの API トークンが失効した。
  • 更新が 1 ノードでしか走らなかった。 ロードバランサーが 2 台、cron は 1 つ。2 台目は古い証明書を配り続け、ある日配れなくなります。
  • そもそも Web サーバー上の証明書ではない。 社内 mTLS のクライアント、Kafka ブローカー、LDAP サーバー、VPN 装置、デバイス管理のプッシュ証明書。公開インターネットからは見えず、ACME クライアントも管理していません。
  • 更新間隔が誤った値でハードコードされている。 Let's Encrypt はこの点を明言しています。既定プロファイルが 64 日、さらに 45 日になると「更新間隔を 60 日でハードコードするのはもはや不十分」です(Let's Encrypt)。

最後の 1 つは強調に値します。カレンダーが全員をそこへ追い込んでいる故障だからです。更新の周期が 2022 年に誰かが打ち込んだ数字のままなら、証明書の有効期間のほうがその数字に向かって歩み寄ってきています。

コマンドラインで有効期限を確認するには?

依存関係なしの openssl パイプライン 1 本です。 稼働中のホストなら:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -startdate -enddate

ディスク上のファイルなら:

openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate

共有 IP では -servername は省略できません。SNI なしだとサーバーが既定と見なす証明書が返り、それはあなたが心配している証明書とは限りません。

Yes / No だけが欲しいなら、日付の解析は丸ごと省けます。openssl x509 -checkend <秒数> は、その期間を証明書が持ちこたえるなら 0、その中で期限切れになるなら(すでに切れている場合を含めて)1 で終了します。

openssl x509 -in fullchain.pem -noout -checkend $((14 * 86400)) \
  || echo "14 日以内に期限切れ"

この終了コードの取り決めが監視のプリミティブそのものです。以下はすべて、その周りの配管にすぎません。

「更新したのにリロードしていない」を捕まえる

ディスク上のものと、実際に配信されているものを比べます。 これはほとんど誰もやっていない点検で、しかも更新の自動化自身には見えない故障を捕まえられます。

served=$(openssl s_client -connect 127.0.0.1:443 -servername example.com </dev/null 2>/dev/null \
         | openssl x509 -noout -fingerprint -sha256)
ondisk=$(openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -fingerprint -sha256)

[ "$served" = "$ondisk" ] || echo "サービスが古い証明書を配信中 — リロードが必要"

どちらのコマンドも sha256 Fingerprint=AB:CD:... という同じ形式で出力するので、単純な文字列比較で十分です。更新タイマーの実行枠から数分後に走らせてください。

証明書アラートのしきい値はどう決めるべきか?

固定の日数ではなく、有効期間に対する割合で決めます。 「30 日前に警告」は 90 日証明書なら妥当でした。47 日証明書に当てはめると、まったく健全な証明書で鳴ります。6 日証明書に当てはめると、鳴りっぱなしになります。

代わりに、更新ポイントを基準にはしごを組みます。Let's Encrypt は「現在の証明書の有効期間のおよそ 3 分の 2 を過ぎたあたり」での更新を推奨しています。つまり残り 3 分の 1 は、更新がすでに済んでいるはずの時点です。それより後はすべて、済んでいなかったことの証拠です。

残り有効期間意味通知タイプ
1/3更新ウィンドウが開いた通知しない — これは正常
1/6更新ウィンドウを 1 回逃した通常プッシュ
1/12更新は遅れているのではなく失敗している時間指定
1/24 未満・期限切れ・到達不能障害まで数時間通話

具体的な数字にすると、47 日証明書ではおおよそ「7.8 日までは沈黙、3.9 日でプッシュ、2 日で時間指定、1 日未満で通話」です。90 日証明書なら 15 日、7.5 日、3.75 日。6 日証明書では単位が時間になり、人間向けのはしごはもう適切な道具ではありません——ACME Renewal Information(ARI、RFC 9773 として公開)に任せて CA からクライアントへ更新時期を伝えてもらい、クライアントが繰り返し失敗したときだけ通知しましょう。

割合の計算は 4 行です。これで、発行元が何であれ手持ちのすべての証明書に対して同じスクリプトが正しく動きます。

to_epoch() {
  date -u -d "$1" +%s 2>/dev/null || date -u -j -f '%b %d %T %Y %Z' "$1" +%s 2>/dev/null
}

pct_left() {  # 標準入力から PEM を読み、残り有効期間の割合(%)を出力する
  local pem nb na
  pem=$(cat)
  nb=$(to_epoch "$(printf '%s' "$pem" | openssl x509 -noout -startdate | cut -d= -f2)")
  na=$(to_epoch "$(printf '%s' "$pem" | openssl x509 -noout -enddate | cut -d= -f2)")
  echo $(( (na - $(date -u +%s)) * 100 / (na - nb) ))
}

1 つめの date は GNU 形式、2 つめは BSD/macOS 形式です。|| が手元にあるほうを選びます。

これを本当に届くアラートにするには?

Echobell は Webhook やメールを、通常プッシュ・時間指定通知・集中モードやおやすみモードを突き抜けて鳴る本物の着信に変えます(iOS の集中モードを突破するを参照)。証明書でこれが効くのは、上の表の最後の段が、日曜の午前 3 時に踏むことになる段だからです。

ステップ 1 —— チャンネルを 1 つではなく 2 つ作る

アプリでチャンネルを作り、通知タイプを時間指定にして「証明書の期限接近」と名付けます。もう 1 つ作り、通話にして「証明書が失効寸前」と名付けます。それぞれのチャンネル詳細から Webhook URL をコピーします。https://hook.echobell.one/t/<channel-token> という形です。これらは秘密として扱ってください。通話側の URL を持つ人は誰でもあなたの電話を鳴らせます(Webhook ガイド)。

ロック画面のまま行動できるようにテンプレートを書きます。

タイトル: TLS {{state}}: {{host}}
本文: 残り {{daysLeft}} 日、{{notAfter}} に期限切れ — 発行者 {{issuer}}

POST した JSON のキーはそのまま変数になります(テンプレート)。

ステップ 2 —— 点検を走らせる

これまでの節をまとめ、端から端まで実際に動かして確認したスクリプトです。/usr/local/bin/cert-watch として保存します。

#!/usr/bin/env bash
# cert-watch — TLS 証明書が期限に近づいたら Echobell へ POST する。
set -uo pipefail

HOOK="${ECHOBELL_CERT_HOOK:?ECHOBELL_CERT_HOOK にチャンネルの Webhook URL を設定してください}"
WARN_DAYS="${WARN_DAYS:-14}"

post() {
  curl -sS -m 10 -X POST "$HOOK" \
    -H 'content-type: application/json' \
    -d "{\"host\":\"$1\",\"daysLeft\":$2,\"notAfter\":\"$3\",\"state\":\"$4\"}" \
    >/dev/null
}

days_left() {
  local end
  end=$(date -u -d "$1" +%s 2>/dev/null) ||
    end=$(date -u -j -f '%b %d %T %Y %Z' "$1" +%s 2>/dev/null) || return 1
  echo $(( (end - $(date -u +%s)) / 86400 ))
}

check() {
  local host="$1" port="$2" pem state not_after days

  pem=$(openssl s_client -connect "$host:$port" -servername "$host" \
        </dev/null 2>/dev/null | openssl x509 2>/dev/null)

  if [ -z "$pem" ]; then
    post "$host" 0 "" "unreachable"
    return
  fi

  if printf '%s' "$pem" | openssl x509 -noout -checkend 0 >/dev/null 2>&1; then
    printf '%s' "$pem" | openssl x509 -noout -checkend $((WARN_DAYS * 86400)) >/dev/null 2>&1 && return 0
    state="expiring"
  else
    state="expired"
  fi

  not_after=$(printf '%s' "$pem" | openssl x509 -noout -enddate | cut -d= -f2)
  days=$(days_left "$not_after") || days=-999
  post "$host" "$days" "$not_after" "$state"
}

for target in "$@"; do
  case "$target" in
    *:*) check "${target%:*}" "${target##*:}" ;;
    *) check "$target" 443 ;;
  esac
done

状態は expiringexpiredunreachable の 3 つを報告し、問題がないときは黙ります。対象は host または host:port で書けるので、Web 以外の証明書も同じようにカバーできます。

ECHOBELL_CERT_HOOK="https://hook.echobell.one/t/<token>" \
  cert-watch example.com api.example.com mail.example.com:993 ldap.internal:636

unreachable は意図的に、黙ってスキップではなくアラートにしてあります。「見に行けなかった」を「異常なし」として扱う監視こそが、そもそも証明書が期限切れになる原因です。

ステップ 3 —— スケジュールし、スケジュール自体の失敗にも気づく

45 日以上の証明書なら 1 日 1 回で十分、6 日証明書を使うなら 1 日 2 回にします。systemd タイマーの例:

# /etc/systemd/system/cert-watch.service
[Unit]
Description=TLS certificate expiry check

[Service]
Type=oneshot
EnvironmentFile=/etc/echobell.env
ExecStart=/usr/local/bin/cert-watch example.com api.example.com mail.example.com:993
# /etc/systemd/system/cert-watch.timer
[Unit]
Description=Daily TLS certificate expiry check

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true は重要です。これがないと、予定時刻に電源が入っていなかったマシンはその回を単純に飛ばします。

次に点検そのものの輪を閉じます。Type=oneshot のユニットが非ゼロで終了すると OnFailure= が発火するので、drop-in を 1 つ置くだけで壊れた更新が自分から声を上げるようになります。

# /etc/systemd/system/certbot.service.d/echobell.conf
[Unit]
OnFailure=echobell-alert@%n.service
# /etc/systemd/system/echobell-alert@.service
[Unit]
Description=Echobell alert for %i

[Service]
Type=oneshot
EnvironmentFile=/etc/echobell.env
ExecStart=/usr/local/bin/echobell-notify %i

/usr/local/bin/echobell-notify は 3 行です。

#!/usr/bin/env bash
curl -sS -m 10 -X POST "$ECHOBELL_CERT_HOOK" \
  -H 'content-type: application/json' \
  -d "{\"host\":\"$(hostname -f)\",\"state\":\"renewal-failed\",\"unit\":\"$1\",\"daysLeft\":-1}"

certbot renew は、いずれかの更新が失敗すると非ゼロで終了します。これはまさに欲しい信号で、そしてまさに現状どこにも届いていない信号です。なお certbot の --deploy-hook成功時にしか動かないため、この用途には使えません。失敗経路はユニット側から取る必要があります。

ステップ 4 —— 条件ではしごを分ける

2 つのチャンネルは同じペイロードを受け取り、どちらが実際に発火するかは条件が決めます。時間指定チャンネル:

state == "expiring" && daysLeft > 3

通話チャンネル:

state == "expired" || state == "unreachable" || state == "renewal-failed" || daysLeft <= 3

<= は両辺を Number() で変換するので、daysLeft を文字列で送っても数値として比較されます。条件には「含む」演算子がないため、パターンマッチが要る自由文ではなく、明示的な state フィールドを送っているわけです。

ステップ 5 —— 信じる前にテストする

証明書が壊れていると分かっているホストにスクリプトを向け、アラートが届くのを確認します。

ECHOBELL_CERT_HOOK="https://hook.echobell.one/t/<token>" cert-watch expired.badssl.com

このとき、実際に受け取る端末でおやすみモードを有効にしたうえで行い、アプリの失敗した通話を再試行をオンにして、集中モードに抑えられた着信が再度かかるようにしてください。一度も発火させたことのないエスカレーション経路は、ただの推測です。

監視ツールがすでに証明書を見ている場合は?

既存の Webhook をチャンネルにつなげば、スクリプトは不要です。 たいていの監視はすでに期限を知っています。足りないのは、眠っている人間を越えて届く経路のほうです。

  • Uptime Kuma には証明書期限の通知が組み込まれています。チャンネルに向けるだけです(Uptime Kuma ガイド電話通知の設定)。
  • Prometheus + Alertmanager に blackbox exporter を組み合わせると probe_ssl_earliest_cert_expiry が得られます。それにアラートを設定してチャンネルへルーティングします(Prometheus ガイドAlertmanager の電話通知)。
  • Grafana のアラートルールはチャンネルへ直接 POST できます(Grafana ガイド)。
  • UpptimeUptimeRobot は公開エンドポイントをカバーします(UpptimeUptimeRobot)。
  • メールしか送ってこないもの——CA 自身のポータル、クラウド事業者の ACM 通知、社内 PKI——は転送ルールで扱います。チャンネルごとに専用アドレスがあり、fromtosubjecttexthtml を変数として使えます(メールトリガー)。

これらのどれも代わりにならない点が 1 つあります。証明書を配信しているマシンの外側から点検を走らせることです。監視が同じホストに住んでいると、ホストを落とす障害はアラートも一緒に連れていきます。

Echobell がやらないこと

ここは正確に書く価値があります。証明書管理は、これよりずっと多くをこなす製品であふれた分野だからです。

Echobell がやること: Webhook やメールを通常プッシュ・時間指定通知・着信に変える。条件で絞り込む。テンプレートで整形する。共有チャンネルの購読者全員に同じトリガーを届け、各自が緊急度を選べるようにする。

Echobell がやらないこと:

  • 証明書の発見やインベントリ。 ネットワークをスキャンせず、証明書透明性ログも巡回せず、誰も発行を覚えていない証明書について教えてもくれません。上のスクリプトは、あなたが並べたホストしか見ません。証明書の氾濫は実在する問題ですが、これはその解ではありません。
  • 更新そのもの。 ACME クライアントも持たず、鍵にも触れません。更新が壊れたことは伝えますが、直すのは引き続きあなたの仕事です。
  • 自前のスケジュールでの巡回。 ホスト型のプローブはありません。見に行く役目は、あなたが動かす何か——タイマー、CI ジョブ、既存の監視——が担います。
  • オンコールのローテーション、エスカレーションポリシー、確認応答。 「5 分で誰も出なければ次の人に電話」はありません。それが必要ならインシデント管理プラットフォームが必要です——Opsgenie 代替の比較を参照してください。
  • 配信の保証。 着信はプッシュ基盤・ネットワーク・充電された端末に依存します。壊れてから気づくまでの距離を縮めるものであって、絶対的に寄りかかれる管理策ではありません。

よくある質問

Let's Encrypt は本当に期限切れメールをやめたのですか?

はい。期限切れ通知サービスは 2025 年 6 月 4 日に終了し、発行記録に紐づけて保持していたメールアドレスも削除されました。告知ではサードパーティの監視が推奨され、選択肢の 1 つとして 250 枚まで無料の Red Sift Certificates Lite が挙げられています。1 年以上 Let's Encrypt の期限警告を受け取っていないなら、理由はこれです。何も期限に近づかなかったからではありません。

2026 年の TLS 証明書の有効期間は?

2026 年 3 月 15 日以降、公的に信頼される TLS 証明書は最長 200 日です。SC-081v3 により、2027 年 3 月 15 日に 100 日、2029 年 3 月 15 日に 47 日へ下がります。各 CA は安全のため上限より短く発行します。たとえば DigiCert は「許容される最大有効期間を超えないため」に 199 日で発行しています。Let's Encrypt は独自に、より速く先へ進んでおり、6 日間証明書が 2026 年 1 月から一般提供されています。

ARI を使っていても期限アラートは要りますか?

必要ですが、対象イベントが変わります。ARI(RFC 9773)は ACME クライアントにいつ更新すべきかを伝えるので、ハードコードされた間隔という故障要因は完全に消えます。しかし、更新が成功することも、サービスがリロードされることも、クライアントがまだ生きていることも保証しません。もう自分で管理する必要のないカウントダウンではなく、「更新の連続失敗」と「配信中の証明書がディスク上のものと食い違っていること」にアラートを設定してください。

Web サーバー以外の証明書はどうしますか?

そうした証明書こそ最も期限切れになりやすいものです。ACME クライアントが見ておらず、何かが壊れるまでブラウザも文句を言わないからです。スクリプトは host:port を受け取るので、993 の IMAP、636 の LDAPS、9093 の Kafka ブローカー、8443 の社内 API も同じ書き方で扱えます。ソケットに一度も現れない証明書——コード署名、プッシュ通知証明書、端末群のクライアント証明書——は、それが置かれている場所から日付を取り出し、同じチャンネルへ POST してください。

毎日の点検はノイズになりませんか?

異常がないときに黙っていればなりません。だからこのスクリプトはしきい値より上では何も送りません。ノイズになるのはむしろ「証明書は正常です」と毎日報告する設計のほうです。2 週間で誰も読まなくなり、それが届かなくなった日に誰も気づきません。ハートビートが欲しいなら、別の通常チャンネルに置き、鳴るチャンネルには絶対に置かないでください。この議論の一般形はアラート疲れの解消にあります。

チーム全員が証明書アラートを受け取れますか?

はい。チャンネルを共有すれば購読者全員にトリガーが届き、各自が通知タイプを選べます。うまくいく分け方の一例:当番の人は通話チャンネル、他のメンバーは時間指定チャンネルを購読する。こうすれば午前 3 時の期限切れで起きるのは 6 人ではなく 1 人です。

macOS でも動きますか?

動きます。ただし 1 点だけ、BSD の date-d を受け付けません。そのため days_leftto_epoch はまず GNU 形式を試し、date -u -j -f にフォールバックします。macOS の openssl は既定で LibreSSL ですが、-checkend-fingerprint は同じように使えます。Homebrew で OpenSSL を入れても変わりません。

通知に証明書の情報を載せても安全ですか?

ここで使っているフィールド——ホスト名、期限日、発行者——はいずれも公開情報で、同じ openssl コマンドで誰でもあなたのサーバーから読み取れます。秘密鍵、内部パス、非公開ホスト上の証明書についてホスト名を超える情報などをペイロードに足さないでください。Echobell は通知の内容と履歴を端末上にのみ保存し、サーバーにはアカウント・チャンネル・購読関係だけを置きます(プライバシーモデル)。良い既定値ですが、必要以上に送ってよい理由にはなりません。


関連記事