TL;DR
SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE が発生している原因は、サーバーのチェーンに含まれる中間CA証明書のいずれかが期限切れになっているためです。CAが新しい中間証明書を発行したか、誤った中間証明書を提供している可能性があります。手順の概要:
- CAから新しい中間証明書バンドルをダウンロードする。
- サーバーの設定を更新して、その証明書を使用するようにする。
- Webサーバーをリロード/再起動する。
Let's Encryptを使用していて2021年9月以降にこの問題が発生した場合、期限切れのDST Root CA X3クロス署名チェーンを削除する必要がある可能性が高いです。続きをお読みください。
実際に何が起きているのか
ブラウザがTLS証明書を検証する際、チェーンをたどっていきます:リーフ証明書 → 1つ以上の中間CA → 信頼されたルートCA。チェーン内のいずれかの中間証明書のnotAfterの日付が過去のものであれば、Firefoxは接続を受け入れずSEC_ERROR_EXPIRED_ISSUER_CERTIFICATEをスローします。
よくある原因:
- CAが中間証明書をローテーションしたが、サーバーのバンドルを更新していなかった。
- Let's EncryptのDST Root CA X3クロス署名チェーンが期限切れになった(2021年9月30日)— 一部の設定では古いチェーンが引き続き提供されている。
- デプロイ時に特定の中間証明書をピン留めしており、気づかないうちに期限が切れていた。
- 内部PKIの中間証明書が期限切れになり、Firefoxがブロックし始めるまで誰も気づかなかった。
まず診断する — 推測しない
何かを変更する前に、以下を実行してください:
# サーバーが実際に送信している完全なチェーンを確認する
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null | openssl x509 -noout -text | grep -A2 'Validity'
# より詳しく:チェーン内の全証明書と有効期限を確認する
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null \
| awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' \
| csplit -z -f cert- - '/-----BEGIN CERTIFICATE-----/' '{*}' 2>/dev/null
for f in cert-*; do
echo "=== $f ==="
openssl x509 -noout -subject -issuer -dates -in "$f"
done
各証明書のnotAfterの行を確認してください。過去の日付になっているものが問題の原因です。
素早く確認したい場合はcurlで:
curl -vI https://yourdomain.com 2>&1 | grep -E 'expire|SSL|certificate'
修正1 — 中間証明書バンドルを更新する(最も一般的)
CAのWebサイトから最新の中間証明書を取得してください。Let's Encryptの場合:
# 有効なISRG Root X1チェーンをダウンロードする(期限切れのDSTクロス署名版ではない)
wget https://letsencrypt.org/certs/lets-encrypt-r3.pem -O /etc/ssl/intermediate.pem
# またはフルチェーン(中間証明書 + ルート)
wget https://letsencrypt.org/certs/lets-encrypt-r3-cross-signed.pem
商用CAの場合は、サポートポータルからバンドルを取得してください。通常「intermediate certificate」または「CA bundle」というラベルが付いています。
Apache
# VirtualHostブロック内
SSLCertificateFile /etc/ssl/certs/yourdomain.crt
SSLCertificateKeyFile /etc/ssl/private/yourdomain.key
SSLCertificateChainFile /etc/ssl/intermediate.pem
# リロード前に設定をテストする
apachectl configtest
systemctl reload apache2
Nginx
# Nginxはリーフ証明書と中間証明書を1つのファイルにまとめる必要がある
cat yourdomain.crt intermediate.pem > /etc/ssl/certs/yourdomain_bundle.crt
# serverブロック内
ssl_certificate /etc/ssl/certs/yourdomain_bundle.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
nginx -t
systemctl reload nginx
HAProxy
# HAProxyは秘密鍵・証明書・チェーンをすべて1つのPEMにまとめる必要がある
cat yourdomain.key yourdomain.crt intermediate.pem > /etc/haproxy/yourdomain.pem
# frontend/backendセクション内
bind *:443 ssl crt /etc/haproxy/yourdomain.pem
systemctl reload haproxy
修正2 — Let's Encrypt固有の問題(DST Root CA X3の期限切れ)
certbotが期限切れの旧DST Root CA X3クロス署名チェーンを使って証明書を生成し続けている場合、ISRG Root X1の優先チェーンを使用するよう強制してください:
# 期限切れでないチェーンで強制更新する
certbot renew --preferred-chain "ISRG Root X1" --force-renewal
# または /etc/letsencrypt/cli.ini に恒久的に追加する
preferred-chain = ISRG Root X1
その後、バンドルを再構築してサーバーをリロードしてください。
修正3 — 内部PKI(自己管理CA)
独自のCAを運用している場合、新しい中間証明書を発行し、配布して、フリート内のすべてのサーバーを更新する必要があります。手順:
# 新しい中間証明書を生成する(今回は有効期限5年)
openssl genrsa -out intermediate.key 4096
openssl req -new -key intermediate.key -out intermediate.csr
# ルートCAで署名する
openssl x509 -req -in intermediate.csr \
-CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-out intermediate.crt -days 1825 -sha256 \
-extfile intermediate_ext.cnf
# intermediate.crt を全サーバーに配布する
ansible all -m copy -a "src=intermediate.crt dest=/etc/ssl/intermediate.crt" \
&& ansible all -m service -a "name=nginx state=reloaded"
修正を確認する
リロード後、チェーンが正常であることを確認してください:
# すべての日付が未来の日付になっているはず
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null \
| openssl x509 -noout -dates
# SSL Labsでフルチェーンのグレードを確認する(AまたはA+)
# https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com
# またはtestssl.shをローカルで使用する
./testssl.sh --chain yourdomain.com
Firefoxでハードリロード(Ctrl+Shift+R)後にDevTools → セキュリティタブを開いてください。証明書の警告なしに「接続は安全です」と表示されるはずです。
次回の予防策
- 中間CAの
notAfterの60日前にカレンダーリマインダーを設定してください。 - チェーン内の証明書が90日以内に期限切れになる場合にアラートを出すcronジョブを追加してください:
# /etc/cron.weekly/check-chain-expiry
#!/bin/bash
HOST="yourdomain.com"
EXPIRY=$(openssl s_client -connect $HOST:443 -showcerts 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 ))
[ $DAYS -lt 90 ] && echo "WARNING: chain cert on $HOST expires in $DAYS days" | mail -s "Cert Expiry Alert" ops@yourcompany.com
- Certbotの自動更新タイマー(
systemctl status certbot.timer)を使用し、実際に動作していることを確認してください。 - UptimeRobotやChecklyなどの外部サービスで監視してください。ユーザーより先に期限切れを検知できます。

