「certificate verify failed」の解決 - CA証明書とSSL検証

「certificate verify failed」の解決 - CA証明書とSSL検証

この記事で解決できること

  • 「certificate verify failed」「unable to get local issuer certificate」の 本当の原因 が分かる
  • openssl s_client3 系統のどれが原因か を即切り分けできる
  • CA 証明書・チェーン不備・時刻ズレを 正しい手順で直せる

結論(切り分けの型)

原因は次の 3 系統のどれか。

  1. クライアント側: CA 証明書バンドルが古い / 欠けている → update-ca-certificates
  2. サーバ側: 中間証明書を送っていない(チェーン不完全)→ サーバ設定を fullchain に
  3. 環境側: システム時刻がズレて有効期限を誤判定 → timedatectl で同期

切り分けの起点は openssl s_clientVerify return code

前提(対象環境)

  • OS: Ubuntu / Debian 系(RHEL 系は適宜読み替え。後述)
  • TLS 検証を行うクライアント(curl / wget / Python / git 等)でエラーが出ている

certificate verify failed とは何か?

結論: クライアントがサーバ証明書を、信頼するルート CA まで辿れなかった状態。証明書が「偽物」とは限らず、検証材料が足りていないことが多い。

TLS 接続では、クライアントはサーバから受け取った証明書を ルート CA まで連鎖(チェーン)させて検証 する。このチェーンが繋がらない、あるいは有効期限・ホスト名が一致しないと検証は失敗する。

ツールによってメッセージは異なるが、中身は同じ検証失敗である。

curl: (60) SSL certificate problem: unable to get local issuer certificate
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)

unable to get local issuer certificate は「発行元(issuer)の証明書が見つからない」という意味。チェーン不備か CA バンドル不足 を強く示唆する。

まず原因を切り分けるには?

結論: openssl s_client で実際のチェーンと Verify return code を見る。番号がどの系統の問題かを一意に指す。

ブラウザや curl の前に、まず生の TLS ハンドシェイクを観測する。

openssl s_client -connect example.com:443 -servername example.com

-servername は SNI を指定する(バーチャルホスト環境で必須)。出力末尾の Verify return code を確認する。

return code 意味 主な原因
0 (ok) 検証成功 クライアント側 CA バンドルの問題(後述)
20 (unable to get local issuer certificate) 発行元が辿れない CA バンドル不足
21 (unable to verify the first certificate) チェーンが繋がらない サーバが中間証明書を送っていない
10 (certificate has expired) 期限切れ 証明書失効 or 時刻ズレ
9 (certificate is not yet valid) まだ有効でない ほぼ時刻ズレ
19 (self signed certificate in certificate chain) 自己署名 社内 CA / プロキシ

openssl s_client0 (ok) を返すのに curl や Python だけ失敗する場合、OS の CA は正常でクライアント固有のバンドル(後述の Python certifi 等)が原因。切り分けの分岐点になる。

チェーンの中身は次でも確認できる。

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer
notBefore=Apr  1 00:00:00 2026 GMT
notAfter=Jun 30 23:59:59 2026 GMT
subject=CN=example.com
issuer=CN=Example Intermediate CA

CA証明書が古い・欠けている場合は?

結論: クライアント側の CA バンドルを更新する。Debian 系は ca-certificates を入れて update-ca-certificates を実行する。

Verify return code: 20 で、サーバ証明書自体は正規 CA 発行の場合、ローカルの信頼ストアが古い。

sudo apt update
sudo apt install --reinstall ca-certificates
sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
3 added, 0 removed; done.

社内 CA など独自のルート証明書を信頼させたい場合は、PEM 形式(拡張子 .crt)を所定ディレクトリに置いて再生成する。

sudo cp company-root-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

/usr/local/share/ca-certificates/ に置くファイルは 拡張子 .crt・PEM 形式が必須.pem や DER 形式は取り込まれない。DER の場合は openssl x509 -inform der -in ca.der -out ca.crt で変換する。

RHEL / CentOS / Fedora 系の場合

ディレクトリとコマンドが異なる。

sudo cp company-root-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

サーバの証明書チェーンが不完全な場合は?

結論: Verify return code: 21 はサーバ側の設定ミス。中間証明書を含む fullchain を配信させるのが正攻法。クライアント側で回避すべきではない。

s_client の出力で Certificate chainサーバ証明書しか並んでいない(中間 CA が無い)なら、サーバが中間証明書を送っていない。多くのブラウザは中間証明書をキャッシュ・補完するため「ブラウザでは見えるのに curl で落ちる」が起きる。

正しい対処は サーバ側で fullchain を配信 すること。

  • Nginx: ssl_certificate にサーバ証明書 + 中間証明書を連結した fullchain.pem を指定する
  • Apache: SSLCertificateFile に fullchain を指定(または SSLCertificateChainFile で中間証明書を指定)
  • Let's Encrypt(certbot): cert.pem ではなく fullchain.pem を使う

検証はブラウザではなく外部チェッカー(SSL Labs 等)か、別ホストからの openssl s_client で行う。

システム時刻のズレが原因の場合は?

結論: certificate has expired / not yet valid が出て証明書自体は有効なら、システム時刻を疑う。timedatectl で同期状態を確認する。

証明書の有効期限(notBefore / notAfter)は システム時刻と比較 して検証される。コンテナや復帰直後の VM で時刻が大きくズレると、有効な証明書でも expirednot yet valid と判定される。

timedatectl
               Local time: Fri 2026-06-05 12:00:00 UTC
           Universal time: Fri 2026-06-05 12:00:00 UTC
          System clock synchronized: yes
                NTP service: active

System clock synchronized: no や明らかな日付ズレがあれば NTP を整える。

sudo timedatectl set-ntp true

時刻同期の詳細は サーバ時刻ズレの対処 を参照。

自己署名・期限切れ証明書への対処は?

結論: 自己署名(return code 19/18)は、その CA を明示的に信頼ストアへ追加する。検証無効化は一時切り分けに限定する。

開発環境や社内プロキシでは自己署名証明書が使われる。正攻法は その証明書(または発行元 CA)を信頼ストアに追加 すること(前述の update-ca-certificates 手順)。

どうしても一時的に検証をスキップする場合のみ、影響範囲を理解した上で使う。

# 一時切り分け限定。常用しない
curl -v https://internal.example.com   # まず原因を見る
curl -k https://internal.example.com   # 検証スキップ(危険)

-k--insecure)や verify=False暗号化は維持するが相手の正当性を検証しない。盗聴は防げても、なりすまし(MITM)を防げない。本番・認証情報を送る通信では絶対に使わない。

ツール別の対処(curl / Python / git)

結論: OS の CA を直しても Python だけ失敗するのは certifi 別バンドルが原因。ツールごとに参照する CA ストアが違う点を押さえる。

curl / wget

OS の CA ストアを参照する。一時的に特定 CA を指定する場合:

curl --cacert /path/to/ca.pem https://example.com
wget --ca-certificate=/path/to/ca.pem https://example.com

Python(requests / urllib)

requests は OS ではなく 同梱の certifi バンドル を参照する。OS の CA を更新しても直らない典型例。

# requests が見ている CA バンドルの場所を確認
python3 -c "import certifi; print(certifi.where())"

特定の CA を使わせる場合は環境変数で指定する。

export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt

SSL_CERT_FILE は Python 標準の ssl モジュール(OpenSSL)が参照する。REQUESTS_CA_BUNDLE は requests 専用。両方を OS のバンドルに向けると、システム CA と挙動を揃えられる。

git

# 特定リポジトリのみ CA を指定
git config http.sslCAInfo /path/to/ca.pem

# 環境変数でも指定可能
export GIT_SSL_CAINFO=/path/to/ca.pem

git config --global http.sslVerify false は検証を完全に無効化するため使わない。

やってはいけないこと

結論: 検証の恒久無効化、無関係な CA の大量追加、時刻を手動固定するなどの「とりあえず通す」対処は、後で必ず事故になる。

まとめと次に読む