最初に「誰の端末で出るか」とエラーコードを確認する

原因を調べる前に、2つのことを確認します。1つ目は、エラーが出る範囲です。全員の端末で出るなら、サーバー側の証明書や設定が原因である可能性が高くなります。特定の端末や特定の拠点だけで出るなら、端末の時刻、社内プロキシ、ルート証明書の配布状況を疑います。社外のスマートフォン回線から同じURLを開いてみると、短時間で切り分けられます。

2つ目は、警告画面に小さく表示されるエラーコードです。「この接続ではプライバシーが保護されません」はChromeとEdgeの表示です。この文言はどの原因でも同じですが、下に出るコードは原因ごとに変わります。コードから、最初に見るべき手順の見当をつけます。

Chrome / Edge のコードFirefox のコード最初に見る手順
NET::ERR_CERT_DATE_INVALIDSEC_ERROR_EXPIRED_CERTIFICATE手順1、手順4
NET::ERR_CERT_AUTHORITY_INVALIDSEC_ERROR_UNKNOWN_ISSUER手順2、手順6、手順7
NET::ERR_CERT_COMMON_NAME_INVALIDSSL_ERROR_BAD_CERT_DOMAIN手順3
ERR_SSL_VERSION_OR_CIPHER_MISMATCHSSL_ERROR_NO_CYPHER_OVERLAP など手順5

以降の手順では、主に次のコマンドを使います。-servername はSNI(1つのIPアドレスで複数のサイトを運用するときに、接続先のホスト名をサーバーへ伝える仕組み)を指定するオプションです。これを省くと、目的とは別の証明書が返ってくることがあります。

openssl s_client -connect www.example.jp:443 -servername www.example.jp -showcerts </dev/null

出力の末尾にある Verify return code を最初に見てください。0 (ok) なら、検証を実行した端末からは証明書チェーンが正しく見えています。その場合は、エラーが出る端末側に原因がある可能性が高くなります。Windowsでopensslが使えない場合は、ブラウザの証明書ビューアを使います。警告画面の証明書表示、またはアドレスバー左のアイコンから開けます。

手順1:証明書の有効期限と端末の時刻を確認する

最も手間が少なく、最も多い原因です。有効期間は次のコマンドで確認できます。

openssl s_client -connect www.example.jp:443 -servername www.example.jp </dev/null 2>/dev/null | openssl x509 -noout -dates

notAfter が過去の日時なら、証明書の期限切れです。ブラウザでは、証明書ビューアの「全般」タブにある有効期間で確認できます。期限切れであれば、更新の仕組みが動いていなかったことになります。手順4に進んでください。

証明書の期限内なのに、一部の端末だけで日付エラーが出る場合は、その端末の時計を確認します。時計が大きく進んでいたり遅れていたりすると、正しい証明書も期限外と判定されます。原因としては、BIOSの電池切れ、NTP(時刻同期の仕組み)への通信の遮断、仮想マシンの復元直後などが考えられます。時刻を直して解消すれば、そこで終わりです。解消しなければ手順2へ進みます。

手順2:中間証明書の欠落を確認する

サーバー証明書は、認証局の中間証明書を経由してルート証明書につながります。サーバーが中間証明書を送っていないと、クライアントは信頼の経路をたどれません。-showcerts を付けた出力の Certificate chain 欄を見ます。0 s:(サーバー証明書)しかなく、1 s: 以降がなければ中間証明書が欠けています。この場合、Verify return code は 21 (unable to verify the first certificate) や 20 (unable to get local issuer certificate) になることが多いです。

この不具合は気づきにくい性質があります。ブラウザは中間証明書を自分で補える場合があるためです。そのため、PCのブラウザでは表示できるのに、スマートフォンアプリ、curl、Javaなどのサーバー間通信だけが失敗する、という形で表れます。certbotを使っている場合は、サーバー設定が cert.pem ではなく fullchain.pem を参照しているか確認します。チェーンが揃っていて、それでも20番台のエラーが出る場合は、手順6と手順7の可能性があります。

手順3:ホスト名とSAN(サブジェクト代替名)の一致を確認する

現在のブラウザは、証明書のCN(コモンネーム)ではなくSAN(Subject Alternative Name:証明書が有効なホスト名の一覧)で照合します。SANは次のコマンドで確認できます。

openssl s_client -connect www.example.jp:443 -servername www.example.jp </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName

アクセスしたホスト名が一覧になければ、そのホスト名では証明書が使えません。よくあるのは次の3つです。example.jp だけで発行していて、www 付きのURLでアクセスしている。*.example.jp のワイルドカード証明書を、2階層下の a.b.example.jp で使っている。社内システムにIPアドレスや短いホスト名でアクセスしている。

s_clientに -verify_hostname www.example.jp を加えると、不一致のときに 62 (hostname mismatch) が返ります。SANにホスト名が含まれているのにエラーになる場合は、別の仮想ホストの証明書が返っていないか確認してください。-servername の有無で結果を比べるとわかります。

手順4:自動更新(ACME/certbot)の失敗を確認する

ACMEはLet's Encryptなどが採用している、証明書の自動発行・更新のためのプロトコルです。期限切れが判明したら、更新処理のどこで止まったかを調べます。certbot certificates で、管理対象の証明書と期限を確認します。続いて certbot renew --dry-run で更新を試し、エラーの内容を見ます。詳細なログは /var/log/letsencrypt/letsencrypt.log に残ります。

dry-runが失敗する場合、多くは認証の段階で止まっています。HTTP-01方式では、ポート80への外部からの到達が必要です。ファイアウォールの変更、リダイレクト設定、CDNの導入によって認証が通らなくなることがあります。DNS-01方式では、DNSのAPI認証情報の失効が原因になりがちです。dry-runが成功するなら、定期実行そのものを疑います。systemctl list-timers でcertbotのタイマーが登録されているか、cronが動いているかを確認します。

証明書ファイルは更新済みなのに古い証明書が返り続ける場合は、Webサーバーが再読み込みされていません。--deploy-hook でnginxやApacheをreloadする設定を加えます。また、Let's Encryptは2025年に有効期限切れの通知メールを終了しています(執筆時点)。期限の監視は自前で用意してください。証明書の最大有効期間は、CA/Browser Forumの決定によって段階的に短縮されていきます。手動更新に頼る運用は今後さらに続けにくくなります。手順はcertbot公式ドキュメントで確認してください。

手順5:TLSバージョンと暗号スイートの不一致を確認する

ERR_SSL_VERSION_OR_CIPHER_MISMATCH は、証明書そのものではなく、通信方式の合意に失敗したことを示します。TLS 1.0と1.1は、2020年前後に主要ブラウザで既定で無効になりました。そのため、古いNAS、UPS、複合機などの管理画面でこのエラーがよく出ます。まず openssl s_client -connect host:443 -tls1_2 と -tls1_3 で、それぞれ接続できるか確認します。どちらも失敗するなら、サーバーが古い方式にしか対応していません。機器のファームウェア更新か、TLS 1.2以上に対応した装置への置き換えを検討します。

反対に、サーバー側で古い方式を無効にした直後に、古いクライアントや業務アプリだけが失敗することもあります。設定変更の履歴と、エラーが出始めた時期を照らし合わせてください。OpenSSL 3系のクライアントでは、SHA-1署名や短い鍵長の証明書が既定で拒否される場合があります。

手順6:プロキシやセキュリティ製品によるTLSインスペクションを確認する

TLSインスペクションは、プロキシやUTM、エンドポイント製品が通信をいったん復号して検査する機能です。このとき、製品は独自の証明書を発行し直してクライアントに渡します。証明書ビューアで発行元を見て、本来の認証局ではなく製品名や自社名が表示されていれば、通信が中継されています。社内と社外で同じs_clientを実行し、i:(発行者)の行を比べると確実です。

中継が確認できた場合、エラーの原因は次のいずれかであることが多いです。1つは、製品のルート証明書が端末に入っていないこと。もう1つは、そのアプリが独自の信頼ストア(信頼するルート証明書の一覧)を使っていることです。Java、Python、Node.jsなどは、OSの証明書ストアを参照しない構成がよくあります。証明書ピンニング(特定の証明書だけを受け入れる仕組み)を使うアプリは、中継されると必ず失敗します。対処は、検査対象からの除外設定か、アプリの信頼ストアへの登録です。どちらが適切かは、製品ベンダーの資料に従ってください。

手順7:社内CAのルート証明書の配布状況を確認する

社内CAで発行した証明書を使うシステムでは、社内CAのルート証明書が各端末に入っていないと AUTHORITY_INVALID になります。新しく導入した端末、ドメインに参加していない端末、BYOD端末、Linuxサーバーで起きやすい問題です。Windowsではグループポリシーなどで配布状況を確認します。Linuxでは update-ca-certificates や update-ca-trust で登録します。Firefoxは独自の信頼ストアを持っています。そのため、OS側の企業ルートを参照する設定が有効になっているかも確認してください。

ここまでで直らないときの次の手

まず、エラーコード、発生した端末とネットワーク、s_clientの Certificate chain と Verify return code の出力を記録します。比較用に、社内と社外の両方で取得してください。サーバー側の設定は、外部の診断サービスで総合的に確認できます。ただし、診断結果を自社のポリシーとどう照らし合わせるかは、自社で判断してください。証明書の発行元に問題があると考えられる場合は、記録を添えて認証局のサポートに問い合わせます。セキュリティ製品が関わる場合は、その製品のベンダーに問い合わせます。

原因が判明したら、再発防止として2つを整備してください。1つは、証明書の期限を外部から監視する仕組みです。もう1つは、TLSインスペクションの除外リストと社内CAの配布手順を文書にしておくことです。