HOWTO · Python
Python의 SSL 인증서 검증 오류(`CERTIFICATE_VERIFY_FAILED`) 해결하기
이 문서에서는 Python에서 발생하는 SSL 인증서 검증 오류의 원인을 진단하고 안전하게 해결하는 방법을 설명합니다.
이 페이지의 내용
SSL: CERTIFICATE_VERIFY_FAILED는 TLS 연결에서 제시된 인증서가 Python이 신뢰하는 인증 기관으로 이어지는지, 요청한 호스트와 일치하는지 확인하지 못했다는 뜻입니다. 이는 클라이언트 측 인증서 검증 실패입니다. 도메인이 존재하지 않아 인증 기관이 오류를 반환했다는 뜻은 아닙니다.
TLS는 트래픽을 암호화하고 인증서를 통해 서버의 신원을 확인합니다. 신뢰 확인의 어느 부분에서 실패했는지 파악하는 동안에도 인증서 검증은 활성화된 상태로 유지해야 합니다.
SSL: CERTIFICATE_VERIFY_FAILED 오류의 의미
연결 중에 서버가 TLS 인증서를 제시합니다. 클라이언트는 인증서의 유효 기간, 호스트 이름, 발급자 체인을 확인하고, 그 체인이 클라이언트의 신뢰 저장소에 있는 신뢰할 수 있는 루트 인증서까지 이어지는지 검사합니다. Python의 ssl 모듈은 인증서 검증이 활성화된 SSL 컨텍스트를 사용할 때 이러한 검사를 수행합니다. Requests도 기본적으로 인증서를 검증합니다.
연결의 어느 한쪽에서든 문제가 생기면 이 오류가 발생할 수 있습니다. 예를 들어 서버가 중간 인증서를 누락했거나 로컬 Python 환경의 CA 번들이 오래되었을 수 있습니다. 호스트 이름 불일치, 잘못된 시스템 시각, 사설 인증 기관, TLS를 검사하는 프록시도 원인일 수 있습니다. 오류 메시지만으로는 어떤 원인인지 알 수 없습니다.
원인 진단하기
실패한 애플리케이션과 동일한 호스트 이름 및 Python 환경을 사용해 다음 항목을 확인하세요.
- 서버 인증서와 체인을 확인합니다. 인증서가 유효 기간 내에 있고 URL의 호스트 이름에 발급되었는지, 발급자까지 이어지는 데 필요한 중간 인증서가 포함되어 있는지 확인합니다. 브라우저나 다른 클라이언트에서는 연결된다면 해당 클라이언트의 신뢰 설정과 네트워크 경로를 비교하세요. 차이는 단서일 뿐, 서버 설정이 올바르다는 증거는 아닙니다.
- 시계를 확인합니다. 컴퓨터나 컨테이너의 날짜와 시각이 정확한지 확인합니다. 인증서의 유효 기간에서 크게 벗어난 시계 때문에 유효한 인증서가 만료되었거나 아직 유효하지 않은 것으로 판단될 수 있습니다.
- Python 환경과 HTTP 클라이언트를 확인합니다. 애플리케이션을 실행하는 인터프리터와 실패한 요청이 표준 라이브러리, Requests 또는 다른 패키지 중 무엇을 사용하는지 확인합니다. 시스템 Python, 가상 환경, 컨테이너, 패키징된 애플리케이션은 각각 다른 신뢰 저장소를 사용할 수 있습니다.
- 신뢰 출처와 네트워크 경로를 확인합니다. 서버가 사설 CA를 사용하는지, 회사 프록시가 TLS를 가로채는지 확인합니다. 네트워크 또는 보안 관리자에게 승인된 CA 인증서와 해당 환경의 설정 지침을 요청하세요. 오류를 없애기 위해 출처가 확인되지 않은 인증서를 내려받지 마세요.
서버 인증서가 만료되었거나 호스트 이름이 잘못되었거나 필요한 중간 인증서가 빠졌다면 서버 관리자에게 배포 설정을 수정해 달라고 요청하세요. 클라이언트의 CA 번들을 바꿔도 잘못된 서버 인증서 체인은 바로잡을 수 없습니다.
인증서 또는 신뢰 저장소 수정하기
서버 측 문제라면 서버 관리자가 요청된 호스트 이름에 맞춰 인증서를 갱신하거나 인증서 체인을 올바르게 설정하는 것이 지속 가능한 해결책입니다. 다른 브라우저로 다시 시도하면 동작을 비교하는 데 도움이 될 수 있지만, 인증서를 수정하거나 신뢰할 수 없는 연결을 안전하게 만들지는 않습니다.
사설 서비스나 승인된 TLS 검사 프록시를 사용하는 경우 조직의 신뢰할 수 있는 배포 경로를 통해 루트 CA 인증서를 받으세요. 해당 운영 체제, 컨테이너 또는 Python 환경에서 사용하는 신뢰 저장소에 인증서를 설치하거나 애플리케이션이 해당 CA 번들을 사용하도록 설정합니다. 인증서의 출처와 적용 범위를 확인할 수 있도록 관리자가 문서화한 절차를 따르세요.
오래된 CA 번들도 오류의 원인일 수 있습니다. 서버가 새롭지만 유효한 공개 CA를 사용하면 특히 그렇습니다. 애플리케이션에서 사용하는 신뢰 저장소 또는 CA 번들 패키지를 업데이트하세요. 일부 Python 설치에서는 pip를 업데이트하면 pip 자체의 패키지 다운로드 인증서 문제를 해결하는 데 도움이 될 수 있습니다. certifi를 업데이트하면 Requests가 해당 번들을 사용하도록 설정된 경우 Requests의 번들을 갱신할 수 있습니다. 그러나 이러한 업데이트로 만료된 서버 인증서, 호스트 이름 불일치, 누락된 서버 중간 인증서, 신뢰하도록 설정되지 않은 조직 전용 CA 문제가 해결되지는 않습니다.
Python과 Requests에서 인증서 검증 설정하기
Python 표준 라이브러리의 ssl 모듈과 Requests는 서로 다른 신뢰 출처를 사용할 수 있습니다. ssl.create_default_context()는 해당 Python/OpenSSL 환경에서 제공하는 기본값을 불러오며, 이는 빌드와 플랫폼 설정에 따라 달라질 수 있습니다. Requests는 보통 certifi가 제공하는 CA 번들로 인증서를 검증합니다. session.verify = True는 “Requests에 설정된 기본 번들을 사용한다”는 뜻이지 “Python의 ssl 기본값을 사용한다”거나 반드시 운영 체제의 저장소를 사용한다는 뜻은 아닙니다. 세션을 명시적으로 설정하지 않으면 Requests는 REQUESTS_CA_BUNDLE(또는 CURL_CA_BUNDLE)에서 번들 경로를 가져올 수도 있습니다.
다음 예제는 Python의 기본 컨텍스트에서 인증서 검증이 필수임을 보여 주는, 자체적으로 완료되는 성공 사례입니다.
이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.
import ssl
context = ssl.create_default_context()
print(context.verify_mode == ssl.CERT_REQUIRED)
True
사설 CA를 사용하는 경우 신뢰할 수 있는 CA 번들을 cafile로 지정한 다음, 해당 검증 컨텍스트를 표준 라이브러리의 HTTPS 요청에 전달하세요. 두 값은 관리자가 제공한 서비스 URL과 번들 경로로 바꾸면 됩니다. 이 예제는 실제 서비스 연결이 필요하므로 여기서는 실행하지 않았습니다. 값을 입력한 뒤 실행할 수 있도록 연결 오류를 처리합니다.
import ssl
import urllib.error
import urllib.request
SERVICE_URL = "https://service.example"
CA_BUNDLE = "/path/to/organization-ca-bundle.pem"
try:
context = ssl.create_default_context(cafile=CA_BUNDLE)
with urllib.request.urlopen(SERVICE_URL, context=context, timeout=10) as response:
print(response.status)
except (OSError, urllib.error.URLError) as error:
print(f"Connection not completed: {type(error).__name__}: {error}")
Requests의 verify 설정에는 CA 번들 경로를 지정할 수 있습니다. 조직에서 제공한 번들을 세션에 지정하고 같은 세션으로 요청을 보내면 인증서 검증이 계속 활성화됩니다. URL과 경로를 서비스에 맞는 값으로 바꾸세요. 이 예제도 실제 서비스 연결이 필요하므로 여기서는 실행하지 않았습니다.
import requests
SERVICE_URL = "https://service.example"
CA_BUNDLE = "/path/to/organization-ca-bundle.pem"
session = requests.Session()
session.verify = CA_BUNDLE
try:
response = session.get(SERVICE_URL, timeout=10)
print(response.status_code)
except (OSError, requests.RequestException) as error:
print(f"Connection not completed: {type(error).__name__}: {error}")
조직에서 신뢰하는 경로를 통해 발급자를 확인한 CA 번들을 사용하세요. 사용자 지정 번들에는 서비스의 인증서 체인을 검증하는 데 필요한 CA 인증서가 들어 있어야 합니다. CA를 설정해도 만료된 인증서, 호스트 이름 불일치 또는 서버 측 중간 인증서 누락은 해결되지 않습니다. Requests가 기본 번들을 사용해야 한다면 session.verify에 True를 할당하지 말고 기본값을 그대로 두세요. 조직 CA를 사용할 때는 명시적인 경로를 지정합니다. 오래된 신뢰 출처가 Requests에 포함된 공개 루트라면 certifi를 업데이트해 번들을 갱신할 수 있고, pip를 업데이트하면 pip 자체의 패키지 다운로드에 도움이 될 수 있습니다. 어느 쪽도 모든 환경에서 Python의 ssl 신뢰 설정을 바꾸거나 잘못된 서버 인증서를 고치지는 않습니다.
인증서 검증을 비활성화하면 안 되는 이유
인증서 검증을 끄면 공격자나 신뢰할 수 없는 프록시가 잘못된 신원의 인증서를 제시해도 클라이언트가 연결을 받아들일 수 있습니다. 암호화만으로는 상대가 의도한 서버인지 확인할 수 없습니다. 그러면 자격 증명, 토큰, 응답 데이터가 노출되거나 변조될 수 있습니다.
verify=False, 검증하지 않는 SSL 컨텍스트 또는 Python의 기본 HTTPS 컨텍스트를 전역으로 교체하는 방법을 해결책으로 사용하지 마세요. 이런 설정은 진단에 필요한 신호를 숨기고 영향을 받는 모든 연결의 보안을 약화합니다. 서버 인증서 체인, 클라이언트 시계나 신뢰 저장소를 수정하거나 신뢰할 수 있는 CA 번들을 명시적으로 설정해 검증을 복구하세요.