Published on

Python SSL CERTIFICATE_VERIFY_FAILED 10분 해결

Authors

서드파티 API 호출, 패키지 설치(pip), 내부 HTTPS 엔드포인트 접근 등에서 갑자기 아래 오류를 만나면 대부분 “내 코드” 문제가 아니라 신뢰할 수 있는 CA(인증기관) 체인을 Python이 찾지 못하는 환경 문제입니다.

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

이 글은 “일단 되게 만들기”가 아니라 10분 안에 원인을 분류하고, 가장 안전한 방식으로 고치는 루트를 제공합니다. 마지막에 정말 불가피할 때만 쓰는 임시 우회도 함께 정리합니다.

> 참고로 네트워크/프록시가 개입된 장애는 타임아웃/재시도 설계와 함께 봐야 재발이 줄어듭니다. 연결 오류 대응은 Python httpx ReadTimeout·ConnectError 재시도 설계도 같이 확인해두면 좋습니다.

1) 10분 진단 체크리스트(가장 빠른 분기)

아래 4가지만 확인해도 80%는 바로 갈립니다.

  1. 어디서 터지나?

    • pip install 중인가?
    • requests/httpx/aiohttp로 API 호출 중인가?
    • 특정 도메인에서만 터지나?
  2. 환경이 뭔가?

    • macOS 로컬 Python?
    • Ubuntu/Alpine Docker 컨테이너?
    • 회사 프록시(SSL 검사) / 사내망?
    • AWS Lambda/EKS 같은 런타임?
  3. 서버 인증서 체인 문제인가? (서버가 중간 인증서 미제공)

  4. 클라이언트 CA 번들 문제인가? (컨테이너에 CA 번들이 없음/오래됨)

진단을 위해 다음 스니펫을 먼저 실행해 “Python이 어떤 CA 번들을 쓰는지”부터 확인합니다.

import ssl
import certifi

print("ssl default verify paths:", ssl.get_default_verify_paths())
print("certifi.where():", certifi.where())
  • ssl.get_default_verify_paths()가 가리키는 경로에 파일이 없거나,
  • 컨테이너가 minimal 이미지라 CA 패키지가 없거나,
  • 회사 프록시가 자체 CA로 TLS를 재서명하는데 그 CA가 신뢰 저장소에 없으면

대부분 CERTIFICATE_VERIFY_FAILED로 귀결됩니다.

2) 가장 흔한 원인 1: Docker/Alpine에 CA 인증서가 없다

증상

  • 로컬에서는 되는데 컨테이너에서만 실패
  • 특히 python:3.x-alpine, distroless, 최소 이미지에서 빈번

해결(권장): CA 번들 설치

Alpine

FROM python:3.12-alpine

RUN apk add --no-cache ca-certificates \
  && update-ca-certificates

# (선택) pip가 HTTPS 접근 시에도 안전
RUN python -m pip install --upgrade pip

Debian/Ubuntu

FROM python:3.12-slim

RUN apt-get update \
  && apt-get install -y --no-install-recommends ca-certificates \
  && update-ca-certificates \
  && rm -rf /var/lib/apt/lists/*

컨테이너에서 CA 번들이 설치되면 requests/httpx는 OS trust store를 통해 정상 검증합니다.

추가 확인: 인증서 파일이 실제로 존재하는지

python - <<'PY'
import ssl
p = ssl.get_default_verify_paths()
print(p)
PY

ls -al /etc/ssl/certs || true
ls -al /etc/pki/tls/certs || true

3) 가장 흔한 원인 2: 회사 프록시(SSL Inspection)로 인증서가 재서명된다

증상

  • 사내 네트워크에서만 실패, 집/모바일 핫스팟에서는 정상
  • 브라우저는 잘 되는데 Python만 실패(브라우저에는 회사 Root CA가 이미 배포됨)

해결(권장): 회사 Root CA를 OS/Python 신뢰 저장소에 추가

회사 보안 장비가 TLS를 중간에서 복호화/재암호화하면, 서버 인증서는 “공인 CA”가 아니라 “회사 CA”로 서명됩니다. 따라서 Python이 그 CA를 신뢰하도록 해야 합니다.

(A) OS trust store에 추가 (가장 바람직)

  • Debian/Ubuntu: /usr/local/share/ca-certificates/*.crt에 넣고 update-ca-certificates
sudo cp corp-root-ca.crt /usr/local/share/ca-certificates/corp-root-ca.crt
sudo update-ca-certificates
  • RHEL/CentOS 계열: /etc/pki/ca-trust/source/anchors/에 넣고 update-ca-trust
sudo cp corp-root-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

(B) 앱 레벨에서 CA 번들 지정(운영에서 자주 쓰는 방식)

requests/httpx는 CA 번들을 파일로 지정할 수 있습니다.

import httpx

r = httpx.get(
    "https://api.example.com",
    verify="/etc/ssl/certs/corp-bundle.pem",
    timeout=10,
)
print(r.status_code)

환경변수로도 지정 가능합니다.

export SSL_CERT_FILE=/etc/ssl/certs/corp-bundle.pem
# 또는
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/corp-bundle.pem

> 프록시가 있는 환경에서는 “연결은 되는데 응답이 끊김/오류 코드가 튀는” 문제도 함께 생깁니다. 스트리밍/장시간 연결이라면 프록시 튜닝 관점에서 LLM SSE 스트리밍 499 502 급증과 응답 끊김을 잡는 프록시 튜닝 체크리스트도 같이 보면 원인 분리가 빨라집니다.

4) 가장 흔한 원인 3: 서버가 중간 인증서(Intermediate)를 누락했다

증상

  • 특정 도메인에서만 실패
  • 브라우저는 “알아서” 보완해 보이기도 하지만, Python은 엄격하게 실패

확인: openssl로 체인 검증

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

출력에서 Verify return code: 0 (ok)가 아니면 서버 측 체인 문제일 가능성이 큽니다.

해결

  • 서버(예: Nginx/ALB/Ingress)에 fullchain(서버 인증서 + intermediate)을 설정
  • Let’s Encrypt라면 보통 fullchain.pem을 사용

예: Nginx

server {
  listen 443 ssl;
  server_name example.com;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

이 경우 클라이언트(Python)를 건드리는 게 아니라 서버 설정을 고치는 게 정답입니다.

5) macOS에서만 나는 경우: Python 인증서 번들 설치(특히 python.org 설치본)

macOS에서 python.org에서 받은 Python을 쓰면, 시스템 키체인과 Python 번들이 분리되어 인증서가 비어 있는 케이스가 있습니다.

해결

  • /Applications/Python 3.x/ 아래에 있는 Install Certificates.command 실행
  • 또는 certifi를 사용해 verify 경로를 명시
import requests
import certifi

resp = requests.get("https://pypi.org/simple", verify=certifi.where(), timeout=10)
print(resp.status_code)

6) pip install에서 터질 때(패키지 설치 자체가 막힘)

6-1) CA 업데이트

가장 먼저 ca-certificates 설치/업데이트를 확인하세요(컨테이너면 2번 참고).

6-2) 사내 미러/프록시 사용

사내에서 PyPI를 직접 못 나가면 미러를 쓰거나 프록시 설정이 필요합니다.

pip config set global.index-url https://pypi.company.local/simple
pip config set global.trusted-host pypi.company.local

> trusted-host는 TLS 검증 자체를 끄는 게 아니라 “호스트 검증 예외”에 가까운 설정이라 보안적으로도 주의가 필요합니다. 가능하면 사내 CA를 신뢰하도록 구성하는 편이 낫습니다.

7) (정말 마지막 수단) 검증 비활성화는 이렇게, 그리고 왜 위험한가

긴급 장애 대응 중 임시로만 쓰고, 티켓/리마인더를 남겨 반드시 원인 해결로 되돌리세요.

requests

import requests

resp = requests.get("https://api.example.com", verify=False, timeout=10)
print(resp.status_code)

httpx

import httpx

with httpx.Client(verify=False, timeout=10) as client:
    r = client.get("https://api.example.com")
    print(r.status_code)

위험성

  • 중간자 공격(MITM)에 취약
  • 사내 프록시 환경에서는 “누가 재서명했는지” 구분 불가
  • 토큰/쿠키/자격증명 유출 가능

운영 환경에서는 “회사 CA 추가” 또는 “서버 fullchain 구성”이 정석입니다.

8) 10분 해결 루트 요약(가장 성공률 높은 순서)

  1. 컨테이너/서버라면 ca-certificates 설치 및 업데이트
  2. 사내망이라면 회사 Root CA를 OS trust store에 추가하거나 SSL_CERT_FILE로 지정
  3. 특정 도메인만 문제면 openssl s_client로 서버 체인(fullchain) 누락 여부 확인
  4. macOS라면 Install Certificates.command 또는 certifi.where()로 검증 경로 고정
  5. 임시로만 verify=False (운영 금지에 가깝게 취급)

9) 재발 방지 체크(운영 관점)

  • Docker 베이스 이미지 변경 시(CI에서 slim/alpine로 바꿀 때) ca-certificates 설치가 빠지지 않게 Dockerfile 템플릿화
  • 사내 프록시 사용 조직이라면 회사 CA 배포/로테이션 절차를 문서화하고, 컨테이너 이미지 빌드 단계에서 CA 주입
  • 외부 API 의존 서비스는 TLS 오류가 곧 장애로 이어지므로, 재시도/타임아웃/서킷브레이커를 함께 설계(위에서 언급한 httpx 재시도 글 참고)

이 순서대로 따라가면 대부분의 SSL CERTIFICATE_VERIFY_FAILED는 “원인에 맞는 방식”으로 10분 내 정리됩니다.