Public

PythonでSSLエラーが発生したときの原因と解決方法

※本記事には広告・アフィリエイトリンクが含まれます

Python Python
t-salad t-salad committed ef0c0d7

PythonのrequestsやurllibでHTTPS通信をすると、次のようなSSLエラーで止まることがあります。「python ssl」で検索して来た人が実際に遭遇するもっとも多いのがこのメッセージです。

スポンサーリンク
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1000)

requests経由だと、同じ原因がこの形で出ることもあります。

requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1000)')))

このエラーはサーバーが危険なわけではなく、Python側がサーバー証明書を検証する材料(ルート証明書)を持っていない・古い・見つけられないときに出るのがほとんどです。この記事では、まずエラー文から原因を切り分け、検証を無効化せずに正しく直す方法を原因別に解説します。

まずエラー文で原因を切り分ける

SSLエラーはメッセージの末尾を見ると原因がほぼ特定できます。よく出るものを対応表にまとめます。

エラー文(末尾)主な原因対応セクション
unable to get local issuer certificateルート証明書が無い/古い(最頻・特にMac)原因1
self signed certificate in certificate chain社内プロキシ・自己署名証明書原因2
certificate has expired証明書の期限切れ(サーバー側)原因4
hostname '...' doesn't matchホスト名の不一致原因4
tlsv1 alert protocol version / no protocols available古いTLS/OpenSSL原因5

原因1: ローカルにルート証明書が無い(最頻・特にMac)

unable to get local issuer certificateはこのパターンです。Pythonは証明書の検証にcertifiというルートCAの束を使いますが、これが無い・古いと検証に失敗します。まずはcertifiを更新します。

pip install --upgrade certifi

Macでpython.org版のPythonを使っている場合

macOSでpython.orgのインストーラからPythonを入れた場合、システムの証明書とリンクされておらずこの状態になりがちです。Python同梱の証明書インストールスクリプトを一度だけ実行すると解決します(3.xは自分のバージョンに置き換えてください)。

/Applications/Python\ 3.x/Install\ Certificates.command

これは内部でcertifiをインストールし、sslのデフォルト証明書パスに紐付けます。Homebrew版やpyenv版のPythonでは通常この問題は起きません。

原因2: 社内プロキシ・自己署名証明書(企業ネットワーク)

self signed certificate in certificate chainが出る場合、社内プロキシ(Zscaler等)やファイアウォールがHTTPS通信を検査するために独自のルート証明書を挿入していることが多いです。この場合はその社内ルート証明書をPythonに信頼させるのが正攻法です(検証は無効化しない)。

方法A: verifyでCAバンドルを指定する

import requests

response = requests.get(
    'https://internal.example.com',
    verify='/path/to/corp-root-ca.pem',  # 社内ルート証明書(PEM)
)

方法B: 環境変数で指定する(コードを変えずに済む)

requestsREQUESTS_CA_BUNDLE、標準sslSSL_CERT_FILEを参照します。両方設定しておくと確実です。

export REQUESTS_CA_BUNDLE=/path/to/corp-root-ca.pem
export SSL_CERT_FILE=/path/to/corp-root-ca.pem

社内CAが複数ある場合は、certifiのバンドル末尾に社内証明書を追記する方法もあります。python -m certifiでバンドルのパスを確認できます。

原因3: 中間証明書の欠落(サーバー側のチェーン不備)

ブラウザでは開けるのにPythonだけ失敗する場合、サーバーが中間証明書を配信していないことがあります。ブラウザは中間証明書を補完してくれますが、Pythonは補完しないためチェーンが切れて検証に失敗します。まずopensslでチェーンを確認します。

openssl s_client -connect example.com:443 -showcerts

出力に中間CAが含まれていなければサーバー側の設定不備です。サーバーを直せない場合は、中間証明書を含む完全なチェーンのPEMを用意し、原因2と同じくverify=で指定して回避します。

スポンサーリンク

原因4: 証明書の期限切れ・ホスト名の不一致

certificate has expiredはサーバー証明書が失効しています。基本はサーバー側の更新待ちで、自分のコード側では直せません(一時的に通したい場合のみ後述の応急処置)。

hostname '...' doesn't matchはアクセス先URLのホスト名が証明書のCN/SANと一致しないケースです。URLのスペルミス、IP直打ち、内部向けホスト名などが原因になりやすいので、まずURLを確認します。

import requests

try:
    requests.get('https://expired.badssl.com/')
except requests.exceptions.SSLError as e:
    print(f'SSL Error: {e}')  # メッセージ末尾で原因を確認

原因5: 古いTLSバージョン・OpenSSL

tlsv1 alert protocol versionno protocols availableは、サーバーが古いTLS(1.0/1.1)しか話せない、または手元のOpenSSLが古いことが原因です。バージョンを確認します。

python -c "import ssl; print(ssl.OPENSSL_VERSION)"
python --version

Python 3.10以降はデフォルトでTLS 1.2未満を拒否します。相手が更新できないレガシーサーバーの場合は、後述のカスタムSSLContextで許可TLSを下げる回避が可能ですが、セキュリティ上は非推奨です。

応急処置: 検証を一時的に無効化する(本番禁止)

開発中に今すぐ通したいときの最終手段がverify=Falseです。中間者攻撃を防げなくなるため本番では絶対に使わないでください。

import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get('https://example.com', verify=False)

無効化の詳しい方法(urllib3・ssl各ライブラリでの書き方)と安全な代替は、PythonでSSL証明書エラーが出るときの最終兵器にまとめています。

より細かく制御する: カスタムSSLContext

CAの指定だけでなくTLSバージョンや暗号スイートまで制御したい場合はSSLContextを使います。ただしrequests.get()ssl_context引数は無いため、HTTPAdapter経由で渡すのが正しい方法です(この書き方を間違えている情報が多いので注意)。

import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.poolmanager import PoolManager

class SSLContextAdapter(HTTPAdapter):
    def __init__(self, ssl_context=None, **kwargs):
        self.ssl_context = ssl_context
        super().__init__(**kwargs)

    def init_poolmanager(self, connections, maxsize, block=False, **kwargs):
        self.poolmanager = PoolManager(
            num_pools=connections, maxsize=maxsize, block=block,
            ssl_context=self.ssl_context,
        )

ctx = ssl.create_default_context(cafile='custom_ca.pem')
# 例: 必要ならTLS下限を調整(レガシー対応・非推奨)
# ctx.minimum_version = ssl.TLSVersion.TLSv1_2

session = requests.Session()
session.mount('https://', SSLContextAdapter(ssl_context=ctx))
response = session.get('https://example.com')

多くのケースでは原因1〜3のverify=指定で足ります。ここまでの制御が必要になるのはクライアント証明書認証やTLS要件が特殊な場合だけです。

まとめ

PythonのSSLエラーは、原因さえ切り分けられれば検証を無効化せずに直せます。

  • まずエラー文の末尾を見て原因を特定する(冒頭の対応表)
  • unable to get local issuer certificate: certifi更新/MacはInstall Certificates.command
  • self signed certificate in certificate chain: 社内ルートCAをverify=REQUESTS_CA_BUNDLEで信頼させる
  • 中間証明書欠落はopenssl s_clientで確認し、完全なチェーンを指定する
  • verify=Falseは開発時の最終手段のみ。本番では使わない

参考

スポンサーリンク
スポンサーリンク
★ この記事をXでシェア はてなブックマーク
関連記事