【Linux】SSL証明書の有効期限確認と自動更新スクリプト自作ガイド|openssl・certbot・cron連携

スクリプト設計実務レシピ

年末年始や大型連休の直前、ふと「あのサーバーのSSL証明書、いつ切れるんだっけ?」と冷や汗をかいた経験はありませんか?

WebサイトのSSL/TLS証明書失効は、ブラウザでの警告画面表示やサービス停止に直結する重大インシデントです。しかし、手作業でカレンダーにメモして更新日を管理するのは消耗するだけであり、担当者の異動や休暇が重なれば必ず漏れが生じます。

「手作業は負け、スクリプトはシンプルに」が運用の鉄則です。

本記事では、Linux環境で今すぐ使える `openssl` コマンドによる証明書期限の即時確認ワンライナーから、残り日数を自動計算してSlackへ警告を飛ばす堅牢なBash監視スクリプト、そして `certbot` と `cron` を連携させた完全自動更新の仕組みまで徹底解説します。

—

## 【今すぐ使える】SSL証明書の有効期限を確認する1行コマンド

まずは、目の前のサーバーやWebサイトの証明書期限を今すぐ調べたいときに役立つ1行コマンドを紹介します。ブラウザの開発者ツールを開く必要はなく、ターミナルから一撃で確認できます。

### リモートサーバーのSSL証明書有効期限を確認する(openssl s_client)

稼働中のWebサーバーに対して外部からTLS接続を行い、適用されているSSL証明書の有効期限を取得するワンライナーです。

“`bash
# リモートサーバーのSSL証明書有効期限(notBefore / notAfter)を取得
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
“`

実行すると、以下のように証明書の有効開始日時(`notBefore`)と失効日時(`notAfter`)が出力されます。

“`text
notBefore=Apr 10 00:00:00 GMT
notAfter=Jul 9 23:59:59 GMT
“`

このコマンドのポイントは以下の3点です:

1. `echo |`: `openssl s_client` はデフォルトで対話型接続を待ち受けるため、標準入力に空行を流し込んで即座にコネクションを終了させています。
2. `-servername example.com`: SNI(Server Name Indication)を指定しています。1台のサーバー(同一IP)で複数のSSLサイトをホストしている現代のWeb環境では、このオプションを省略すると意図しないデフォルトドメインの証明書が返される原因になります。
3. `2>/dev/null`: ハンドシェイク時の詳細なデバッグ出力を破棄し、後続の `openssl x509` に必要な証明書データだけをパイプで渡しています。

終了日時(`notAfter`)だけをピンポイントで取得したい場合は、末尾を `-noout -enddate` に変更してください。

“`bash
# 終了日時(notAfter)のみを抽出
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate
“`

### サーバー上のローカル証明書ファイル(.crt / .pem)の期限を確認する

SSHログイン中のサーバー内部で、NginxやApacheが参照している証明書ファイルの実体(`.crt` や `.pem`)から直接期限を確認する場合は、`openssl x509` コマンドを直接実行します。

“`bash
# サーバー上の証明書ファイルから有効期限を確認
openssl x509 -in /etc/ssl/certs/example.com.crt -noout -dates
“`

実行結果はリモート確認時と同様に、`notBefore` と `notAfter` が返されます。

“`text
notBefore=Jan 15 09:00:00 GMT
notAfter=Apr 15 09:00:00 GMT
“`

Let’s Encryptを利用している場合、実際の証明書ファイルはシンボリックリンク経由で `/etc/letsencrypt/live/example.com/fullchain.pem` などに配置されています。そのパスを `-in` に指定すれば同様に確認可能です。

“`bash
# Let’s Encrypt の証明書期限を確認
sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate
“`

実務では、「Webサーバーの設定ファイルが古い証明書パスを指していないか」「証明書を更新したのにWebサーバーの再読込を忘れていないか」を確認するために、このローカルファイル確認と先ほどのリモート接続確認を突き合わせるのが鉄則です。

—

## openssl x509コマンドの主要オプション早見表

SSL証明書の調査・運用では、有効期限以外にも発行元CAやドメイン名(SANs)、シリアル番号の確認が頻繁に発生します。日常業務で使う主要オプションを早見表にまとめました。

### 有効期限・発行者・ドメイン名取得オプション一覧(-enddate, -dates, -subject, -issuer, -checkend)

| オプション | 機能概要 | 実用コマンド例 |
| :— | :— | :— |
| `-enddate` | 証明書の有効期限終了日時(`notAfter`)を表示 | `openssl x509 -in cert.pem -noout -enddate` |
| `-startdate` | 証明書の有効期限開始日時(`notBefore`)を表示 | `openssl x509 -in cert.pem -noout -startdate` |
| `-dates` | 有効期限の開始日時と終了日時の両方を表示 | `openssl x509 -in cert.pem -noout -dates` |
| `-subject` | 証明書のサブジェクト情報(ドメイン名・CN等)を表示 | `openssl x509 -in cert.pem -noout -subject` |
| `-issuer` | 証明書を発行した認証局(CA)情報を表示 | `openssl x509 -in cert.pem -noout -issuer` |
| `-text` | 証明書の全メタデータを人間が読める形式で完全表示 | `openssl x509 -in cert.pem -noout -text` |
| `-serial` | 証明書のシリアル番号を表示 | `openssl x509 -in cert.pem -noout -serial` |
| `-fingerprint` | 証明書のフィンガープリント(デフォルトSHA-1)を表示 | `openssl x509 -in cert.pem -noout -fingerprint -sha256` |
| `-checkend <秒>` | 指定秒数以内に期限が切れるかを終了ステータスで判定 | `openssl x509 -in cert.pem -noout -checkend 2592000` |

#### スクリプト分岐で大活躍する `-checkend` オプション

この中で特にシェルスクリプトで強力なのが `-checkend <秒数>` オプションです。

指定した秒数以内に証明書が切れる場合、コマンドは終了コード `1`(偽)を返し、まだ有効期限内であれば終了コード `0`(真)を返します。面倒な日付計算を行わずに条件分岐を作りたい場合、このオプションが最短の解になります。

“`bash
# 30日(86400秒 × 30 = 2592000秒)以内に期限切れになるかチェック
if ! openssl x509 -in /etc/ssl/certs/example.com.crt -noout -checkend 2592000 > /dev/null 2>&1; then
echo “【警告】証明書の有効期限まで残り30日を切っています!”
else
echo “【正常】証明書は30日以上有効です。”
fi
“`

ただし、実務では「あと正確に何日残っているのか」を通知メッセージに含めたいケースがほとんどです。次のセクションでは、残り日数を算出してSlack通知まで完結させるBashスクリプトを構築します。

—

## 実務で使えるSSL証明書監視・自動化スクリプト集

ここからは、複数ドメインの監視、残り日数の算出、Slack通知、そしてLet’s Encryptの自動更新設定まで、現場でそのまま使えるスクリプトと設定例を紹介します。

### 証明書の残日数を自動計算し30日未満で警告するBashスクリプト

稼働中の複数Webサイトをリモートから一括チェックし、残日数を算出する堅牢なスクリプトです。エラーハンドリングとして `set -euo pipefail` を定義し、変数はすべてダブルクォートで保護しています。

“`bash
#!/usr/bin/env bash
set -euo pipefail

# ==============================================================================
# SSL証明書 有効期限監視スクリプト
# ==============================================================================

# チェック対象ドメイン一覧
TARGET_DOMAINS=(
“example.com”
“api.example.com”
“shop.example.com”
)

# 警告閾値(日数)
WARNING_DAYS=30
CRITICAL_DAYS=7

# 現在日時のUNIXエポック秒
NOW_EPOCH=$(date +%s)

echo “=== SSL証明書 有効期限チェック開始 ===”

for domain in “${TARGET_DOMAINS[@]}”; do
# リモート接続で終了日時(notAfter)を取得
expire_date_raw=$(echo | openssl s_client -connect “${domain}:443” -servername “${domain}” 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null || true)

if [[ -z “${expire_date_raw}” ]]; then
echo “[ERROR] ${domain}: 証明書情報の取得に失敗しました(接続不可またはSSL未設定)”
continue
fi

# “notAfter=” プレフィックスを削除
expire_date=”${expire_date_raw#notAfter=}”

# 終了日時のUNIXエポック秒を計算
expire_epoch=$(date -d “${expire_date}” +%s)

# 残り秒数および日数を算出
diff_seconds=$(( expire_epoch – NOW_EPOCH ))
remaining_days=$(( diff_seconds / 86400 ))

if (( remaining_days <= CRITICAL_DAYS )); then echo "[CRITICAL] ${domain}: 残り ${remaining_days} 日 (失効日: ${expire_date})" elif (( remaining_days <= WARNING_DAYS )); then echo "[WARNING] ${domain}: 残り ${remaining_days} 日 (失効日: ${expire_date})" else echo "[OK] ${domain}: 残り ${remaining_days} 日 (失効日: ${expire_date})" fi done echo "=== チェック終了 ===" ``` ### 期限切れ直前にSlack/Webhookへアラート通知するスクリプト(curl連携) 上記のロジックをベースに、残り日数が閾値を下回った場合のみSlack Incoming Webhookへ自動通知する関数を追加した完全版スクリプトです。 ```bash #!/usr/bin/env bash set -euo pipefail # ============================================================================== # SSL証明書 有効期限監視&Slackアラート通知スクリプト # ============================================================================== # Slack Incoming Webhook URL SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" # 監視対象ドメイン一覧 TARGET_DOMAINS=( "example.com" "api.example.com" ) # 閾値設定(日数) WARNING_DAYS=30 CRITICAL_DAYS=7 NOW_EPOCH=$(date +%s) # Slack通知送信用関数 send_slack_alert() { local domain="$1" local days="$2" local expire_date="$3" local level="$4" local color="" if [[ "${level}" == "CRITICAL" ]]; then color="#danger" else color="#warning" fi local payload payload=$(cat < /dev/null
}

for domain in “${TARGET_DOMAINS[@]}”; do
expire_date_raw=$(echo | openssl s_client -connect “${domain}:443” -servername “${domain}” 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null || true)

if [[ -z “${expire_date_raw}” ]]; then
# 接続エラー時の通知
payload=$(cat < /dev/null
continue
fi

expire_date=”${expire_date_raw#notAfter=}”
expire_epoch=$(date -d “${expire_date}” +%s)
diff_seconds=$(( expire_epoch – NOW_EPOCH ))
remaining_days=$(( diff_seconds / 86400 ))

if (( remaining_days <= CRITICAL_DAYS )); then send_slack_alert "${domain}" "${remaining_days}" "${expire_date}" "CRITICAL" elif (( remaining_days <= WARNING_DAYS )); then send_slack_alert "${domain}" "${remaining_days}" "${expire_date}" "WARNING" fi done ``` このスクリプトを設置しておけば、手作業による確認を一切行うことなく、異常時や更新時期のみSlackに通知が届くクリーンな運用体制が完成します。 ### Let's Encrypt(certbot)の自動更新とWebサーバー再読込設定(--deploy-hook) Let's Encryptを採用しているWebサーバーでは、`certbot` を用いて自動更新を行います。しかし、現場で非常によく見かける致命的なミスがあります。 それは、「certbot renew で証明書ファイル自体は新しくなったのに、Webサーバー(Nginx / Apache)を再読込していないため、古いメモリ上の証明書が配信され続けて失効する」というトラブルです。

これを完全に防ぐには、証明書の更新が成功したタイミングでのみWebサーバーを安全にリロードする `–deploy-hook` オプションを使用します。

“`bash
# Nginx環境: 更新成功時のみ graceful reload を実行
sudo certbot renew –deploy-hook “systemctl reload nginx”

# Apache環境: 更新成功時のみ graceful reload を実行
sudo certbot renew –deploy-hook “systemctl reload apache2”
“`

#### なぜ `–post-hook` ではなく `–deploy-hook` なのか?

`certbot` には類似のフックオプションとして `–post-hook` も存在しますが、挙動が異なります:

– `–post-hook`: 更新の成否や更新の有無にかかわらず、`certbot renew` コマンドが終了するたびに毎回実行される。
– `–deploy-hook`: 証明書が実際に新しく更新された場合のみ実行される。

不要なWebサーバーのリロードを避け、安全かつ確実に新証明書を反映させるには `–deploy-hook` の利用が鉄則です。

設定を変更したら、必ず `–dry-run` オプションでシミュレーションテストを実施してください。

“`bash
# 本番更新を行わずに更新フックの動作をシミュレーション検証
sudo certbot renew –dry-run
“`

### cronへの安全な定期実行スケジュール登録例(毎週月曜日の定期チェック)

作成した監視スクリプトや証明書更新コマンドは、`cron` に登録して無人運用します。

実務では、毎日実行すると通知過多になりアラートが形骸化(オオカミ少年化)しやすいため、「毎週月曜日の朝9時」などに設定するのがおすすめです。

“`bash
# rootユーザーのcrontabを編集
sudo crontab -e
“`

以下の設定を登録します:

“`cron
# PATHを明示的に定義(cronのデフォルトPATH不足によるコマンド未検出を防止)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# 毎週月曜日の午前9時00分に証明書監視スクリプトを実行
0 9 * * 1 /usr/local/bin/ssl_cert_checker.sh >> /var/log/ssl_cert_checker.log 2>&1

# 毎日午前3時30分に certbot の自動更新を試行(有効期限30日未満の証明書のみ自動更新される)
30 3 * * * certbot renew –deploy-hook “systemctl reload nginx” –quiet
“`

#### cron運用の鉄則チェックリスト

1. `PATH` の明示: `cron` は最小限の環境変数で動作するため、`openssl` や `curl` のパスが通らずに失敗することがあります。必ずcrontab冒頭に `PATH` を宣言してください。
2. 標準出力と標準エラー出力のリダイレクト: `>> /var/log/… 2>&1` を指定してログを残し、サイレント障害を防ぎます。
3. `–quiet` オプションの活用: 自動更新が成功した際や更新対象がない平常時はログを汚さないよう、`certbot renew` には `–quiet` を付与します。

—

## 現場でハマるよくあるエラーとトラブルシューティング

SSL証明書の確認や自動更新を自動化する際、現場でエンジニアが直面しやすい4大トラブルとその解決策を解説します。

### SNI(-servername)未指定による誤ったデフォルト証明書の取得

`openssl s_client` で有効期限を確認した際、「まだ期限があるはずなのに、なぜか別サイトの証明書情報が返ってくる」「期限切れと判定される」という現象に遭遇することがあります。

“`bash
# 【NG例】SNIを指定していない
openssl s_client -connect sub.example.com:443 2>/dev/null | openssl x509 -noout -subject
“`

このコマンドを実行すると、`sub.example.com` ではなく、Webサーバー側でデフォルト設定されている `default.example.com` の証明書が返されてしまうことがあります。

現代のWebサーバーは1つのグローバルIPアドレスで数十〜数百のバーチャルホストを運用しています。クライアントがTLSハンドシェイク時に「どのドメイン宛の接続か」を伝える仕組みがSNI(Server Name Indication)です。

必ず `-servername <ドメイン名>` を付与してリクエストを送信してください。

“`bash
# 【OK例】SNIを明示的に指定して目的のバーチャルホストの証明書を取得
openssl s_client -connect sub.example.com:443 -servername sub.example.com 2>/dev/null | openssl x509 -noout -subject
“`

### タイムゾーン(UTC vs JST)による日付計算のズレ防止ロジック

`openssl x509 -dates` が出力する日付文字列は、常にGMT(UTC)基準です。

“`text
notAfter=Jul 9 23:59:59 GMT
“`

これを日本標準時(JST, UTC+9)の現在日時と比較する際、文字列のまま単純比較したり、時差の計算を手動で行うと9時間のズレが生じ、当日の失効判定を誤るリスクがあります。

この問題を最もシンプルかつ安全に解消する方法が、「UNIXエポック秒(1970-01-01 00:00:00 UTC からの経過秒数)への変換」です。

GNU coreutils の `date` コマンド(Linux標準)は、末尾の `GMT` を認識して自動的にUTC基準のエポック秒へと変換してくれます。

“`bash
# GMT表記の日付文字列をエポック秒に変換
expire_epoch=$(date -d “Jul 9 23:59:59 GMT” +%s)

# 現在日時のエポック秒を取得
now_epoch=$(date +%s)

# 差分(秒数)を計算
diff=$(( expire_epoch – now_epoch ))
echo “残り日数: $(( diff / 86400 )) 日”
“`

すべてエポック秒(絶対時間)に変換してから引き算を行うことで、タイムゾーンやサマータイムの影響を一切受けずに正確な残日数を計算できます。

### 中間証明書の更新漏れとブラウザでの証明書エラー(SEC_ERROR_UNKNOWN_ISSUER)

証明書を更新した直後、社内PCのChromeでは正常に表示されているのに、スマートフォンのブラウザや外部API連携クライアントから「SEC_ERROR_UNKNOWN_ISSUER」や「NET::ERR_CERT_AUTHORITY_INVALID」というエラーが報告されるトラブルがあります。

この原因の多くは、サーバー証明書のみを配置し、中間CA証明書(Intermediate CA Certificate)の連結を忘れていることです。

デスクトップ版の主要ブラウザは過去にキャッシュした中間証明書を参照してエラーを回避することがあるため、管理者が自分のPCだけで動作確認すると見落としがちです。

Nginxの設定では、単体のサーバー証明書(`cert.pem`)ではなく、中間証明書が結合された `fullchain.pem` を指定しなければなりません。

“`nginx
# 【NG例】中間証明書が含まれていない単体証明書を指定
ssl_certificate /etc/ssl/certs/cert.pem;

# 【OK例】サーバー証明書と中間証明書が連結されたファイルを指定
ssl_certificate /etc/ssl/certs/fullchain.pem;
“`

商用SSL証明書を手動で結合する場合は、以下の順序で1つのファイルに連結します。

“`bash
# サーバー証明書を先頭に、中間証明書を末尾にして結合
cat example.com.crt intermediate.crt > /etc/ssl/certs/fullchain.pem
“`

### certbot renew が失敗する原因(80番ポート競合・DNS認証エラー)

`certbot renew` を自動化していても、サイレントに更新が失敗し続けて最終的に期限切れを迎えるケースがあります。頻出原因は以下の2つです:

1. standaloneモードによる80番ポート競合:
`certbot` を `standalone` モードで運用している場合、更新時に独自の一時Webサーバーを80番ポートで起動しようとします。すでに稼働中のNginxやApacheが80番ポートを占有していると、バインドに失敗して更新がストップします。

対策: Webrootプラグイン(`–webroot -w /var/www/html`)またはNginx/Apache専用プラグイン(`–nginx` / `–apache`)へ設定を切り替えてください。

2. DNS認証(DNS-01チャレンジ)のAPIトークン失効:
ワイルドカード証明書などでDNS認証を利用している場合、CloudflareやAWS Route53のAPIキーやアクセストークンの有効期限切れによって更新がブロックされます。

トラブルを未然に防ぐため、定期更新スクリプトとは別に前述のリモート監視スクリプトを動かしておくことが不可欠です。万が一 `certbot` の更新が失敗しても、残り30日の段階でSlackアラートが発報され、手動復旧の猶予を確実に確保できます。

—

## まとめ

Webサイトの安定運用において、SSL証明書の期限管理はインシデント防止の要です。

1. 即時確認はワンライナー: `echo | openssl s_client -connect :443 -servername 2>/dev/null | openssl x509 -noout -dates` でSNIを忘れずに即時チェック。
2. 監視の自動化: `openssl` と `date` コマンドで残日数をエポック秒から算出し、Slack通知スクリプトを `cron` に登録して無人化する。
3. 更新の自動化: Let’s Encrypt運用時は `–deploy-hook “systemctl reload nginx”` を指定し、新証明書のWebサーバーへの反映漏れを根絶する。

「いつか確認しよう」と人間の記憶に頼る運用は今日で終わりにしましょう。今すぐ監視スクリプトをサーバーに配備し、安心できる自動化基盤を整えてください。

—

## 関連記事

– [長期休暇前のサーバー点検・logrotate監視](/post/holiday-server-disk-maintenance/)
– [自動バックアップスクリプト自作ガイド](/post/server-auto-backup-rsync-cron/)
– [死活監視&Slack通知スクリプト](/post/bash-server-healthcheck-slack/)
– [Linuxのcron設定方法と自動実行の実務例](/post/cron/)

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする

コメント