「certificate verify failed」の解決 - CA証明書とSSL検証
この記事で解決できること
- 「certificate verify failed」「unable to get local issuer certificate」の 本当の原因 が分かる
openssl s_clientで 3 系統のどれが原因か を即切り分けできる- CA 証明書・チェーン不備・時刻ズレを 正しい手順で直せる
結論(切り分けの型)
原因は次の 3 系統のどれか。
- クライアント側: CA 証明書バンドルが古い / 欠けている →
update-ca-certificates - サーバ側: 中間証明書を送っていない(チェーン不完全)→ サーバ設定を fullchain に
- 環境側: システム時刻がズレて有効期限を誤判定 →
timedatectlで同期
切り分けの起点は openssl s_client の Verify 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_client が 0 (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 で行う。
サーバを直せない事情があっても、クライアントで検証を無効化(-k / verify=False)して放置するのは禁物。中間者攻撃を検知できなくなる。一時切り分け以外で使わない。
システム時刻のズレが原因の場合は?
結論:
certificate has expired/not yet validが出て証明書自体は有効なら、システム時刻を疑う。timedatectlで同期状態を確認する。
証明書の有効期限(notBefore / notAfter)は システム時刻と比較 して検証される。コンテナや復帰直後の VM で時刻が大きくズレると、有効な証明書でも expired や not 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 の大量追加、時刻を手動固定するなどの「とりあえず通す」対処は、後で必ず事故になる。
避けるべき対処
curl -k/verify=False/sslVerify falseを 設定ファイルやスクリプトに常設 する- 原因不明のまま信頼できない CA を信頼ストアに追加する
- 時刻ズレを
date -sで手動固定し、NTP を止める(再発・ログ不整合の元) - サーバ側チェーン不備をクライアント全台で回避する(直すべきはサーバ 1 台)