FDE 캡스톤: 창고가 같은 주문을 세 번 받았다 · 사내 프록시 뒤에서 연동하기 · 이론
NO_PROXY 는 표준이 아니다 — 도구마다 다른 세 가지 해석
한 줄 요약
프록시 환경 변수는 약속일 뿐 표준이 아니어서, 같은 NO_PROXY 한 줄을 curl·Python urllib·requests 가 서로 다르게 읽는다. 고객 현장에서는 설정을 "맞게" 쓰는 것보다 도구별로 실제 길을 재서 증거로 남기는 것이 먼저다.
왜 이게 필요했나
납품 첫날, 고객 보안팀이 설정 세 줄을 보내 왔다. "사내 표준입니다. 이대로 넣으세요."
HTTP_PROXY=http://127.0.0.1:3128HTTPS_PROXY=http://127.0.0.1:3128NO_PROXY=corp.example,127.0.0.0/8넣고 나서 연동기의 헬스체크(curl)는 초록불인데, 파이썬으로 짠 지표 수집기는 사내 API 에서 403 을 받기 시작했다. 바깥 SaaS 로 나가야 할 requests 호출은 오히려 프록시를 건너뛰어 이름 풀이에서 죽었다. 같은 세 줄을 읽었는데 세 프로그램이 세 가지로 움직인 것이다. 누가 틀린 게 아니다. http_proxy 는 여러 도구가 함께 쓰는 관습일 뿐이고, 아래에서 인용하는 curl·Python·requests 문서도 각자 자기 규칙만 설명한다. 공통 문법을 정해 둔 곳이 없으니 구현마다 규칙이 조금씩 다르고, 그 차이가 고객 망 안에서 한꺼번에 드러난다.
FDE 가 이 상황에서 "설정을 이렇게 바꾸면 됩니다" 라고 먼저 말하면 위험하다. 고객 쪽 담당자는 curl 로 확인하고 "우리 쪽은 정상" 이라고 답할 것이고, 그 말도 사실이기 때문이다. 필요한 것은 어느 요청이 프록시를 탔는지 기록으로 보여 주는 증거다.
어떻게 동작하나
아래는 이 실습 이미지에서 기록하는 가짜 프록시를 띄워 놓고 직접 잰 결과다(실측: curl 8.5.0, Python 3.12.3, requests 2.32.3).
1. 변수 이름의 대소문자. [curl 매뉴얼](https://curl.se/docs/manpage.html)의 ENVIRONMENT 절은 소문자와 대문자를 모두 읽되 http_proxy 만은 소문자로만 받는다고 적는다. 이 예외가 왜 필요한지는 [urllib.request 문서](https://docs.python.org/3/library/urllib.request.html)가 설명한다 — CGI 환경에서는 클라이언트가 보낸 Proxy: 헤더가 HTTP_PROXY 라는 환경 변수로 주입될 수 있다. 그래서 Python 은 REQUEST_METHOD 가 설정된 CGI 환경에서만 대문자 HTTP_PROXY 를 무시하고, 평소에는 대문자도 받는다. 실측으로도 그렇게 동작했다. 결과적으로 대문자 HTTP_PROXY 하나만 넣으면 curl 은 직접, 파이썬 둘은 프록시로 간다. 소문자와 대문자가 둘 다 있으면 소문자가 이긴다 — 이건 curl 매뉴얼과 urllib 문서가 같은 말을 한다.
ALL_PROXY 도 갈린다. curl 매뉴얼은 스킴별 변수가 없을 때 이것을 쓴다고 적고, requests 문서도 all_proxy 를 읽는 변수로 꼽는다. 실측에서 둘은 프록시로 갔지만 urllib 은 ALL_PROXY 만 있을 때 직접 나갔다.
2. NO_PROXY 의 이름 맞추기. curl 매뉴얼은 각 항목을 "그 호스트 자신이거나 그 도메인에 속한 이름" 으로 맞춘다고 설명하며, local.com 이 www.local.com 은 맞추지만 www.notlocal.com 은 맞추지 않는다고 예를 든다. urllib 도 점 경계를 지킨다. requests 는 다르다 — 실측에서 NO_PROXY=corp.example 이 saascorp.example 까지 직접 보냈다. 문자열 끝이 같은지만 보기 때문이다. 선행 점(.corp.example)은 세 도구 모두 api.corp.example 과 corp.example 자신을 맞추고 saascorp.example 은 맞추지 않았다. 반면 *.corp.example 처럼 별표를 붙인 항목은 세 도구 모두 아무것도 맞추지 못했다. 별표는 목록 전체가 * 한 글자일 때만 "전부 직접" 의 뜻이고, *,localhost 처럼 섞으면 별표가 힘을 잃는다(실측).
3. IP·CIDR·포트. curl 은 7.86.0 부터 127.0.0.0/8 같은 CIDR 표기를 받는다고 매뉴얼에 적혀 있다. requests 도 IP 주소 호스트에는 CIDR 을 적용했다. urllib 은 CIDR 을 모른다 — 문자열로만 비교해서 127.0.0.2 를 프록시로 보냈다. 포트는 거꾸로다. urllib 문서는 some.host:8080 처럼 포트를 붙인 항목을 허용한다고 적고 실제로 맞췄지만, curl 은 포트 붙은 항목을 맞추지 못했고, requests 는 IP 에 포트를 붙이면 못 맞추고 이름에 포트를 붙이면 맞췄다. 또 NO_PROXY=127.0.0.1 은 localhost 를 맞추지 않았다. 세 도구 모두 이름을 주소로 풀어서 비교하지 않았다는 뜻이다(실측).
4. 코드가 넘긴 프록시. [requests 문서의 Proxies 절](https://requests.readthedocs.io/en/latest/user/advanced/#proxies)은 session.proxies 에 넣은 값이 환경 변수의 프록시에 덮일 수 있으니, 확실히 쓰려면 요청마다 proxies 인자로 넘기라고 권한다. 그런데 요청마다 넘긴 proxies 에는 NO_PROXY 가 적용되지 않았다(실측). 설정 파일의 프록시를 인자로 넘기는 연동기는 NO_PROXY 를 아무리 고쳐도 사내 API 로 직접 가지 못한다.
| 같은 조건 | curl | urllib | requests |
| --- | --- | --- | --- |
| 대문자 HTTP_PROXY 만 | 직접 | 프록시 | 프록시 |
| NO_PROXY=corp.example → saascorp.example | 프록시 | 프록시 | 직접 |
| NO_PROXY=127.0.0.0/8 → 127.0.0.2 | 직접 | 프록시 | 직접 |
| NO_PROXY=127.0.0.2:8080 → 127.0.0.2:8080 | 프록시 | 직접 | 프록시 |
현장에서 만나는 모습
고객 망에서는 원인이 늘 다른 증상으로 보인다. 프록시가 사내 목적지를 403 으로 거절하면 "API 인증이 깨졌다" 로 보이고, 바깥 호출이 프록시를 건너뛰면 "DNS 가 이상하다" 로 보인다. 헬스체크가 curl 이라 초록불이면 모두가 애플리케이션을 의심한다. 이럴 때 프록시 접근 기록 한 줄이 논쟁을 끝낸다 — 그 요청이 프록시에 도착했는가, 아닌가.
두 번째로 흔한 모습은 "보안팀 표준" 과 "도구의 현실" 이 부딪치는 것이다. 표준 문서에 CIDR 로 적힌 사내 대역을 urllib 기반 도구가 이해하지 못한다고 해서 표준을 고치라고 할 수는 없다. FDE 는 표준을 지키면서 세 도구가 모두 이해하는 공통분모(선행 점 도메인, 명시한 IP 목록, 소문자와 대문자 모두)를 한 벌 더 적어 넣고, 그 이유를 기록으로 남긴다.
실무에서 진짜 중요한 것
- 설정을 추론하지 말고 잰다. 프록시 쪽에 기록이 남는 가짜 프록시를 로컬에 띄우면, 고객 프록시에 접근 권한이 없어도 도구별 판단을 확인할 수 있다.
- 공통분모로 쓴다: 소문자·대문자 둘 다, 도메인은 선행 점, IP 는 CIDR 과 함께 개별 주소도, 포트는 붙이지 않는다.
- 코드가 프록시를 직접 넘기면
NO_PROXY는 코드가 챙겨야 한다. 환경의 다른 프록시가 섞여 들지 않게 하는 것도 같이 챙긴다. - 확인 결과는 도구 이름과 버전을 붙여 남긴다. 버전이 바뀌면 규칙도 바뀐 적이 있다(curl 의 CIDR 지원이 그 예다).
다음 실습에서 할 것
기록하는 가짜 프록시와 사내 API 를 띄워 고객 설정의 장애를 재현하고, 준비된 사례 18개를 세 도구로 직접 재서 표로 남긴다. 그다음 세 도구가 같은 길로 가는 설정을 만들고, proxies 인자를 쓰는 연동기를 고치고, 마지막으로 어떤 설정 파일이든 도구별 길을 실제로 재서 판정하는 탐침을 만든다.