기업 VPN 뒤의 무엇에도 닿지 못하는 컨테이너는 보통 터널과 다른 네트워크 네임스페이스에 존재합니다. 호스트 클라이언트의 경로, DNS 서버, 방화벽 표식은 호스트의 스택에 속하고, 브리지 네트워크 컨테이너는 자체 스택을 가집니다. 이 한 가지 사실이 플랫폼 팀에 들어오는 대부분의 보고를 설명하며, «VPN이 고장 났다»와 «Docker가 VPN을 우회한다»가 동시에 틀릴 수 있는 이유이기도 합니다. 하나의 증상 뒤에 두 가지 매우 다른 구성이 숨어 있으므로 설정을 바꾸기 전에 어느 쪽인지 확인하세요.

터널이 누구의 네임스페이스에 속하는지 찾아내세요

macOS나 Windows용 Docker Desktop에서 컨테이너는 Linux 가상 머신 안에서 실행됩니다. 데스크톱에 설치된 VPN 클라이언트는 데스크톱 운영 체제에 터널을 만들고, 가상 머신은 그것을 결코 보지 못합니다. 어떤 컨테이너 구성도, 컨테이너에 필요한 인터페이스가 그 네트워크 네임스페이스에 아예 없기 때문에 이를 고치지 못합니다.

Linux 호스트에서는 터널이 호스트의 네임스페이스에 있고, 브리지 컨테이너는 보통 나가는 방향에서 소스 NAT가 적용된 호스트 라우팅 테이블을 통해 도달합니다. 그 경로가 실패할 때 원인은 대개 빠진 경로가 아니라 포워딩 정책, 역경로 필터링 또는 겹치는 서브넷입니다. 더 읽기 전에 두 세계 중 어디에 있는지 확인하세요:

docker info --format '{{.OperatingSystem}}'
docker run --rm --net=host alpine ip route | head -5
docker run --rm alpine ip route | head -5

이 두 라우팅 테이블의 차이가 문제 전체를 한 화면에 보여줍니다. 호스트 보기에 터널 인터페이스가 있고 브리지 보기에 없다면 네임스페이스 격리를 보고 있는 것이며, 나머지는 그것을 어떻게 넘을지의 선택입니다.

호스트 네트워킹은 시험이지 설계가 아닙니다

컨테이너를 호스트 네트워킹으로 실행하면, Docker가 호스트 네트워크 네임스페이스 공유로 문서화한 대로, 터널 인터페이스와 그 경로를 직접 상속합니다. 같은 요청이 그때 성공한다면 경로 이론이 확인되며, 실패는 자격 증명이나 DNS, 액세스 규칙이 아니라 네임스페이스 격리입니다.

docker run --rm --net=host curl -sS -o /dev/null -w '%{http_code}\n' https://api.internal/health

진단용으로 남겨 두세요. 호스트 네트워킹은 포트 게시와 컨테이너에서 원하던 격리의 대부분을 포기하며, Docker Desktop에서 «host»는 데스크톱이 아니라 가상 머신을 뜻하므로 어느 호스트를 상대했는지 잊으면 그곳의 성공이나 실패가 오해를 부를 수 있습니다.

tun 장치와 그에 따르는 권한

컨테이너 안의 VPN 클라이언트에는 터널 장치와 이를 구성할 권한이 필요합니다. 흔한 지름길은 --privileged이며, 모든 장치를 부여하고 컨테이너 전체의 보안 정책을 완화합니다. 이는 이 작업이 요구하는 것보다 훨씬 많고 검토에서 정당화하기 어렵습니다.

sudo modprobe tun
docker run --rm -it --device /dev/net/tun --cap-add NET_ADMIN myvpn-client

더 좁은 형태는 단일 장치 노드를 매핑하고 네트워크 관리 권한만 추가합니다. 노드가 존재하도록 먼저 호스트에서 모듈을 로드하세요. 컨테이너가 시작되고 클라이언트가 터널 장치를 열 수 없다고 보고하면, 그 오류는 대개 문자 그대로이므로 프로필을 의심하기 전에 매핑을 확인하세요.

이 설계는 클라이언트가 컨테이너마다 실행된다는 뜻이기도 합니다. 각 워크로드에 자체 터널이 필요할 때는 정답이고, 호스트의 터널이 보이길 원했을 뿐이라면 오답이며, 플랫폼 팀은 아마 후자를 가정했을 것입니다.

브리지 NAT는 VPN이 보는 소스를 바꿉니다

기본 브리지에서는 컨테이너의 나가는 트래픽이 호스트 주소로 변환됩니다. 따라서 VPN 뒤의 기업 측은 컨테이너가 아니라 호스트의 터널 주소를 보게 되고, 소스별 액세스 규칙은 호스트가 요청한 것처럼 동작하며, 두 컨테이너는 VPN 서버에 구분되지 않습니다.

macvlan이나 ipvlan은 대신 컨테이너에 LAN 주소를 주어 LAN 검색에 도움이 되고 컨테이너를 세그먼트의 동등한 구성원처럼 보이게 합니다. 또한 호스트 터널을 완전히 우회하므로, 그 LAN에서도 VPN에 닿을 수 없다면 VPN 전용 프리픽스에는 도달하지 못합니다.

두 형태 모두 정당하며 서로 다른 요구에 답합니다. 요구가 «이 컨테이너는 VPN 전용 서비스에 도달해야 한다»라면 호스트를 통한 라우팅과 컨테이너 내부에서 터널을 끝내는 것 사이에서 선택합니다. 그 선택은 배포 전에, 첫 실패 후가 아니라 적어 두세요. 무엇을 고르든, 그 액세스 규칙과 로그에 나타날 것이 바로 원격 끝이 보게 될 소스 주소이므로 그것을 기록하세요. 그 주소를 말할 수 없는 설계는 미완성입니다.

DNS는 터널이 아니라 데몬을 따릅니다

컨테이너는 시작할 때 리졸버 구성을 받으며, 데몬이나 이미지에서 복사됩니다. VPN이 연결 후 밀어 넣는 주소는 그 복사본에는 너무 늦게 도착하며, 컨테이너 안이 아니라 호스트 리졸버로 밀려갑니다.

서비스에 대해 리졸버를 명시적으로 설정하거나, 터널을 통해 닿을 수 있는 내부 리졸버를 가리키세요. 사설 이름을 공인 주소로 해석하는 컨테이너는 보통 공용 리졸버를 쓰는 것이지 고장 난 경로 때문에 고통받는 것이 아니며, 수정은 compose 파일이나 데몬 구성에 속합니다.

같은 논리가 출구 주소 문제에도 적용됩니다. 각 컨테이너가 서로 다른 VPN 주소로 보여야 한다면 호스트 터널 하나로는 제공할 수 없는데, NAT가 이들을 하나의 소스로 압축하기 때문입니다. 실현 가능한 형태는 각 컨테이너의 네임스페이스 안의 클라이언트이거나 터널을 소유하고 스택을 공유하는 사이드카이며, 둘 다 애플리케이션이 주소 지정되는 방식을 바꿉니다.

티켓을 닫기 전 점검 목록: 터널을 소유한 네임스페이스를 확인하고, 호스트 네트워킹으로 경로를 증명하고, 필요한 tun 장치만 매핑하고, NAT 후 소스 주소를 기억하고, 리졸버를 명시적으로 설정하고, 컨테이너별 출구 주소를 플래그가 아니라 아키텍처 결정으로 다루세요.

관련 글