환경 변수 HTTP_PROXY, HTTPS_PROXY 및 NO_PROXY는 매우 간단해 보입니다: 변수를 설정하면 모든 트래픽이 프록스를 통해 전송됩니다. 그러나 실제로는 이 지점에서 개발자들이 디버깅에 몇 시간을 낭비하게 됩니다: 프록스는 터미널에서는 작동하지만 Docker 컨테이너에서는 작동하지 않습니다; curl은 프록스를 통해 작동하지만 Python 애플리케이션은 그렇지 않습니다; CI 러너가 갑자기 내부 레지스트리를 차단하는데, 이는 NO_PROXY가 잘못된 구문으로 지정되어 있기 때문입니다. 7가지 실제 함정을 분석하고 구체적인 코드로 이를 우회하는 방법을 보여드리겠습니다.
애플리케이션이 프록시 변수를 읽는 방법
중요한 점은: HTTP_PROXY, HTTPS_PROXY 및 NO_PROXY는 HTTP 프로토콜의 표준이 아니라 각 라이브러리가 저마다 구현하는 관례입니다. Linux 커널은 이러한 변수에 대해 아무것도 알지 못하며, 특정 애플리케이션이나 라이브러리가 네트워크 클라이언트를 초기화하는 단계에서 읽습니다.
예를 들어, curl 및 libcurl 기반의 유틸리티는 프로세스가 시작될 때마다 변수를 읽습니다. Python 라이브러리 requests는 세션 생성 시 프로세스 환경을 스캔하는 urllib.request.getproxies() 모듈을 통해 읽습니다. 반면, Node.js의 내장 fetch()는 버전 18 이전에는 이러한 변수를 전혀 고려하지 않습니다.
따라서 첫 번째 규칙은: 프록시 변수는 특정 라이브러리가 명시적으로 지원하는 곳에서만 작동합니다. curl을 통해 확인하는 것은 Go, Java 또는 Node.js 애플리케이션이 동일하게 작동할 것이라는 보장을 하지 않습니다.
export HTTP_PROXY="http://user:[email protected]:8080"
export HTTPS_PROXY="http://user:[email protected]:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.corp"
curl -v https://api.example.com/ping
# curl은 verbose 로그에: "Trying proxy.example.com..."을 표시합니다.
함정 1: 변수의 대소문자 및 취약점 httpoxy
대부분의 Linux 콘솔 유틸리티는 변수를 소문자로 읽습니다: http_proxy, https_proxy, no_proxy. 많은 현대 SDK는 대문자를 기준으로 합니다. 이 차이는 사소한 것처럼 보이지만, 바로 이로 인해 "로컬에서는 작동하지만 서버에서는 작동하지 않는다"는 스타일의 버그가 설정 첫날에 발생합니다.
별도의 이야기로, httpoxy 취약점(CVE-2016-5385 및 관련)이 있습니다. CGI 환경(PHP, Python WSGI, 일부 Ruby 프레임워크)은 자동으로 들어오는 요청의 HTTP 헤더 Proxy를 환경 변수 HTTP_PROXY로 변환합니다. 공격자가 Proxy: http://attacker.com 헤더를 보내면, 귀하의 서버 애플리케이션은 외부 프록시 서버를 통해 아웃바운드 요청을 보내기 시작하여 MITM 공격 및 데이터 유출의 경로를 열게 됩니다.
해결책은 항상 두 대소문자로 변수를 설정하고, 들어오는 요청에 대해 웹 서버 수준에서 HTTP_PROXY를 명시적으로 초기화하는 것입니다:
# 호환성을 위해 두 대소문자로 설정합니다
export http_proxy="http://proxy.example.com:8080"
export HTTP_PROXY="http://proxy.example.com:8080"
export https_proxy="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
# nginx에서 — 클라이언트의 Proxy 헤더를 차단합니다
proxy_set_header Proxy "";
함정 2: NO_PROXY 구문은 도구마다 다릅니다
NO_PROXY는 예외 목록이지만, 도메인 및 마스크의 기록 형식은 표준화되어 있지 않습니다. 일부 도구는 .example.com을 "모든 하위 도메인"으로 이해하지만, 다른 도구는 *.example.com을 요구하며, 세 번째 도구는 와일드카드 없이 호스트의 정확한 일치를 이해합니다.
예를 들어, Go 라이브러리 net/http는 NO_PROXY에서 CIDR 표기법(10.0.0.0/8)을 지원하지만, 특정 버전의 curl은 지원하지 않습니다. 이는 동일한 NO_PROXY가 하나의 CI 단계에서 내부 레지스트리를 올바르게 제외하고, 다른 언어로 작성된 다른 단계에서는 제외하지 않는 상황을 초래합니다.
| 도구 | 하위 도메인 와일드카드 | CIDR 마스크 |
|---|---|---|
| curl / libcurl | .example.com을 통해 | 아니오 |
| Go net/http | .example.com을 통해 | 예 |
| Python requests | .example.com을 통해 | 아니오 |
| npm / Node.js | *.example.com이 필요합니다 | 아니오 |
실용적인 조언: NO_PROXY에 도메인 형식과 와일드카드 형식을 동시에 나열하여 다양한 파서를 커버하십시오:
export NO_PROXY="localhost,127.0.0.1,.internal.corp,*.internal.corp,10.0.0.0/8"
함정 3: Docker build vs Docker run — 서로 다른 환경 변수
아마도 Docker에서 가장 흔한 함정입니다. docker run -e HTTP_PROXY=...를 통해 설정된 변수는 컨테이너가 실행되는 동안에만 작동합니다. 이들은 이미지 빌드 단계로 자동으로 전달되지 않습니다. Dockerfile이 내부에서 apt-get install 또는 pip install을 호출하면, 빌드는 기업 프록스를 통해 진행되는 경우 타임아웃으로 멈추게 됩니다.
빌드를 위해서는 Docker가 1.9+ 버전부터 기본적으로 이해하는 빌드 인수를 전달해야 합니다:
docker build \
--build-arg HTTP_PROXY=http://proxy.example.com:8080 \
--build-arg HTTPS_PROXY=http://proxy.example.com:8080 \
--build-arg NO_PROXY=localhost,127.0.0.1 \
-t my-app:latest .
변수가 실행된 컨테이너 내에서도 사용 가능하도록 하려면, Dockerfile에서 ENV를 통해 명시적으로 중복해야 합니다 — ARG와 ENV는 Docker에서 본질적으로 다른 메커니즘이며, ARG는 레이어 빌드가 완료되면 즉시 사라집니다:
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV NO_PROXY=${NO_PROXY}
RUN apt-get update && apt-get install -y curl
또 다른 세부 사항: Docker 데몬은 레지스트리에서 이미지를 다운로드하기 위해 프록스를 사용할 수 있으며, 이는 Dockerfile이 아니라 /etc/systemd/system/docker.service.d/http-proxy.conf의 systemd 유닛 구성에서 설정됩니다. 이는 종종 컨테이너 내 변수와 혼동됩니다.
함정 4: docker-compose 및 서비스 간 변수 상속
docker-compose에서는 호스트의 환경 변수가 명시적으로 environment 섹션에 작성되지 않거나 .env 파일을 사용하지 않는 한 컨테이너로 자동으로 전달되지 않습니다. 자주 발생하는 오류는 docker-compose up 실행 전에 터미널에서 프록스를 설정하고 모든 서비스가 이를 인식할 것이라고 기대하는 것입니다.
version: "3.9"
services:
scraper:
build:
context: .
args:
HTTP_PROXY: "${HTTP_PROXY}"
HTTPS_PROXY: "${HTTPS_PROXY}"
environment:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- NO_PROXY=localhost,127.0.0.1,db,redis
depends_on:
- db
- redis
NO_PROXY에 주목하십시오: docker-compose 네트워크 내의 컨테이너는 서비스 이름(db, redis)을 통해 서로 접근하며, localhost를 통해 접근하지 않습니다. 서비스 이름을 예외에 추가하는 것을 잊으면, 컨테이너 간의 모든 내부 트래픽도 외부 프록스를 통해 나가게 되어 지연이 증가하고, 프록스가 PostgreSQL이나 Redis와 같은 내부 프로토콜을 허용하지 않는 경우 연결이 끊길 수 있습니다.
함정 5: CI/CD 러너 및 프록시 비밀 유출
GitHub Actions, GitLab CI 및 Jenkins에서는 프록시 변수가 종종 러너 또는 조직 수준에서 설정되며, 이는 두 가지 문제를 발생시킵니다. 첫째, 프록시 URL에 로그인 및 비밀번호가 평문으로 포함되어 있으면, 이는 환경을 출력하는 모든 명령(env, printenv, 스크립트의 디버그 출력)에서 빌드 로그에 노출됩니다. 둘째, 셀프 호스팅 러너는 때때로 CI 작업 트래픽을 위해 설계되지 않은 호스트의 시스템 프록스를 상속받습니다.
# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
env:
HTTP_PROXY: ${{ secrets.PROXY_URL }}
HTTPS_PROXY: ${{ secrets.PROXY_URL }}
NO_PROXY: "localhost,127.0.0.1,github.com"
steps:
- uses: actions/checkout@v4
- name: 연결 테스트
run: curl -sS https://api.example.com/health
보안 규칙: CI의 비밀에만 프록시 URL을 저장하고(GitHub Secrets, GitLab CI/CD Variables에 "masked" 플래그 사용), 디버그 로그에서 환경 변수를 전체적으로 출력하지 않으며, NO_PROXY를 제한하여 CI 플랫폼(예: github.com, gitlab.com)에 대한 요청이 프록스를 통해 가지 않도록 하십시오 — 그렇지 않으면 프록스가 피크 시간에 사용할 수 없을 경우 빌드가 멈출 수 있습니다.
함정 6: 프록시 로그인 및 비밀번호의 특수 문자
프록시 비밀번호에 @, :, / 또는 #와 같은 문자가 포함되어 있으면, URL 파서는 잘못된 위치에서 문자열을 나누게 됩니다. 전형적인 오류는 비밀번호 P@ssw0rd가 http://user:P@[email protected]:8080 문자열에서 파서가 "[email protected]"을 호스트로 읽고 "proxy.com"으로 읽지 않는 것입니다.
해결책은 항상 로그인 및 비밀번호를 URL 인코딩하여 변수를 대입하기 전에 처리하는 것입니다:
# Python: 프록시 문자열을 구성하기 전에 비밀번호를 인코딩합니다
import urllib.parse
user = "login"
password = "P@ss:w0rd/1"
encoded_password = urllib.parse.quote(password, safe="")
proxy_url = f"http://{user}:{encoded_password}@proxy.example.com:8080"
import os
os.environ["HTTPS_PROXY"] = proxy_url
# Node.js: encodeURIComponent를 통한 유사한 구성
const user = "login";
const password = "P@ss:w0rd/1";
const encoded = encodeURIComponent(password);
const proxyUrl = `http://${user}:${encoded}@proxy.example.com:8080`;
process.env.HTTPS_PROXY = proxyUrl;
함정 7: 환경 변수를 무시하는 애플리케이션
모든 HTTP 클라이언트가 시스템 프록시 변수를 존중하는 것은 아닙니다. Python의 requests 라이브러리는 requests.Session()을 통해 자동으로 읽지만, 한 번이라도 명시적으로 proxies={} 사전을 호출하면, 이는 환경 변수를 재정의하여 HTTP_PROXY가 시스템에 설정되어 있어도 트래픽이 직접적으로 흐르게 됩니다.
import requests
# 빈 proxies 사전은 환경 변수를 무시합니다
response = requests.get("https://api.example.com", proxies={})
# 올바른 방법 — 프록시를 명시적으로 전달하거나 proxies를 아예 전달하지 않습니다
proxies = {
"http": "http://user:[email protected]:8080",
"https": "http://user:[email protected]:8080",
}
response = requests.get("https://api.example.com", proxies=proxies)
Node.js의 내장 fetch는 추가 라이브러리 없이 HTTP_PROXY를 전혀 알지 못합니다. undici의 ProxyAgent 또는 전통적인 https-proxy-agent를 명시적으로 연결해야 합니다:
const { ProxyAgent, setGlobalDispatcher } = require("undici");
const proxyUrl = process.env.HTTPS_PROXY;
if (proxyUrl) {
setGlobalDispatcher(new ProxyAgent(proxyUrl));
}
fetch("https://api.example.com/data")
.then(res => res.json())
.then(console.log);
Java도 환경 변수 대신 시스템 속성을 요구합니다: -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080. JVM에서는 기본적으로 HTTP_PROXY 환경 변수를 읽지 않습니다.
프록시 변수를 올바르게 테스트하는 방법
애플리케이션 디버깅에 시간을 낭비하기 전에, 코드와 분리하여 변수와 프록시 자체를 확인하십시오. 이는 몇 시간을 절약할 수 있습니다.
# 1. 현재 세션에서 변수가 실제로 설정되어 있는지 확인합니다
env | grep -i proxy
# 2. curl을 통해 프록시의 작동 여부를 직접 확인합니다
curl -x "$HTTP_PROXY" -v https://api.ipify.org
# 3. 컨테이너 내부에서 변수가 도달했는지 확인합니다
docker exec -it my_container env | grep -i proxy
# 4. NO_PROXY가 실제로 필요한 호스트를 제외하는지 확인합니다
curl -v --noproxy "internal.corp" https://service.internal.corp/health
대량의 외부 API 요청, 마켓플레이스 파싱 또는 CI 파이프라인에서 광고의 지리적 타겟팅 테스트가 목적이라면, 부하에 적합한 프록시 유형을 선택하는 것이 중요합니다. CI에서 안정적이고 고부하 시나리오를 위해서는 일반적으로 데이터 센터 프록시가 충분합니다 — 이들은 대량의 빌드 및 테스트 트래픽에서 레지던트 프록시보다 빠르고 저렴합니다. 만약 파이프라인이 특정 지역의 실제 사용자 IP로 웹사이트의 동작을 확인한다면, 레지던트 프록시를 연결하는 것이 좋습니다 — 이들은 "데이터 센터" IP로 인한 차단 위험을 줄입니다.
배포 전 체크리스트
- 변수가 대문자와 소문자 모두에서 설정되어 있습니다
- 프록시 URL의 로그인 및 비밀번호가
urllib.parse.quote또는encodeURIComponent를 통해 URL 인코딩되어 있습니다 - NO_PROXY는 도구 간 호환성을 위해 도메인 형식과 와일드카드 형식을 모두 포함하고 있습니다
- docker-compose에서 NO_PROXY는 localhost뿐만 아니라 Compose 네트워크의 서비스 이름을 포함합니다
- Docker build를 위해 빌드 인수가 Dockerfile의 런타임 env와 별도로 전달됩니다
- CI에서 프록시 URL이 포함된 비밀은 마스킹되어 있으며 디버그 로그에 출력되지 않습니다
- Java 애플리케이션의 경우 프록시가 env뿐만 아니라 시스템 속성을 통해 설정됩니다
- httpoxy로부터의 보호가 확인되었습니다 — 클라이언트의 Proxy 헤더는 웹 서버 수준에서 초기화됩니다
결론
HTTP_PROXY, HTTPS_PROXY 및 NO_PROXY 변수는 보기에는 간단한 관례처럼 보이지만, 실제로는 대소문자, 예외 구문, Docker build와 Docker run 간의 경계, docker-compose에서의 환경 상속, CI 로그에서의 비밀 유출, 그리고 마지막으로 모든 라이브러리가 이러한 변수를 읽는 것은 아니라는 점에서 문제가 발생합니다. curl 및 docker exec를 통해 각 변수를 분리하여 확인한 후, 애플리케이션 비즈니스 로직에서 버그를 찾으십시오.
외부 사이트 파싱, 광고의 지리적 타겟팅 테스트 또는 CI 파이프라인에서 IP 차단 우회를 위한 인프라가 필요하다면, 환경 변수를 올바르게 설정하는 것뿐만 아니라 작업에 적합한 프록시 유형을 선택하는 것이 중요합니다. 모바일 트래픽 및 모바일 운영자에서의 동작을 확인하는 애플리케이션 테스트에는 모바일 프록시가 적합합니다 — 이들은 실제 이동통신 네트워크의 IP를 제공하며, 안티프로드 시스템의 필터에 덜 걸립니다.