증상

공공데이터포털(apis.data.go.kr) API를 Python requests로 호출할 때 아래와 같은 SSL 오류가 발생했습니다.

requests.exceptions.SSLError: HTTPSConnectionPool(host='apis.data.go.kr', port=443): Max retries exceeded ...
(Caused by SSLError(SSLError(1, '[SSL: SSLV3_ALERT_ILLEGAL_PARAMETER] ssl/tls alert illegal parameter (_ssl.c:1006)')))

특징

  • 동일 요청이 브라우저나 curl에서는 정상 동작
  • requests만 TLS 핸드셰이크 단계에서 실패

원인 추정

  • TLS 버전 협상 문제: 일부 서버가 TLS 1.3 또는 특정 확장을 안정적으로 처리하지 못함
  • OpenSSL 보안 레벨 상승 영향: OpenSSL 3 기본 SECLEVEL이 높아져 레거시 암호군/해시가 거절될 수 있음
  • 클라이언트 기본값 차이: curlrequests가 사용하는 TLS 스택/암호군/확장이 상이

재현 코드 (문제 상황)

import requests

url = "https://apis.data.go.kr/1160100/service/GetGeneralProductInfoService/getGoldPriceInfo?..."
requests.get(url, timeout=10).json()  # SSL 오류 발생

진단 팁

  • curl로 TLS 버전 고정 비교: curl --tlsv1.2 -v "https://..."
  • OpenSSL로 핸드셰이크 확인: openssl s_client -tls1_2 -connect apis.data.go.kr:443 -servername apis.data.go.kr
  • Python 런타임의 OpenSSL 버전: python -c "import ssl; print(ssl.OPENSSL_VERSION)"

해결 방법

핵심은 서버가 수용 가능한 TLS 파라미터로 낮춰 호출하는 것입니다.

  • TLS 1.2로 고정
  • OpenSSL 보안 레벨 완화: DEFAULT:@SECLEVEL=1
  • 신뢰 저장소 명시: certifi의 CA 번들 사용
  • (선택) 연결 안정화를 위해 Connection: close

안전한 구현 예시

도메인 전용 세션을 만들어 필요한 범위에서만 정책을 완화합니다. 전역 설정은 바꾸지 않습니다.

import ssl
import certifi
import requests
from requests.adapters import HTTPAdapter

class TLS12Adapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        ctx = ssl.create_default_context()
        try:
            ctx.minimum_version = ssl.TLSVersion.TLSv1_2
            ctx.maximum_version = ssl.TLSVersion.TLSv1_2
            ctx.set_ciphers("DEFAULT:@SECLEVEL=1")
        except Exception:
            pass
        kwargs["ssl_context"] = ctx
        return super().init_poolmanager(*args, **kwargs)

def http_get_json_compat(url: str, timeout: int = 10) -> dict:
    headers = {"Accept": "*/*", "Connection": "close"}
    session = requests.Session()
    session.mount("https://", TLS12Adapter())
    try:
        resp = session.get(
            url,
            headers=headers,
            timeout=timeout,
            allow_redirects=True,
            verify=certifi.where(),
        )
        resp.raise_for_status()
        return resp.json()
    finally:
        session.close()

관련 리소스