SSD Nodes Learn
Guias Matt ConnorPor Matt Connor · Atualizado 2026-07-24

Docker ignorando UFW como resolver

Docker insere regras DNAT no iptables que ignoram o UFW. Aprenda a corrigir o bypass de portas publicadas usando bind local ou customizando a chain FORWARD.

Por que o Docker ignora o UFW

O Docker ignora o UFW porque as portas de containers publicadas não passam pelas regras de firewall gerenciadas pelo UFW. Quando você executa docker run -p 8080:80, o Docker escreve uma regra de DNAT (destination network address translation) na chain PREROUTING da tabela nat do kernel. Essa regra altera o destino de cada pacote para o endereço privado do container antes que o kernel decida o destino final do pacote. O pacote reescrito é então encaminhado para o container através da chain FORWARD, que é controlada pelo Docker. As regras do UFW residem na chain INPUT, e o pacote nunca entra nela. Portanto, o ufw status mostra o bloqueio padrão, o sudo ufw deny 8080 reporta sucesso, e a porta 8080 continua respondendo para toda a internet.

Isso não é um bug do Docker, e o UFW não está quebrado. Ambas as ferramentas programam o mesmo firewall do kernel. As regras do Docker atuam em um ponto anterior no caminho do pacote, então o UFW nunca é consultado. Este guia demonstra o bypass, explica o mecanismo e apresenta as duas correções que funcionam: publicar portas no 127.0.0.1 e filtrar na chain DOCKER-USER. Se você ainda não conhece o UFW, configure-o primeiro com o guia básico do firewall UFW, pois um firewall com bloqueio padrão (default-deny) continua sendo a base correta para todo o restante no servidor.

Veja o bypass no seu próprio servidor

Comece em uma VPS onde o UFW está ativo com uma política padrão de deny para tráfego de entrada. Execute um container web com uma porta publicada:

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

ufw status verbose mostra Default: deny (incoming), allow (outgoing) e nenhuma regra para a porta 8080. Pelo relatório do próprio firewall, a porta está fechada. Agora teste de uma máquina diferente, não do próprio servidor:

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

O container responde. Adicione uma regra de deny explícita e teste novamente:

sudo ufw deny 8080/tcp

A porta continua respondendo, porque a regra de deny está em uma chain que o pacote nunca visita. O UFW não falhou. Ele nunca foi consultado. É por isso que o problema é tão difícil de detectar: nenhum erro é impresso, o deploy funciona e a saída do status do firewall parece exatamente a de um servidor travado e saudável.

O mecanismo: PREROUTING ocorre antes de INPUT

O kernel processa pacotes recebidos em uma ordem fixa, e o problema reside nessa ordem.

  1. PREROUTING é executado primeiro. Regras aqui podem reescrever o destino do pacote; a regra do Docker para uma porta publicada faz exatamente isso.
  2. A decisão de roteamento ocorre em seguida. Um pacote destinado ao próprio host vai para a chain INPUT. Um pacote destinado a qualquer outra máquina vai para a chain FORWARD.
  3. As regras do UFW residem em INPUT. As regras do Docker residem em FORWARD.

Veja a regra do Docker para o container que você acabou de iniciar:

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

A linha DNAT explica tudo. Qualquer pacote que chega para a porta 8080 tem seu destino reescrito para 172.17.0.2:80, o endereço do container na rede bridge privada do Docker. Após a reescrita, o pacote não é mais destinado ao host, portanto a decisão de roteamento o envia pelo caminho FORWARD, onde o Docker já adicionou regras que aceitam tráfego em suas próprias redes. Sua regra deny 8080/tcp espera em INPUT por um pacote que nunca chega.

No Ubuntu 24.04, o comando iptables é uma interface para o nftables, mas a ordem das chains e o resultado são idênticos. O UFW e o Docker escrevem no mesmo pipeline de pacotes do kernel, e o ponto de entrada do Docker é anterior.

A solução cotidiana: publique portas em 127.0.0.1

A maioria dos containers não precisa ser pública. Um banco de dados, um servidor de aplicação atrás de um reverse proxy, um painel de administração ou um endpoint de métricas: nenhum deles deve responder diretamente à internet. Publique-os no endereço de loopback:

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

Ou em um arquivo Compose:

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

Isso funciona porque a regra DNAT do Docker agora corresponde apenas a pacotes endereçados a 127.0.0.1. Um pacote da internet nunca terá esse destino legítimo, então o kernel o descarta antes de qualquer regra de firewall ser executada. A porta fica acessível apenas pelo host. Verifique o binding:

sudo ss -tlnp | grep 8080

Você deve ver 127.0.0.1:8080 na saída, não 0.0.0.0:8080 ou [::]:8080. Em seguida, confirme de outra máquina que a conexão em curl http://your-vps-ip:8080/ é recusada.

Para serviços que devem estar expostos à internet, utilize um único reverse proxy que utilize as portas 80 e 443 e faça o roteamento por hostname; não publique mais nada. Esse é o padrão descrito no guia do reverse proxy Traefik, e é assim que um app self-hosted como o Nextcloud em um VPS permanece inacessível, exceto através do seu proxy. Como as entradas ports: são declaradas e o restante do fluxo de trabalho do Compose são abordados no guia de conceitos básicos do Docker Compose.

Com cada container interno no loopback, o UFW volta a realizar sua função normal: proteger as portas servidas pelo próprio host. Construa esse conjunto de regras aqui e execute os comandos em ordem:

ToolUFW rule generator

Filtragem real: a chain DOCKER-USER

Às vezes, uma porta de container deve permanecer publicada na rede, mas restrita. Por exemplo, uma porta de réplica de banco de dados que apenas um endereço de escritório pode acessar. Para isso, o Docker fornece a chain DOCKER-USER. Todo pacote destinado a qualquer container passa pela DOCKER-USER antes das regras de accept do próprio Docker, e o Docker nunca escreve regras nela. A chain existe para o usuário, e o Docker não altera o conteúdo dela durante reinicializações do daemon.

Um erro antes do comando: quando um pacote chega à DOCKER-USER, o rewrite de DNAT já ocorreu. A porta de destino do pacote é a porta do container (80 em nosso exemplo), não a porta publicada (8080). Uma regra que busque por --dport 8080 não encontrará nada. A forma confiável é buscar pela porta que o cliente chamou originalmente, que o connection tracker do kernel memoriza:

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

Leia como: para pacotes que entraram pela eth0 e pertencem a uma conexão cujo destino original era a porta 8080, descarte tudo o que não venha de 10.0.0.10. O match --ctdir ORIGINAL limita a regra à direção cliente-para-container, evitando que pacotes de resposta sejam bloqueados por erro. Substitua eth0 pela sua interface pública; ip route | grep default é o nome dela. Teste da mesma forma que antes: curl a partir do endereço permitido funciona, e de qualquer outro lugar a conexão expira (timeout).

Regras adicionadas com o comando iptables desaparecem no reboot. Como o UFW já gerencia este firewall, o local correto para persistir as regras é o /etc/ufw/after.rules. Adicione um bloco ao final do arquivo:

*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

Depois, execute sudo ufw reload. O UFW aplica esse arquivo em cada reload e em cada boot, então o seu filtragem de containers agora reside no mesmo local que o restante do seu firewall, sobrevivendo tanto a um reboot quanto a uma atualização do Docker.

Por que você não deve desativar a integração do iptables do Docker

Respostas antigas para este problema sugerem configurar { "iptables": false } em /etc/docker/daemon.json. Não faça isso. As regras de firewall do Docker fazem muito mais do que apenas publicar portas. A regra de masquerade é o que permite o acesso dos containers à internet via endereço do host; com a integração desativada, os containers não conseguem baixar imagens, acessar mirrors de pacotes ou chamar qualquer API externa. As regras de DNAT são o que permitem o funcionamento do -p; sem elas, as portas publicadas param de funcionar completamente. As regras de isolamento que mantêm redes Compose separadas também deixam de existir. Você corrigiria o bypass quebrando a rede dos containers, e você teria que escrever e manter cada uma dessas regras manualmente. A documentação oficial do Docker descreve essa configuração para usuários que pretendem fazer exatamente isso. A chain DOCKER-USER existe precisamente para que ninguém precise desse switch.

O lado IPv6 do mesmo problema

Primeiro, verifique como está a porta publicada no IPv6:

sudo ss -tlnp | grep 8080

Desde o Docker Engine 27, o Docker gerencia o ip6tables por padrão. Em uma rede Docker com IPv6 habilitado, uma porta publicada recebe o mesmo tratamento de DNAT nas tabelas IPv6. Portanto, o mesmo bypass existe lá e a mesma correção se aplica: a chain DOCKER-USER também existe no ip6tables. Espelhe sua regra usando sudo ip6tables -I DOCKER-USER ... e teste externamente com o curl contra o endereço IPv6 público do seu servidor, por exemplo curl -6 http://[2001:db8:2a::1]:8080/.

Em uma rede sem IPv6, os clientes IPv6 são gerenciados pelo docker-proxy, um processo normal de user-space que escuta em [::]:8080 e encaminha o tráfego para o container via IPv4. O tráfego para um processo do host passa pelo INPUT, então o UFW pode filtrar esse caminho, mas apenas quando o UFW está gerenciando o IPv6. Se ele está, e as outras formas como uma brecha de IPv6 ocorre em uma VPS, é o assunto do guia de UFW e IPv6.

Publicar no loopback evita todo o problema: o -p 127.0.0.1:8080:80 faz o bind apenas no loopback IPv4, portanto não há um listener IPv6 e nada pode ser acessado externamente em nenhuma das pilhas.

O padrão que sustenta tudo

  • Publique todas as portas internas no 127.0.0.1, para que nunca fiquem expostas.
  • Atribua o acesso público a um único reverse proxy que utilize as portas 80 e 443.
  • Mantenha o UFW com a política padrão de negação (deny) no host, permitindo apenas SSH e as portas do proxy.
  • Filtre portas de containers genuinamente públicas no DOCKER-USER, baseando-se na porta de destino original, persistida no /etc/ufw/after.rules.
  • Mantenha a integração do Docker com o iptables ativada.

Configurado uma vez, isso elimina surpresas: ufw status descreve o host, e DOCKER-USER descreve os containers. Nada é publicado por acidente, e o próximo docker run -p que você digitar expõe exatamente o que você pretendia.

FAQ

Por que consigo acessar meu container Docker mesmo com o UFW bloqueando a porta?

Porque o Docker publica a porta com uma regra DNAT na chain PREROUTING, que reescreve o destino do pacote para o endereço do container antes de qualquer filtragem. O pacote então segue pelo caminho FORWARD, e as regras do UFW estão na chain INPUT, uma chain que o pacote nunca acessa. O firewall não é consultado, portanto, suas regras de bloqueio não têm efeito em portas de containers publicadas.

Como faço para o UFW bloquear as portas publicadas pelo Docker?

O UFW sozinho não consegue, pois suas regras estão na chain errada. Ou você para de expor a porta, publicando-a como 127.0.0.1:8080:80 para que apenas o host possa acessá-la, ou filtra na chain DOCKER-USER com uma regra de iptables que corresponda à porta de destino original via conntrack. Persista essa regra em /etc/ufw/after.rules para que ela sobreviva a reboots e ufw reload.

Devo configurar "iptables": false no daemon.json do Docker?

Não. Essa configuração remove todas as regras de firewall e NAT do Docker, o que causa muito mais problemas do que o bypass. Os containers perdem o acesso à internet externa porque a regra de masquerade é removida, e as portas publicadas param de funcionar porque as regras de DNAT são removidas. Use publicação via loopback e a chain DOCKER-USER; elas corrigem a exposição sem quebrar a rede dos containers.

O Docker também ignora o UFW no IPv6?

No Docker Engine 27 e versões posteriores, o gerenciamento do ip6tables está ativado por padrão. Assim, uma porta publicada em uma rede Docker com IPv6 habilitado é reescrita contornando o UFW exatamente como no IPv4, e precisa da mesma regra DOCKER-USER espelhada com ip6tables. Em redes sem IPv6, o processo docker-proxy escuta em [::] e esse tráfego passa por INPUT, onde o UFW pode filtrá-lo se o UFW gerenciar o IPv6. Publicar em 127.0.0.1 evita ambos os casos, pois nada escuta em IPv6.