証明書障害の90%はチェーンと期限切れだ
一言でいうと
証明書障害の大半は、暗号学ではなく、2つのことから生じます。中間証明書を付けていないチェーンと、誰も見ていなかった有効期限です。
なぜこれが問題なのか
この2つが危険な理由は、開発者のPCでは再現されない点です。ブラウザーは中間証明書をキャッシュしたり、自動的にダウンロードして補ってくれるため、チェーンが抜けていても、画面は問題なく表示されます。ところが、サーバー同士の連携(Javaクライアント、バッチ)は、そのような補正をしないため、そのまま失敗します。「私のブラウザーでは動くのですが」は、ここから出てきます。
期限切れは、もっと単純で、もっと痛いです。有効期間が過ぎた瞬間に全面障害で、明け方に始まり、対応はファイルの交換1つなのに、そのファイルを発行してもらうのに数日かかります。そのため、対応ではなく監視の問題です。残りの日数を数えるスクリプト1つとアラート1つで済む話を、毎年どこかで経験します。
SI現場で証明書に出会う瞬間
証明書は、普通このような形で届きます。顧客企業のセキュリティチームが、.pfxファイル1つとパスワードをメールで送ってきます。または、server.crt、server.key、chain.crtの3つのファイルが届きます。ときには、.cerが1つだけ届きます。そして「サービスインまでに適用をお願いします」と書かれています。
このとき必要な知識は、暗号学ではなく、ファイルが何を含んでいるかを確認する方法です。
# 인증서 내용 보기 (주체, 발급자, 유효기간, SAN)
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName
# 키와 인증서가 짝인지 확인 (두 해시가 같아야 함)
openssl x509 -in server.crt -noout -modulus | openssl md5
openssl rsa -in server.key -noout -modulus | openssl md5
# pfx 를 crt/key 로 분해
openssl pkcs12 -in cert.pfx -clcerts -nokeys -out server.crt
openssl pkcs12 -in cert.pfx -nocerts -nodes -out server.key
このコードブロックの韓国語コメントは、順に、証明書の内容を見る(サブジェクト、発行者、有効期間、SAN)、鍵と証明書がペアかを確認する(2つのハッシュが同じである必要がある)、pfxをcrt/keyに分解する、という意味です。
鍵と証明書がペアでないと、nginxは起動すらせず、key values mismatchという短いメッセージだけを残します。このコマンド2行を知っていれば、30秒で終わります。
チェーン: 開発者のPCでは動くのに、サーバー同士では動かない理由
証明書は、普通3段です。
Root CA (브라우저·OS 에 이미 들어 있음)
└─ Intermediate CA (중간 인증서)
└─ 서버 인증서 (우리 것)
サーバーは自分の証明書+中間証明書を一緒に送る必要があります。Rootは相手が持っています。中間証明書を抜かすと、どうなるでしょうか。
- ブラウザー: たいてい、うまくいきます。ブラウザーが以前に別のサイトから受け取った中間証明書をキャッシュしているか、AIA拡張で自動的に取得するからです。
- サーバー間通信(連携システムのHTTPクライアント、Javaの
HttpsURLConnection、curl): 失敗します。キャッシュもなく、AIAをたどらない実装が多いからです。
そのため、症状は次のように現れます。「開発者PCのブラウザーでは問題なく動くのに、連携相手のシステムでだけSSLエラーが出ます。」これがチェーン欠落の典型的な姿です。確認は1行で済みます。
echo | openssl s_client -connect api.example.com:443 \
-servername api.example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
1が出たら、チェーンがありません。正常なら2以上が出ます。
nginxは、ssl_certificateに、サーバー証明書 → 中間証明書の順につなげたファイルを渡します。順序が逆だとだめです。
cat server.crt intermediate.crt > fullchain.pem
SANがないと、最近のブラウザーは拒否する
昔は、CN(Common Name)にドメインを書いていました。今は、SAN(Subject Alternative Name)拡張がないと、最新のブラウザーが証明書を拒否します。CNは参考用になりました。
自己署名証明書を作るときに、SANを書き忘れて「なぜ動かないのか」を繰り返すことが多いです。開発/検証環境用のプライベートCAを作るときは、必ずSANを入れます。
subjectAltName = DNS:labhub.local, DNS:*.labhub.local, IP:127.0.0.1
期限切れ: 最もよくあり、最もあきれる障害
実際の事例を1つ挙げると、大規模なSSO移行プロジェクトで、SAML署名証明書の期限切れで47分間の全面ログイン障害が起きたことがあります。証明書の期限切れは、
- 予告なしには来ません(期限は最初からわかっていました)
- それなのに、担当者が変わり、アラートが迷惑メールフォルダーに行くと、誰も気づきません
そのため、期限の監視は、人ではなくスクリプトが行う必要があります。
openssl x509 -in server.crt -noout -enddate
# notAfter=Nov 12 09:00:00 2026 GMT
# 임계일 이내면 실패로 종료 (checkend 는 초 단위)
openssl x509 -in server.crt -noout -checkend $((30*86400)) \
|| echo "30일 이내 만료"
-checkendはほとんど知られていませんが、監視スクリプトを作るとき、まさにこの用途です。サーバー証明書だけでなく、連携相手の証明書、クライアント証明書、SAML署名証明書、コード署名証明書まで、一覧で管理する必要があります。一覧がなければ、必ず1つ見落とします。
nginxのTLS設定の実務の基本値
server {
listen 8443 ssl;
server_name labhub.local;
ssl_certificate /etc/nginx/tls/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
}
ssl_protocols TLSv1.2 TLSv1.3: TLS 1.0/1.1はすでに非推奨になっており、韓国の金融・公共機関のセキュリティ点検でも指摘の対象です。ただし、古い連携相手が1.0しか話せない場合が実際にあるので、そのときは、その相手専用のポートを別に開き、期限を決めておく形で処理します。全体を下げてはいけません。ssl_session_cache shared:SSL:10m: 10MBで、およそ4万セッションです。セッションの再利用ができれば、ハンドシェイクのコストが大きく減ります。ssl_prefer_server_ciphers off: TLS 1.3では、クライアントの好みを尊重するほうがよいというのが、現在の推奨です。
HTTP → HTTPSのリダイレクト
server {
listen 8088;
server_name labhub.local;
return 301 https://$host:8443$request_uri;
}
rewriteの代わりにreturn 301を使うことが推奨されます。より速く、意図が明確です。そして、リダイレクトを設定したなら、HSTSも一緒に検討します。ただし、HSTSは一度ブラウザーに記憶されると、元に戻すのが難しいので、内部システムでは、max-ageを短く始めて、延ばしていくのが安全です。