VPN은 연결되어 있고 내부 대시보드는 브라우저에서 열리는데 Python 스크립트는 여전히 프록시 오류를 냅니다. 그 그림에서 터널에는 아무 문제가 없습니다: 인터프리터가 자체 환경에서 프록시 주소를 읽어 요청을 그곳으로 보내고 있을 뿐입니다. 이 런북은 프로세스가 실제로 보는 환경을 읽고, 가장 좁은 제외를 적용한 뒤에야 명시적 세션 정책을 고려합니다. 모든 단계는 출력으로 계속할지 결정하는 명령으로 끝납니다.

프로세스가 실제로 보는 환경을 읽으세요

데스크톱 프록시 설정, 셸 내보내기, systemd 유닛의 Environment= 줄은 서로 다른 세 소스이며 Requests는 상속받은 마지막 하나만 읽습니다. 아래 진단을 실패하는 스크립트를 실행하는 동일한 셸, 컨테이너, 유닛에서 실행하세요(대화형 터미널에서는 아닙니다). 로그인 셸에서 내보낸 변수는 컨테이너 프로세스에 보이지 않으며, 그 간극이 어디서 나타났는지 모를 프록시에 대한 많은 보고를 설명합니다. 두 명령은 같은 데이터를 다른 상세 수준으로 출력합니다.

env | grep -i -E 'proxy|requests_ca_bundle|curl_ca_bundle'
python3 -c "import os;[print(k,'=',v) for k,v in sorted(os.environ.items()) if 'proxy' in k.lower()]"

각 변수의 두 가지 표기를 모두 읽고, 중복 항목은 프로세스가 받은 순서대로 적용되므로 오래된 HTTP_PROXY가 방금 내보낸 값을 덮어쓸 수 있습니다. CA 번들 변수도 여기서 중요합니다: 코드 어디에도 나타나지 않으면서 verify가 무엇으로 해석되는지 바꾸기 때문입니다. 출력이 비어 있다면 환경은 문제가 아니며 이 절을 그만 읽어도 됩니다. 모르는 프록시가 나열된다면 무엇이든 바꾸기 전에 그것을 설정하는 파일이나 유닛을 찾으세요.

특정 URL에 대해 Requests가 무엇을 할지 물어보세요. 이 호출은 패킷을 보내지 않고 해당 요청의 병합된 프록시, verify, cert 값을 보여 줍니다.

python3 -c "import requests;s=requests.Session();print(s.merge_environment_settings('https://api.corp.example/status',{},None,None,None))"

프라이빗 호스트에 대한 비어 있지 않은 proxies 항목이 답이며, 출력된 verify 값은 요청이 어느 CA 번들을 신뢰할지 알려 줍니다. 둘 다 올바르게 보인다면 실패는 스택 뒤쪽에 있으므로 검증 절로 넘어가야 합니다. 코드에서 추측하지 마세요: 병합은 요청마다 일어나므로 한 세션이 두 호스트에 다르게 동작할 수 있습니다.

무엇이든 비활성화하기 전에 프라이빗 호스트를 제외하세요

다른 목적지에 프록시가 정말 필요하다면 좁은 수정은 이 호스트에 대한 제외입니다. 기존 목록을 교체하지 말고 추가하세요: NO_PROXY를 지우면 그것에 의존하던 다른 모든 내부 목적지가 깨집니다. 아래 명령은 항목을 추가한 뒤 우회가 적용되는지 Requests에 묻습니다.

export NO_PROXY="${NO_PROXY:+$NO_PROXY,}api.corp.example,.corp.example"
python3 -c "import requests;print(requests.utils.should_bypass_proxies('https://api.corp.example/status',None))"

일치는 호스트 접미사로 이루어지므로 .corp.example는 전체 도메인을, api.corp.example는 이름 하나를 포함합니다. 맨 별표는 모든 호스트에 대해 프록시를 끄며 프로덕션 작업이 바라는 경우는 거의 없습니다. CIDR 항목이 존중되는지는 이미지의 Requests와 urllib3 버전에 달려 있으므로 작동한다고 가정하지 말고 정확한 항목을 시험하세요. should_bypass_proxies의 결과는 기억으로 입력한 호스트 이름이 아니라 실제 URL로 확인하세요.

두 계층을 머릿속에서 분리하세요. HTTP 프록시를 우회해도 VPN을 우회하지 않습니다: 프록시 결정은 라이브러리에서, 터널 결정은 커널 라우팅 테이블에서 일어나기 때문입니다.

trust_env는 의도적인 세션 정책으로만 비활성화하세요

작업 전체가 주변 구성을 무시해야 한다면 세션을 명시적으로 만들고 그 결정을 코드에 기록하세요. 그것은 정책이지 우회가 아니며 검토자가 볼 수 있는 diff에 속합니다. 아래 예시는 타임아웃, 명시적 CA 경로, 상태 확인을 유지합니다.

import requests

with requests.Session() as session:
    session.trust_env = False
    response = session.get(
        "https://api.corp.example/status",
        timeout=(5, 20),
        verify="/etc/ssl/certs/corp-ca.pem",
    )
    response.raise_for_status()

trust_env를 끄면 세 가지 동작이 한꺼번에 바뀌며 그중 하나만 프록시에 관한 것입니다. Requests는 .netrc 자격 증명을 읽지 않고, REQUESTS_CA_BUNDLE 및 CURL_CA_BUNDLE 변수는 verify에 영향을 주지 않으며, 그 검증은 내장된 인증서 저장소로 폴백합니다. 조직이 자체 CA로 TLS를 종료한다면 그 폴백은 방금 한 변경과 무관해 보이는 검증 오류를 냅니다. verify를 명시적으로 전달하는 것이 두 효과를 분리해 줍니다.

하지 말아야 할 일은 결과 오류를 잠재우기 위해 verify=False를 설정하는 것입니다. 그것은 프록시 설정 오류를 검증되지 않은 연결과 맞바꾸고, 제외가 드러내야 했던 인증서 문제를 숨깁니다.

실패하는 계층을 검증하고 롤백을 마련하세요

세 가지 실패는 로그에서 비슷해 보이고 공통점이 없습니다: 목적지에 도달하지 못하는 프록시는 ProxyError를 던지거나 502를 반환하고, 신뢰 문제는 SSLError를 던지며, 애플리케이션 거부는 401 또는 403을 반환합니다. Requests 밖에서 요청을 재현해 어느 것인지 확인하세요. 아래 명령은 프록시를 제거하고 인증서 체인을 시험한 뒤 라이브러리를 통해 호출을 반복합니다.

curl -sv --noproxy '*' https://api.corp.example/status -o /dev/null
openssl s_client -connect api.corp.example:443 -servername api.corp.example </dev/null
python3 -c "import requests;r=requests.get('https://api.corp.example/status',timeout=10);print(r.status_code)"

curl 호출이 성공하고 Python 호출이 실패한다면 차이는 여전히 환경입니다. 둘 다 TLS 단계에서 실패한다면 프록시는 문제가 아니었고 인증서 체인이 문제입니다. 둘 다 401을 반환한다면 요청이 애플리케이션에 도달했고 남은 작업은 인증입니다. 셋 중 어느 것을 관찰했는지 기록하면 다음 사람의 한 시간을 아낍니다.

롤백은 어느 방향이든 한 줄입니다: 제외를 제거하거나 trust_env를 되돌리세요. 추가하기 전에 이전 NO_PROXY 값을 저장하고, 터미널 기록에만 사는 내보내기보다 래퍼 스크립트나 유닛 수준의 Environment= 항목을 선호하세요. 두 변경 모두 프로세스 환경에 보이므로 보통 재시작하면 이전 동작으로 돌아갑니다.

네 가지를 확인하며 마무리하세요: 프라이빗 URL의 병합 설정에 프록시가 없어야 하고, 인증서가 의도한 CA로 검증되며, 요청이 기대하는 상태를 반환하고, 변경이 재현 가능한 곳에 커밋되어 있어야 합니다. 설명되지 않은 환경을 가진 초록색 스크립트는 다음 머신에서 다시 깨지는 수정입니다. 이 런북의 명령이 티켓에 붙여 넣을 명령입니다.

관련 글