SSD Nodes Learn
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-07-24

Docker가 UFW 방화벽을 우회하는 이유와 해결 방법

Docker가 iptables의 DNAT rule을 사용하여 UFW 설정을 무시하고 포트를 개방하는 원인을 설명합니다. 차단된 포트가 외부에서 접속되는 문제를 해결하는 두 가지 구체적인 방법을 확인하십시오.

Why Docker bypasses UFW

Docker가 UFW를 우회하는 이유는 공개된 container ports가 UFW가 관리하는 firewall rules를 거치지 않기 때문입니다. docker run -p 8080:80을 실행하면, Docker는 kernel의 nat table 내 PREROUTING chain에 DNAT (destination network address translation) rule을 작성합니다. 해당 rule은 kernel이 packet의 목적지를 결정하기 전에 각 packet의 destination을 container의 private address로 재작성합니다. 재작성된 packet은 Docker가 제어하는 FORWARD chain을 통해 container로 전달됩니다. UFW의 rules는 INPUT chain에 존재하며, packet은 이 chain에 진입하지 않습니다. 따라서 ufw status은 default deny를 보여주고, sudo ufw deny 8080은 success를 보고하지만, port 8080은 여전히 인터넷 전체에서 응답합니다.

이것은 Docker의 bug가 아니며, UFW가 고장 난 것도 아닙니다. 두 도구 모두 동일한 kernel firewall을 프로그래밍합니다. Docker의 rules는 packet 경로의 더 이른 시점에서 동작하므로 UFW에 요청이 전달되지 않습니다. 이 가이드는 우회 현상을 입증하고, 그 메커니즘을 설명하며, 두 가지 해결 방법인 127.0.0.1에서 ports를 publishing하는 방법과 DOCKER-USER chain에서 filtering하는 방법을 다룹니다. UFW가 처음이라면 the UFW firewall basics guide를 통해 먼저 설정을 완료하십시오. default-deny firewall은 서버의 다른 모든 작업을 위한 올바른 기본 설정입니다.

자신의 서버에서 우회 현상 확인하기

기본적으로 모든 인바운드 트래픽을 차단(default deny)하도록 설정된 UFW가 활성화된 VPS에서 시작합니다. 포트가 공개된 웹 컨테이너를 실행합니다:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

ufw status verbose를 실행하면 Default: deny (incoming), allow (outgoing)이 표시되며 8080 포트에 대한 규칙이 없습니다. 방화벽의 보고에 따르면 해당 포트는 닫혀 있습니다. 이제 서버 내부가 아닌 다른 머신에서 테스트를 진행합니다:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

컨테이너가 응답합니다. 명시적인 차단(deny) 규칙을 추가하고 다시 테스트합니다:

sudo ufw deny 8080/tcp

포트가 여전히 응답합니다. 차단 규칙이 패킷이 도달하지 않는 체인에 위치하기 때문입니다. UFW가 실패한 것이 아닙니다. UFW는 검토 대상조차 되지 않았습니다. 이것이 문제가 잘 숨겨지는 이유입니다. 어디에도 오류가 출력되지 않으며, 배포는 정상적으로 작동하고, 방화벽 상태 출력은 보안이 철저히 적용된 정상적인 서버와 동일하게 보입니다.

메커니즘: PREROUTING은 INPUT보다 먼저 실행됩니다

커널은 들어오는 패킷을 정해진 순서에 따라 처리하며, 모든 문제는 이 순서에서 발생합니다.

  1. PREROUTING가 가장 먼저 실행됩니다. 이 단계의 규칙은 패킷의 목적지를 변경할 수 있습니다. Docker의 공개 포트(published port) 규칙이 이 작업을 수행합니다.
  2. 다음으로 라우팅 결정이 이루어집니다. 호스트 자신을 대상으로 하는 패킷은 INPUT 체인으로 전달됩니다. 다른 기기를 대상으로 하는 패킷은 FORWARD 체인으로 전달됩니다.
  3. UFW 규칙은 INPUT에 있습니다. Docker 규칙은 FORWARD에 있습니다.

방금 시작한 컨테이너에 대한 Docker 규칙을 확인하십시오:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

DNAT 라인이 핵심입니다. 8080 포트로 들어오는 모든 패킷은 목적지가 172.17.0.2:80으로 변경됩니다. 이는 Docker의 프라이빗 브리지 네트워크에 있는 컨테이너 주소입니다. 목적지가 변경되면 패킷은 더 이상 호스트를 대상으로 하지 않습니다. 따라서 라우팅 결정에 따라 패킷은 FORWARD 경로로 전달됩니다. Docker는 이미 자신의 네트워크로 트래픽을 허용하는 규칙을 해당 경로에 추가해 두었습니다. 사용자의 deny 8080/tcp 규칙은 오지 않을 패킷을 기다리며 INPUT에 머물러 있습니다.

Ubuntu 24.04에서 iptables 명령은 nftables의 프런트엔드 역할을 하지만, 체인 순서와 결과는 동일합니다. UFW와 Docker는 모두 동일한 커널 패킷 파이프라인에 기록하며, Docker의 진입점이 더 앞서 있습니다.

일상적인 해결책: 127.0.0.1에 포트 게시

대부분의 컨테이너는 처음부터 외부에 공개될 필요가 없습니다. 데이터베이스, reverse proxy 뒤의 app server, admin panel, metrics endpoint 등은 인터넷에서 직접 접속해서는 안 됩니다. loopback 주소로 포트를 게시하십시오:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

또는 Compose 파일에서 다음과 같이 설정합니다:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

이 방법이 작동하는 이유는 Docker의 DNAT rule이 이제 127.0.0.1로 향하는 패킷만 매칭하기 때문입니다. 인터넷에서 오는 패킷은 해당 목적지를 가질 수 없으므로, 커널은 firewall rule이 실행되기 전에 패킷을 폐기합니다. 해당 포트는 host에서만 접근 가능하며, 그 외에는 접근할 수 없습니다. 바인딩 상태를 확인하십시오:

sudo ss -tlnp | grep 8080

출력 결과에 0.0.0.0:8080 또는 [::]:8080이 아닌 127.0.0.1:8080이 나타나야 합니다. 그 다음 다른 머신에서 curl http://your-vps-ip:8080/ 접속이 거부되는지 확인하십시오.

인터넷에 공개되어야 하는 서비스의 경우, 80 및 443 포트를 점유하고 hostname으로 라우팅하는 단일 reverse proxy를 실행하고 그 외에는 아무것도 게시하지 마십시오. 이것이 Traefik reverse proxy guide에서 설명하는 패턴이며, Nextcloud on a VPS와 같은 self-hosted app이 proxy를 통해서만 접근 가능하도록 유지하는 방식입니다. ports: 항목을 선언하는 방법과 나머지 Compose 워크플로우는 the Docker Compose basics guide에서 다룹니다.

모든 내부 컨테이너를 loopback으로 설정하면, UFW는 host 자체가 서비스하는 포트를 보호하는 본래의 역할을 수행하게 됩니다. 여기서 rule set을 구축한 후, 다음 명령어를 순서대로 실행하십시오:

ToolUFW rule generator

Real filtering: the DOCKER-USER chain

특정 컨테이너 포트를 네트워크에 공개하되 제한해야 하는 경우가 있습니다. 예를 들어, 특정 사무실 주소에서만 접근 가능한 데이터베이스 복제 포트와 같은 경우입니다. 이를 위해 Docker는 DOCKER-USER chain을 제공합니다. 모든 컨테이너 대상 패킷은 Docker의 자체 accept rule을 거치기 전에 DOCKER-USER를 통과합니다. Docker는 이 chain에 rule을 직접 작성하지 않습니다. 이 chain은 사용자를 위해 존재하며, Docker daemon을 재시작해도 설정 내용이 유지됩니다.

명령어 실행 전 주의사항: 패킷이 DOCKER-USER에 도달하면 이미 DNAT rewrite가 완료된 상태입니다. 패킷의 목적지 포트는 공개 포트(예시에서는 8080)가 아니라 컨테이너 포트(예시에서는 80)입니다. 따라서 --dport 8080를 매칭하는 rule은 아무것도 일치시키지 못합니다. 가장 확실한 방법은 커널의 connection tracker가 기억하고 있는, 클라이언트가 원래 접속을 시도했던 포트를 매칭하는 것입니다.

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

이 규칙의 의미는 다음과 같습니다: eth0를 통해 들어온 패킷 중 원래 목적지 포트가 8080인 연결에 속한 패킷에 대해, 10.0.0.10에서 오지 않은 모든 패킷을 차단합니다. --ctdir ORIGINAL 매칭을 통해 rule을 클라이언트에서 컨테이너로 향하는 방향으로 제한하므로, 응답 패킷이 실수로 차단되지 않습니다. eth0을 공인 인터페이스로, ip route | grep default을 해당 인터페이스 이름으로 변경하십시오. 이전과 동일한 방식으로 테스트하십시오. 허용된 주소에서의 curl은 성공해야 하며, 그 외의 주소에서는 연결 시간이 초과되어야 합니다.

iptables 명령어로 추가된 rule은 재부팅 시 사라집니다. UFW가 이미 이 방화벽을 관리하고 있으므로, rule을 영구적으로 저장하기에 적합한 위치는 /etc/ufw/after.rules입니다. 파일 끝에 다음 블록을 추가하십시오.

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

그 다음 sudo ufw reload을 실행하십시오. UFW는 재로드 또는 부팅 시마다 해당 파일을 다시 적용합니다. 따라서 컨테이너 필터링 설정이 방화벽의 다른 설정과 동일한 위치에 저장되며, 재부팅이나 Docker 업그레이드 후에도 유지됩니다.

Docker의 iptables integration을 비활성화하면 안 되는 이유

이 문제에 대한 이전 답변들은 /etc/docker/daemon.json에서 { "iptables": false }를 설정하라고 권장합니다. 그렇게 하지 마십시오. Docker의 firewall rules는 단순히 포트를 publish하는 것 이상의 역할을 수행합니다. masquerade rule은 컨테이너가 host의 주소를 통해 외부 인터넷에 접속할 수 있게 해줍니다. 따라서 이 integration을 끄면 컨테이너는 이미지를 pull하거나, package mirrors에 접속하거나, 외부 API(application programming interface)를 호출할 수 없습니다. DNAT rules는 -p가 작동하게 만드는 핵심 요소이므로, 이를 비활성화하면 publish된 포트가 완전히 작동을 멈춥니다. 별도의 Compose networks를 격리하는 isolation rules도 사라집니다. 이 설정을 통해 우회 경로를 해결하려 하면 컨테이너 네트워킹이 망가집니다. 또한 모든 rule을 사용자가 직접 작성하고 관리해야 합니다. Docker 공식 문서에서는 이 설정이 바로 그런 작업을 의도하는 사용자를 위한 것이라고 설명합니다. DOCKER-USER chain은 사용자가 이 switch를 사용할 필요가 없도록 하기 위해 존재합니다.

동일한 문제의 IPv6 측면

먼저 IPv6에서 게시된 port의 상태를 확인하십시오:

sudo ss -tlnp | grep 8080

Docker Engine 27부터 Docker는 기본적으로 ip6tables를 관리합니다. IPv6가 활성화된 Docker network에서 게시된 port는 IPv6 tables에서도 동일한 DNAT 처리를 받습니다. 따라서 동일한 우회 경로가 존재하며 동일한 해결 방법이 적용됩니다. DOCKER-USER chain이 ip6tables에도 존재하므로, sudo ip6tables -I DOCKER-USER ...를 사용하여 rule을 미러링하십시오. 그 다음 서버의 공용 IPv6 주소(예: curl -6 http://[2001:db8:2a::1]:8080/)를 대상으로 외부에서 curl을 사용하여 테스트하십시오.

IPv6가 없는 network에서 IPv6 client는 docker-proxy에 의해 처리됩니다. 이는 [::]:8080에서 대기하며 traffic을 IPv4를 통해 container로 전달하는 일반적인 user-space process입니다. Host process로 향하는 traffic은 INPUT를 통과하므로 UFW가 해당 경로를 필터링할 수 있습니다. 단, 이는 UFW가 IPv6를 관리할 때만 가능합니다. UFW가 IPv6를 관리하는지 여부와 VPS에서 IPv6 보안 공백이 발생하는 다른 방식은 UFW 및 IPv6 가이드에서 다룹니다.

loopback에 게시하면 이 문제가 발생하지 않습니다. -p 127.0.0.1:8080:80는 IPv4 loopback에만 bind되므로, IPv6 listener가 생성되지 않으며 두 stack 모두 외부에서 접근할 수 있는 대상이 없습니다.

유지 관리 가능한 패턴

  • 모든 내부 포트를 127.0.0.1에 게시하여, 애초에 외부로 노출되지 않도록 합니다.
  • 80 및 443 포트를 사용하는 단일 reverse proxy에 공용 측면 권한을 부여합니다.
  • 호스트의 UFW 기본 설정을 deny로 유지하고, SSH 및 proxy 포트만 허용합니다.
  • /etc/ufw/after.rules에 저장된 원래 목적지 포트를 기준으로, DOCKER-USER에서 실제 공용 컨테이너 포트를 필터링합니다.
  • Docker의 iptables 통합 기능을 활성화 상태로 유지합니다.

한 번 설정하면 다음과 같은 문제를 방지할 수 있습니다: ufw status은 호스트를 설명하고, DOCKER-USER은 컨테이너를 설명합니다. 실수로 게시되는 것은 없으며, 다음에 docker run -p을 입력하면 의도한 내용만 정확히 노출됩니다.

FAQ

UFW가 포트를 차단하는데도 Docker container에 접속할 수 있는 이유는 무엇입니까?

Docker가 PREROUTING chain에 DNAT rule을 사용하여 포트를 공개하기 때문입니다. 이 rule은 필터링이 수행되기 전에 패킷의 목적지 주소를 container 주소로 재작성합니다. 패킷은 FORWARD 경로를 따르며, UFW의 rule은 패킷이 진입하지 않는 INPUT chain에 위치합니다. 방화벽이 검사 대상에 포함되지 않으므로, 공개된 container 포트에 대해 deny rule이 적용되지 않습니다.

UFW가 Docker의 공개 포트를 차단하게 하려면 어떻게 해야 합니까?

UFW 자체로는 불가능합니다. UFW의 rule이 잘못된 chain에 있기 때문입니다. 포트 노출을 중단하고 127.0.0.1:8080:80로 공개하여 host만 접속할 수 있게 하거나, conntrack을 통해 원래의 목적지 포트와 일치하는 iptables rule을 DOCKER-USER chain에서 사용하십시오. 해당 rule이 재부팅 및 ufw reload 후에도 유지되도록 /etc/ufw/after.rules에 저장하십시오.

Docker의 daemon.json에서 "iptables": false로 설정해야 합니까?

아니요. 해당 설정은 Docker의 모든 firewall 및 NAT rule을 제거합니다. 이는 우회 현상보다 훨씬 더 많은 문제를 일으킵니다. masquerade rule이 삭제되어 container의 외부 인터넷 접속이 끊기며, DNAT rule이 삭제되어 공개된 포트가 작동하지 않습니다. 대신 loopback publishing과 DOCKER-USER chain을 사용하십시오. 이 방법은 container 네트워킹을 망가뜨리지 않고 노출 문제를 해결합니다.

Docker는 IPv6에서도 UFW를 우회합니까?

Docker Engine 27 이상 버전에서는 ip6tables 관리가 기본적으로 활성화되어 있습니다. 따라서 IPv6가 활성화된 Docker network에서 공개된 포트는 IPv4와 동일하게 UFW를 우회하여 재작성되며, ip6tables을 통해 DOCKER-USER rule을 동일하게 복제해야 합니다. IPv6가 없는 network의 경우, docker-proxy 프로세스가 [::]에서 대기하며 해당 트래픽은 INPUT를 통과합니다. UFW가 IPv6를 관리한다면 여기서 필터링이 가능합니다. 127.0.0.1에서 공개하면 IPv6에서 대기하는 항목이 없으므로 두 경우 모두 피할 수 있습니다.