블로그로 돌아가기

SSL/TLS 프록시 문제 해결: 완벽한 가이드

SSL/TLS 프록시 작업 시 발생하는 일반적인 오류 분석: 인증서 문제부터 HTTPS 터널 설정까지. 코드 예제를 포함한 실용적인 해결책.

📅2025년 12월 16일
```html

프록시 사용 시 SSL/TLS 문제 해결: 완전 가이드

SSL/TLS 오류는 프록시 서버를 통한 작업 시 가장 흔한 문제 중 하나입니다. 잘못된 설정은 연결 실패, 데이터 유출 및 보안 리소스 파싱 불가능으로 이어집니다. 이 가이드에서는 오류의 원인과 구체적인 코드 예제를 통한 해결 방법을 살펴보겠습니다.

프록시를 통한 SSL/TLS 작동 원리

문제를 해결하기 전에 프록시를 통한 보안 연결의 메커니즘을 이해하는 것이 중요합니다. HTTPS 트래픽을 프록시하는 두 가지 근본적으로 다른 접근 방식이 있으며, 각각 고유한 특성, 장점 및 잠재적 장애점을 가지고 있습니다.

투명한 터널링 (CONNECT)

CONNECT 메서드를 사용할 때 프록시 서버는 클라이언트와 대상 서버 간에 TCP 터널을 생성합니다. 프록시는 트래픽 내용을 볼 수 없으며, 단순히 양쪽으로 암호화된 바이트를 전달합니다. 이것이 프록시를 통한 HTTPS 작업에 가장 안전하고 일반적인 방법입니다.

연결 설정 프로세스는 다음과 같습니다: 클라이언트는 대상 호스트 및 포트를 지정하여 프록시에 CONNECT 요청을 보냅니다. 프록시는 대상 서버와 TCP 연결을 설정하고 클라이언트에 200 Connection Established 응답을 반환합니다. 그 후 클라이언트는 설정된 터널을 통해 대상 서버와 직접 TLS 핸드셰이크를 수행합니다.

# CONNECT 터널 스키마
클라이언트 → 프록시: CONNECT example.com:443 HTTP/1.1
프록시 → 서버: [포트 443에 TCP 연결]
프록시 → 클라이언트: HTTP/1.1 200 Connection Established
클라이언트 ↔ 서버: [터널을 통한 TLS 핸드셰이크]
클라이언트 ↔ 서버: [암호화된 트래픽]

MITM 프록시 (가로채기)

일부 프록시(특히 기업용)는 중간자 공격 기술을 사용합니다: 자신의 측에서 TLS 연결을 종료하고, 트래픽을 복호화한 다음, 대상 서버와 새로운 TLS 연결을 설정합니다. 이를 위해 프록시는 각 도메인에 대해 프록시의 루트 인증서로 서명된 자체 인증서를 생성합니다.

이 접근 방식은 HTTPS 트래픽을 검사하고 필터링할 수 있지만, 클라이언트의 신뢰할 수 있는 저장소에 프록시의 루트 인증서를 설치해야 합니다. 프록시를 통한 SSL/TLS 작업 시 대부분의 문제가 바로 여기서 발생합니다.

일반적인 오류 및 원인

프록시를 통한 작업 시 가장 흔한 SSL/TLS 오류를 살펴보겠습니다. 각 오류의 원인을 이해하는 것이 빠른 문제 해결의 핵심입니다.

SSL: CERTIFICATE_VERIFY_FAILED

이 오류는 클라이언트가 서버 인증서의 진위를 확인할 수 없음을 의미합니다. 원인은 여러 가지일 수 있습니다: 프록시가 설치되지 않은 루트 인증서로 MITM 검사를 사용하거나, 대상 서버의 인증서가 만료되었거나 자체 서명되었거나, 인증서 체인에 중간 인증서가 없을 수 있습니다.

# Python - 일반적인 오류 메시지
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] 
certificate verify failed: unable to get local issuer certificate

# Node.js
Error: unable to verify the first certificate

# cURL
curl: (60) SSL certificate problem: unable to get local issuer certificate

SSL: WRONG_VERSION_NUMBER

클라이언트가 TLS 연결을 설정하려고 하지만 예상치 못한 형식의 응답을 받을 때 오류가 발생합니다. 대부분의 경우 프록시가 CONNECT 메서드를 지원하지 않고 HTTPS 요청을 일반 HTTP로 처리하려고 할 때 발생합니다. 또한 잘못된 포트 또는 프로토콜이 원인일 수 있습니다.

TLSV1_ALERT_PROTOCOL_VERSION

서버가 호환되지 않는 TLS 프로토콜 버전으로 인해 연결을 거부합니다. 최신 서버는 보안상의 이유로 TLS 1.0 및 TLS 1.1의 구식 버전을 비활성화합니다. 프록시 또는 클라이언트가 이전 버전을 사용하도록 설정된 경우 연결이 거부됩니다.

SSLV3_ALERT_HANDSHAKE_FAILURE

클라이언트와 서버가 암호화 매개변수를 협상할 수 없습니다. 이는 공통 cipher suite 부재, SNI(Server Name Indication) 문제 또는 암호화 라이브러리 비호환성으로 인해 발생할 수 있습니다.

오류 가능한 원인 솔루션
CERTIFICATE_VERIFY_FAILED MITM 프록시, 만료된 인증서 프록시 루트 인증서 설치
WRONG_VERSION_NUMBER HTTPS 대신 HTTP, CONNECT 미지원 프록시 설정 및 프로토콜 확인
PROTOCOL_VERSION 구식 TLS 버전 최소 TLS 버전 업데이트
HANDSHAKE_FAILURE 호환되지 않는 cipher suite 암호 목록 구성

인증서 문제

인증서는 프록시를 통한 작업 시 가장 흔한 문제의 원인입니다. 주요 시나리오와 해결 방법을 살펴보겠습니다.

프록시 루트 인증서 설치

프록시가 MITM 검사를 사용하는 경우 프록시의 루트 인증서를 신뢰할 수 있는 저장소에 추가해야 합니다. 프로세스는 운영 체제 및 사용된 프로그래밍 언어에 따라 다릅니다.

# Linux: 시스템 저장소에 인증서 추가
sudo cp proxy-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

# macOS: Keychain에 추가
sudo security add-trusted-cert -d -r trustRoot \
    -k /Library/Keychains/System.keychain proxy-ca.crt

# Windows PowerShell (관리자 권한)
Import-Certificate -FilePath "proxy-ca.crt" `
    -CertStoreLocation Cert:\LocalMachine\Root

코드에서 인증서 지정

때로는 코드에서 직접 인증서 경로를 지정하는 것이 더 편리합니다. 특히 컨테이너나 루트 액세스 없는 서버에서 작업할 때 시스템 설정 수정을 피할 수 있습니다.

# Python: 사용자 정의 CA-bundle 지정
import requests

# 방법 1: 인증서 파일 경로 지정
response = requests.get(
    'https://example.com',
    proxies={'https': 'http://proxy:8080'},
    verify='/path/to/proxy-ca.crt'
)

# 방법 2: 시스템 인증서와 결합
import certifi
import ssl

# 통합 인증서 파일 생성
with open('combined-ca.crt', 'w') as combined:
    with open(certifi.where()) as system_certs:
        combined.write(system_certs.read())
    with open('proxy-ca.crt') as proxy_cert:
        combined.write(proxy_cert.read())

response = requests.get(url, verify='combined-ca.crt')

인증서 확인 비활성화

일부 경우(테스트 및 디버깅만!) 인증서 확인을 비활성화해야 할 수 있습니다. 이는 안전하지 않으며 프로덕션에서 사용하면 안 됩니다. MITM 공격에 연결이 취약해지기 때문입니다.

⚠️ 경고: 인증서 확인 비활성화는 연결을 가로채기에 취약하게 만듭니다. 격리된 환경에서만 디버깅용으로 사용하고, 절대 프로덕션에서 사용하지 마세요!

# Python - 디버깅 목적으로만!
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

response = requests.get(
    'https://example.com',
    proxies={'https': 'http://proxy:8080'},
    verify=False  # 안전하지 않음!
)

CONNECT 터널 설정

CONNECT 터널의 올바른 설정은 프록시를 통한 HTTPS의 안정적인 작동의 핵심입니다. 다양한 사용 시나리오에 대한 설정 특성을 살펴보겠습니다.

CONNECT 지원 확인

모든 프록시가 CONNECT 메서드를 지원하는 것은 아닙니다. HTTP 프록시는 HTTP 트래픽 프록시만 가능하도록 설정될 수 있습니다. 사용 전에 프록시가 HTTPS 터널링을 지원하는지 확인하세요.

# netcat/telnet을 통한 CONNECT 지원 확인
echo -e "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n" | \
    nc proxy.example.com 8080

# 성공 시 예상 응답:
# HTTP/1.1 200 Connection Established

# 오류 시 가능한 응답:
# HTTP/1.1 403 Forbidden - CONNECT 금지됨
# HTTP/1.1 405 Method Not Allowed - 메서드 미지원

CONNECT 요청의 인증

인증이 필요한 프록시를 사용할 때는 자격 증명을 올바르게 전달하는 것이 중요합니다. Proxy-Authorization 헤더는 CONNECT 요청에 있어야 합니다.

# Python: URL을 통한 인증
import requests

proxy_url = 'http://username:password@proxy.example.com:8080'
response = requests.get(
    'https://target.com',
    proxies={'https': proxy_url}
)

# Python: 헤더를 통한 인증 (복잡한 경우)
import base64

credentials = base64.b64encode(b'username:password').decode('ascii')
session = requests.Session()
session.headers['Proxy-Authorization'] = f'Basic {credentials}'
session.proxies = {'https': 'http://proxy.example.com:8080'}

response = session.get('https://target.com')

SOCKS5 프록시 작업

SOCKS5 프록시는 더 낮은 수준에서 작동하며 HTTPS의 특별한 처리가 필요하지 않습니다. 단순히 TCP 연결을 터널링하므로 모든 프로토콜에 이상적입니다.

# PySocks를 사용한 Python
import requests

proxies = {
    'http': 'socks5h://user:pass@proxy:1080',
    'https': 'socks5h://user:pass@proxy:1080'
}

# socks5h - 프록시를 통한 DNS 해석
# socks5 - 로컬 DNS 해석

response = requests.get('https://example.com', proxies=proxies)

다양한 프로그래밍 언어의 솔루션

각 언어와 라이브러리는 프록시를 통한 SSL/TLS 작업에 고유한 특성을 가지고 있습니다. 가장 일반적인 옵션을 완전한 코드 예제와 함께 살펴보겠습니다.

Python (requests + urllib3)

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
import ssl

class TLSAdapter(HTTPAdapter):
    """사용자 정의 TLS 매개변수가 있는 어댑터"""
    
    def __init__(self, ssl_context=None, **kwargs):
        self.ssl_context = ssl_context
        super().__init__(**kwargs)
    
    def init_poolmanager(self, *args, **kwargs):
        if self.ssl_context:
            kwargs['ssl_context'] = self.ssl_context
        return super().init_poolmanager(*args, **kwargs)

# 최신 설정으로 컨텍스트 생성
ctx = create_urllib3_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.set_ciphers('ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20')

# 추가 인증서 로드
ctx.load_verify_locations('proxy-ca.crt')

session = requests.Session()
session.mount('https://', TLSAdapter(ssl_context=ctx))
session.proxies = {
    'http': 'http://proxy:8080',
    'https': 'http://proxy:8080'
}

response = session.get('https://example.com')
print(response.status_code)

Node.js (axios + https-proxy-agent)

const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const fs = require('fs');
const https = require('https');

// 추가 CA 인증서 로드
const customCA = fs.readFileSync('proxy-ca.crt');

const agent = new HttpsProxyAgent({
    host: 'proxy.example.com',
    port: 8080,
    auth: 'username:password',
    ca: customCA,
    rejectUnauthorized: true  // 프로덕션에서는 비활성화하지 마세요!
});

const client = axios.create({
    httpsAgent: agent,
    proxy: false  // 중요: axios 내장 프록시 비활성화
});

async function fetchData() {
    try {
        const response = await client.get('https://example.com');
        console.log(response.data);
    } catch (error) {
        if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
            console.error('인증서 문제:', error.message);
        }
        throw error;
    }
}

fetchData();

Go (net/http)

package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "net/http"
    "net/url"
    "log"
)

func main() {
    // 추가 인증서 로드
    caCert, err := ioutil.ReadFile("proxy-ca.crt")
    if err != nil {
        log.Fatal(err)
    }
    
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)
    
    // TLS 설정
    tlsConfig := &tls.Config{
        RootCAs:    caCertPool,
        MinVersion: tls.VersionTLS12,
    }
    
    // 프록시 설정
    proxyURL, _ := url.Parse("http://user:pass@proxy:8080")
    
    transport := &http.Transport{
        Proxy:           http.ProxyURL(proxyURL),
        TLSClientConfig: tlsConfig,
    }
    
    client := &http.Client{Transport: transport}
    
    resp, err := client.Get("https://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    
    log.Printf("상태: %s", resp.Status)
}

cURL (명령줄)

# 프록시를 통한 기본 요청
curl -x http://proxy:8080 https://example.com

# 인증 포함
curl -x http://proxy:8080 -U username:password https://example.com

# CA 인증서 지정
curl -x http://proxy:8080 --cacert proxy-ca.crt https://example.com

# TLS 1.2 이상 강제
curl -x http://proxy:8080 --tlsv1.2 https://example.com

# TLS 핸드셰이크 디버깅
curl -x http://proxy:8080 -v --trace-ascii - https://example.com 2>&1 | \
    grep -E "(SSL|TLS|certificate)"

MITM 검사 감지 및 우회

일부 네트워크(기업, 공개 Wi-Fi)는 HTTPS 트래픽의 투명한 MITM 검사를 사용합니다. 이는 certificate pinning 문제를 야기할 수 있으며 특정 인증서를 기대하는 애플리케이션의 작동을 방해합니다.

MITM 프록시 감지

import ssl
import socket

def check_certificate_issuer(hostname, port=443):
    """웹사이트 인증서 발급자 확인"""
    context = ssl.create_default_context()
    
    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert = ssock.getpeercert()
            
            issuer = dict(x[0] for x in cert['issuer'])
            subject = dict(x[0] for x in cert['subject'])
            
            print(f"주체: {subject.get('commonName')}")
            print(f"발급자: {issuer.get('commonName')}")
            print(f"조직: {issuer.get('organizationName')}")
            
            # 알려진 기업 MITM 프록시
            mitm_indicators = [
                'Zscaler', 'BlueCoat', 'Symantec', 'Fortinet',
                'Palo Alto', 'McAfee', 'Cisco', 'Corporate'
            ]
            
            issuer_str = str(issuer)
            for indicator in mitm_indicators:
                if indicator.lower() in issuer_str.lower():
                    print(f"⚠️ MITM 프록시 감지됨: {indicator}")
                    return True
            
            return False

# 확인
check_certificate_issuer('google.com')

MITM 조건에서의 작업

MITM 프록시가 감지되면 여러 전략이 있습니다. 신뢰할 수 있는 기업 네트워크인 경우 프록시의 루트 인증서를 설치하거나, 대체 포트 또는 프로토콜을 사용하거나, VPN을 사용하여 검사를 우회할 수 있습니다.

# MITM 프록시 인증서 획득 및 설치
openssl s_client -connect example.com:443 -proxy proxy:8080 \
    -showcerts 2>/dev/null | \
    openssl x509 -outform PEM > mitm-cert.pem

# 인증서 정보 보기
openssl x509 -in mitm-cert.pem -text -noout | head -20

진단 도구

올바른 진단은 문제 해결의 절반입니다. SSL/TLS 연결 디버깅을 위한 도구와 방법을 살펴보겠습니다.

진단용 OpenSSL

# 프록시를 통한 연결 확인
openssl s_client -connect example.com:443 \
    -proxy proxy.example.com:8080 \
    -servername example.com

# 인증서 체인 확인
openssl s_client -connect example.com:443 \
    -proxy proxy:8080 \
    -showcerts 2>/dev/null | \
    grep -E "(Certificate chain|s:|i:)"

# 특정 TLS 버전 테스트
openssl s_client -connect example.com:443 \
    -proxy proxy:8080 \
    -tls1_2

# 지원되는 cipher suite 확인
openssl s_client -connect example.com:443 \
    -proxy proxy:8080 \
    -cipher 'ECDHE-RSA-AES256-GCM-SHA384'

진단용 Python 스크립트

import ssl
import socket
import requests
from urllib.parse import urlparse

def diagnose_ssl_proxy(target_url, proxy_url):
    """프록시를 통한 SSL의 포괄적인 진단"""
    
    print(f"🔍 진단: {target_url}")
    print(f"📡 프록시: {proxy_url}\n")
    
    # 1. 프록시 가용성 확인
    proxy = urlparse(proxy_url)
    try:
        sock = socket.create_connection(
            (proxy.hostname, proxy.port), 
            timeout=10
        )
        sock.close()
        print("✅ 프록시 사용 가능")
    except Exception as e:
        print(f"❌ 프록시 사용 불가: {e}")
        return
    
    # 2. CONNECT 메서드 확인
    try:
        response = requests.get(
            target_url,
            proxies={'https': proxy_url},
            timeout=15
        )
        print(f"✅ HTTPS 연결 성공: {response.status_code}")
    except requests.exceptions.SSLError as e:
        print(f"❌ SSL 오류: {e}")
        
        # 진단을 위해 확인 비활성화 시도
        try:
            response = requests.get(
                target_url,
                proxies={'https': proxy_url},
                verify=False,
                timeout=15
            )
            print("⚠️ 인증서 확인 없이 연결 작동")
            print("   프록시 CA 인증서 문제일 가능성")
        except Exception as e2:
            print(f"❌ 확인 비활성화 후에도 실패: {e2}")
    
    except Exception as e:
        print(f"❌ 연결 오류: {e}")
    
    # 3. SSL 정보
    print(f"\n📋 OpenSSL 버전: {ssl.OPENSSL_VERSION}")
    print(f"📋 지원되는 프로토콜: {ssl.get_default_verify_paths()}")

# 사용
diagnose_ssl_proxy(
    'https://httpbin.org/ip',
    'http://proxy:8080'
)

심층 분석을 위한 Wireshark

복잡한 경우 TLS 트래픽 필터를 사용하여 Wireshark를 사용하세요. 이를 통해 핸드셰이크의 세부 사항을 보고 정확한 실패 지점을 파악할 수 있습니다.

# TLS 분석용 Wireshark 필터
tls.handshake.type == 1          # Client Hello
tls.handshake.type == 2          # Server Hello
tls.handshake.type == 11         # Certificate
tls.alert_message                 # TLS 알림 (오류)

# tcpdump를 사용한 트래픽 캡처
tcpdump -i eth0 -w ssl_debug.pcap \
    'port 443 or port 8080' -c 1000

안전한 작업의 모범 사례

모범 사례를 따르면 SSL/TLS 문제의 대부분을 피할 수 있으며 프록시를 통한 작업 시 데이터 보안을 보장합니다.

TLS 설정

  • 최소 TLS 1.2 사용, 가능하면 TLS 1.3 사용
  • 최신 cipher suite 설정 (ECDHE, AES-GCM, ChaCha20)
  • 인증서 확인 활성화 (프로덕션에서는 절대 비활성화하지 마세요)
  • CA-bundle 및 암호화 라이브러리 정기 업데이트

프록시 작업

  • HTTPS 트래픽의 경우 CONNECT 메서드 지원 프록시 선호
  • 범용 터널링을 위해 SOCKS5 사용
  • 프록시 작업 시 주거용 프록시 사용 시 ISP 네트워크의 특수성 고려
  • 프록시 자격 증명을 환경 변수에 저장하고 코드에는 저장하지 마세요

오류 처리

import requests
from requests.exceptions import SSLError, ProxyError, ConnectionError
import time

def robust_request(url, proxy, max_retries=3):
    """오류에 강한 프록시를 통한 요청"""
    
    for attempt in range(max_retries):
        try:
            response = requests.get(
                url,
                proxies={'https': proxy},
                timeout=30
            )
            return response
            
        except SSLError as e:
            print(f"SSL 오류 (시도 {attempt + 1}): {e}")
            # SSL 오류는 일반적으로 재시도로 해결되지 않음
            if 'CERTIFICATE_VERIFY_FAILED' in str(e):
                raise  # 설정 수정 필요
                
        except ProxyError as e:
            print(f"프록시 오류 (시도 {attempt + 1}): {e}")
            time.sleep(2 ** attempt)  # 지수 백오프
            
        except ConnectionError as e:
            print(f"연결 오류 (시도 {attempt + 1}): {e}")
            time.sleep(2 ** attempt)
    
    raise Exception(f"{max_retries}회 시도 후 요청 실패")

문제 해결 체크리스트

SSL/TLS 오류 발생 시 다음 진단 순서를 따르세요:

  1. 프록시 서버 가용성 확인 (ping, telnet)
  2. CONNECT 메서드 지원 확인
  3. 인증 자격 증명 정확성 확인
  4. openssl s_client를 통한 인증서 검사
  5. MITM 검사 여부 확인
  6. 필요한 CA 인증서 설치
  7. TLS 버전 및 cipher suite 업데이트

💡 팁: 프록시를 통한 SSL/TLS 문제의 대부분은 인증서와 관련이 있습니다. 인증서 체인 확인으로 진단을 시작하고 모든 중간 인증서가 있는지 확인하세요.

결론

프록시를 통한 SSL/TLS 문제는 체계적인 진단 접근으로 해결할 수 있습니다. 핵심 요점: CONNECT 터널링과 MITM 프록시의 차이 이해, 인증서의 올바른 설정, 최신 TLS 프로토콜 버전 사용. 대부분의 오류는 필요한 CA 인증서 부재 또는 암호화 매개변수 비호환성과 관련이 있습니다.

파싱 및 자동화 작업으로 안정적인 HTTPS 작업이 필요한 경우 CONNECT 메서드를 완벽하게 지원하는 고품질 프록시 사용을 권장합니다. 자세한 내용은 proxycove.com에서 확인할 수 있습니다.

```