ツール

SSL証明書の残り日数を出すスクリプトを書いたら、残り -20670日 という値が返りました。有効期限は数ヶ月先なので明らかにおかしい数字です。原因は macOS の date コマンドが Oct のような英語の月名をパースできないことでした。日本語ロケールで動かしていると %b が日本語の月表記を期待するためです。LC_TIME=C を付けると解決します。

残り日数がマイナス2万日になった

最初に書いたのはこういうコードです。openssl で有効期限を取り出し、date でエポック秒に変換して差を日数にする、という素直な作りです。

for d in example.com; do
  END=$(openssl s_client -connect $d:443 -servername $d </dev/null 2>/dev/null \
    | openssl x509 -noout -enddate | cut -d= -f2)
  DAYS=$(( ( $(date -j -f "%b %d %T %Y %Z" "$END" +%s 2>/dev/null) - $(date +%s) ) / 86400 ))
  echo "$d: $END (残り ${DAYS}日)"
done

結果はこうなりました。

example.com: Oct 21 08:49:07 2026 GMT (残り -20670日)

有効期限の表示自体は正しく取れています。壊れているのは日数計算の部分です。

種明かしをすると、date -j -f がパースに失敗して空文字列を返していました。2>/dev/null でエラーを捨てていたので失敗が見えず、算術式の中で空文字列が0として扱われた結果、0 - 現在時刻 の計算になっていました。1970年からの経過秒数を日数にすると約2万日なので、それがマイナスで出ていたわけです。

エラー出力を捨てるとこうなります。デバッグ中は 2>/dev/null を外すのが先でした。

GMTを取り除いても直らなかった

最初に疑ったのは末尾の GMT です。%Z がタイムゾーン名を解釈できていないと考えて、文字列から削ってみました。

END="Oct 21 08:49:07 2026 GMT"
TZ=UTC date -j -f "%b %d %T %Y" "${END% GMT}" +%s
Failed conversion of ``Oct 21 08:49:07 2026'' using format ``%b %d %T %Y''
date: illegal time format

まだ失敗します。${END% GMT} で末尾を削れているのは出力からも確認できるので、GMT は原因ではありませんでした。

次に %T(時刻の短縮指定)を疑って %H:%M:%S に展開してみましたが、これも同じエラーです。フォーマット指定を1つずつ削って、どこで落ちているかを絞りました。

TZ=UTC date -j -f "%T" "08:49:07" +%s
1785919747

TZ=UTC date -j -f "%b %d" "Oct 21" +%s
Failed conversion of ``Oct 21'' using format ``%b %d''

%T は単体なら通ります。%b %d が通りません。つまり月名の解釈で失敗しているという切り分けができました。

原因はロケールだった

%b は「月名の省略形」ですが、これはロケールに依存します。手元の環境を確認するとこうなっていました。

locale
LANG="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_CTYPE="ja_JP.UTF-8"

日本語ロケールで %b が何を出すか見ると、答えが出ます。

date "+%b"
 8

日本語ロケールでの月名省略形は 8 という数字表記です。英語の Oct とは一致しないので、パースが失敗していました。openssl は常に英語の月名で日付を出すため、日本語ロケールの date との組み合わせで必ず衝突します。

LC_TIME=C を付けると、時刻の解釈だけをCロケール(英語)に切り替えられます。

LC_TIME=C TZ=UTC date -j -f "%b %d %T %Y" "Oct 21 08:49:07 2026" +%s
1792572547

通りました。LC_ALL=C でも動きますが、こちらは全カテゴリを切り替えるので、必要な範囲に絞る LC_TIME=C のほうが影響が小さくて済みます。

日が1桁のときのスペース2個詰めも通る

openssl の日付出力は、日が1桁だとスペースが2個入って Oct 1 のようになります。これも %d で読めるか確認しておきました。

for END in "Oct 21 08:49:07 2026 GMT" "Oct  1 08:49:07 2026 GMT" "Dec  5 23:11:02 2026 GMT"; do
  EPOCH=$(LC_TIME=C TZ=UTC date -j -f "%b %d %T %Y" "${END% GMT}" +%s 2>/dev/null)
  echo "[$END] -> epoch=$EPOCH"
done
[Oct 21 08:49:07 2026 GMT] -> epoch=1792572547
[Oct  1 08:49:07 2026 GMT] -> epoch=1790844547
[Dec  5 23:11:02 2026 GMT] -> epoch=1796512262

3件すべて変換できています。スペースの数は問題にならないので、ロケールだけ揃えれば足ります。

閾値の判定はopensslに任せる

「30日以内に失効するか」を知りたいだけなら、日数計算をせずに openssl-checkend を使えます。指定した秒数の先まで有効かどうかを終了コードで返してくれます。

openssl x509 -noout -checkend 2592000

2592000秒は30日です。有効なら終了コード0、期限内に切れるなら1が返ります。日付のパースを自分でやらないので、ロケールの問題を避けられます。

残り日数の表示と閾値判定を両方やる形にまとめると、最終的にこうなりました。

for d in example.com; do
  CERT=$(openssl s_client -connect $d:443 -servername $d </dev/null 2>/dev/null)
  END=$(echo "$CERT" | openssl x509 -noout -enddate | cut -d= -f2)
  EPOCH=$(LC_TIME=C TZ=UTC date -j -f "%b %d %T %Y" "${END% GMT}" +%s 2>/dev/null)
  DAYS=$(( (EPOCH - $(date +%s)) / 86400 ))
  if echo "$CERT" | openssl x509 -noout -checkend 2592000 >/dev/null 2>&1; then
    echo "✅ $d: $END (残り${DAYS}日)"
  else
    echo "⚠️ $d: $END (残り${DAYS}日) 30日以内に失効"
  fi
done

実行結果です。

✅ example.com: Oct 21 08:49:07 2026 GMT (残り76日)

s_client の結果を変数に入れて2回使い回しているのは、接続を1回で済ませるためです。-enddate-checkend で別々に繋ぐと接続が2回発生します。

この手のバグを早く見つけるために

今回いちばん時間を無駄にしたのは、2>/dev/null でエラーを隠していたことです。パースが失敗している事実がずっと見えず、出力された数字だけを見て原因を推測していました。空文字列が算術式で0になる仕様と組み合わさると、エラーではなく「もっともらしく間違った値」が出てくるので厄介です。

コマンド置換の結果を計算に使うときは、空になる可能性を先に潰しておくと安全です。

EPOCH=$(LC_TIME=C TZ=UTC date -j -f "%b %d %T %Y" "${END% GMT}" +%s 2>/dev/null)
if [ -z "$EPOCH" ]; then
  echo "⚠️ 日付のパースに失敗: $END"
  exit 1
fi

空チェックを入れておけば、次に同じ問題を踏んだときは原因が即座に分かります。日付の扱いは環境差が出やすい領域なので、変換が失敗しうる前提でスクリプトを書くのが現実的です。