Tutoriais

Fechar portas expostas em servidor Linux

Inventário, decisão e aplicação de regras seguras com iptables e docker-compose para restringir portas a redes confiáveis (VPN) mantendo serviços internos funcionando — com safety net pra não se trancar fora

Servidores Linux que rodam Docker tipicamente expõem dezenas de portas no host por padrão — a cada ports: em algum docker-compose.yml aparece um novo binding em 0.0.0.0. Muitas dessas exposições são acidentais: bancos, painéis admin, APIs internas. Cada uma amplia a superfície de ataque sem necessidade.

Esse guia mostra como inventariar, decidir o que fechar, e fechar com segurança — sem se trancar fora.


Inventário: o que está exposto?

# TCP listening em 0.0.0.0 ou IPv6 ::
ss -tlnp | awk '$4 ~ /^(0\.0\.0\.0|\[::\]):/'

# UDP idem
ss -ulnp | awk '$4 ~ /^(0\.0\.0\.0|\[::\]):/'

# Mapear cada porta a um container
docker ps --format 'table {{.Names}}\t{{.Ports}}'

Para cada porta encontrada, três perguntas:

  1. Quem precisa acessar? Internet (público), rede privada/VPN, ou só apps internas?
  2. Tem caminho alternativo? Painel admin já está atrás de um reverse-proxy HTTPS? Então a exposição direta é redundante.
  3. Qual o custo de fechar? Quebra alguma app? Você perde acesso de gerenciamento?

Apps dentro de containers que se comunicam entre si (postgres:5432, keycloak:8080, etc.) usam DNS interno do Docker — não tocam a porta pública do host. Fechar a exposição externa não quebra essa comunicação interna.


Duas estratégias

A) Bind em loopback no docker-compose

Substituir "5432:5432" por "127.0.0.1:5432:5432" faz o docker-proxy escutar só no lo. A porta deixa de ser pública de fato, e você consegue acessar de scripts rodando no próprio host ou via SSH tunnel.

services:
  postgres:
    image: postgres:16
    ports:
      - "127.0.0.1:5432:5432"   # antes: "5432:5432"

Vantagens: documenta a intenção no compose; container recreate mantém a config. Custo: precisa docker compose up -d (recreate do container, downtime de segundos).

B) Firewall com iptables

Mantém o docker-proxy escutando em 0.0.0.0 (visível em docker ps), mas regras do iptables descartam o tráfego antes dele chegar ao processo:

# Aceitar de uma subnet confiável (ex: VPN)
iptables -A INPUT -p tcp --dport 5432 -s 10.0.0.0/24 -j ACCEPT
# Descartar o resto
iptables -A INPUT -p tcp --dport 5432 -j DROP

Vantagens: zero downtime; fácil reverter. Custo: docker ps continua mostrando 0.0.0.0:5432, parece exposto pra quem só olha lá.

Em geral: A para serviços críticos (bancos, brokers, identity providers — onde a config errada no compose é o tipo de bug que volta a doer); B para o resto.


Padrão de regras iptables seguro

Ordem importa. O setup mínimo robusto:

# 1. Loopback sempre permitido (processos no host se comunicam via lo)
iptables -A INPUT -i lo -j ACCEPT

# 2. Conexões já estabelecidas — preserva sessões SSH em curso ao aplicar DROPs
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# 3. Por porta a fechar: ACCEPT da subnet confiável, DROP do resto
for port in 22 5432 8080; do
  iptables -A INPUT -p tcp --dport "$port" -s 10.0.0.0/24 -j ACCEPT
  iptables -A INPUT -p tcp --dport "$port" -j DROP
done

A regra de RELATED,ESTABLISHED é fundamental. Sem ela, ao adicionar um DROP para a porta SSH que você está usando agora, a sessão atual sobrevive (estado TCP já estabelecido), mas qualquer nova conexão de IP não permitido cai.


Safety: rollback automático antes de aplicar

Antes de mexer em regras de firewall remotas, arme um timer de auto-rollback. Se a regra estiver errada e te trancar fora, em alguns minutos o estado retorna sozinho.

cat > /tmp/iptables-rollback.sh <<'EOF'
#!/bin/bash
sleep 600                                   # 10 minutos
iptables -D INPUT -p tcp --dport 22 -j DROP 2>/dev/null
iptables -D INPUT -p tcp --dport 22 -s 10.0.0.0/24 -j ACCEPT 2>/dev/null
echo "$(date): regras rollback automaticamente" >> /var/log/iptables-rollback.log
EOF
chmod +x /tmp/iptables-rollback.sh

# Aplique as regras
iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP

# E arme o rollback
nohup /tmp/iptables-rollback.sh > /dev/null 2>&1 &
echo $! > /tmp/iptables-rollback.pid

Validou que tudo funciona (nova sessão SSH conecta, apps respondem)? Mate o processo pra cancelar o rollback:

kill $(cat /tmp/iptables-rollback.pid)
rm /tmp/iptables-rollback.pid /tmp/iptables-rollback.sh

Se você não conseguir confirmar em 10 minutos (ficou sem acesso), as regras são removidas automaticamente e o servidor volta ao estado anterior.


Persistência entre reboots

Por padrão, regras iptables se perdem ao reiniciar. Em Debian/Ubuntu:

apt-get install -y iptables-persistent
iptables-save  > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6

O pacote habilita o serviço netfilter-persistent, que recarrega /etc/iptables/rules.v4 no boot.


Hardening do sshd em paralelo

Antes de restringir a porta SSH a uma subnet, vale validar que o sshd está endurecido. Cheque o efetivo (drop-ins podem sobrescrever o sshd_config principal):

sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|pubkeyauthentication)'

Se passwordauthentication yes aparecer mesmo você tendo configurado no, provavelmente um arquivo em /etc/ssh/sshd_config.d/ está sobrescrevendo (cloud-init costuma fazer isso). Crie um drop-in com prefixo numérico baixo (carrega primeiro, e no sshd o primeiro valor vence):

cat > /etc/ssh/sshd_config.d/00-hardening.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
EOF

sshd -t && systemctl reload ssh

E instale fail2ban pra brute-force lento:

apt-get install -y fail2ban
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd

[sshd]
enabled = true
port    = 22
EOF
systemctl enable --now fail2ban

Verificação

# Ver regras ativas com contadores
iptables -L INPUT -n -v --line-numbers

# De fora da rede confiável: deve dar timeout/refused
nc -zv -w 5 SEU_SERVIDOR 5432
nmap -p 22,5432,8080 SEU_SERVIDOR

# De dentro da rede confiável (VPN): deve conectar
nc -zv -w 5 SEU_SERVIDOR 5432

Sem acesso a outra rede pra testar, espere alguns minutos e olhe o contador da regra DROP. A internet faz scan constante em portas comuns — se o pkts da DROP subiu, o bloqueio está ativo.


Pegadinhas comuns

nginx + container recreate = 502

Se o nginx faz reverse-proxy via proxy_pass http://app:8080, ele resolve o nome do upstream uma vez ao carregar a config e cacheia o IP indefinidamente (sem resolver directive). Quando você recreia o container app, o IP muda — mas o nginx continua tentando o IP antigo e responde 502.

Soluções:

# Mais simples: restart do container nginx (reseta o cache de DNS)
docker restart nginx-proxy

# Ou no nginx.conf: força re-resolução periódica
resolver 127.0.0.11 valid=10s;     # DNS interno do Docker
set $upstream "http://app:8080";
proxy_pass $upstream;              # usar variável força lookup por request

nginx -s reload não é suficiente nesse caso em todas as versões — o restart completo do container é o caminho confiável.

iptables INPUT não bloqueia tráfego container-a-container

Regras na chain INPUT só afetam pacotes destinados ao host. Tráfego entre containers na mesma docker network passa pela FORWARD (e pelas chains DOCKER-USER / DOCKER-FORWARD). Se você quiser restringir só o acesso público a serviços expostos, regras em INPUT resolvem.

Fechar a porta SSH antes de validar

Se o único acesso de gerenciamento é SSH, valide a allowlist da porta 22 antes de fechar outras portas. Trancar a 22 sem ter conferido o source IP que sua VPN apresenta no host é receita pra precisar do console do provedor pra recuperar.

Dica: rode who ou last no servidor enquanto faz uma nova conexão SSH via VPN. O last_address mostrado é o IP que a regra precisa permitir.

IPs de container são dinâmicos

Se você whitelista 172.20.0.12 pensando que é o IP do container da VPN, lembre que esse IP muda se você recriar o container. Prefira whitelistar a subnet inteira da docker network (ex.: 172.20.0.0/16) — robusto a recreate.

docker-proxy continua escutando após DROP no iptables

Se você só aplicou regras iptables, o ss -tlnp ainda mostra 0.0.0.0:5432 (o docker-proxy segue lá). Isso é esperado — o bloqueio acontece no kernel antes do tráfego chegar ao processo. Pra realmente remover o binding, é a estratégia A (bind em 127.0.0.1).


Fluxo resumido

  1. ss -tlnp + docker ps → inventário
  2. Pra cada porta: pública / VPN-only / loopback-only / fechar tudo
  3. Loopback-only → editar ports: no compose pra 127.0.0.1:HOST:CONTAINER
  4. VPN-only → iptables com ACCEPT da subnet + DROP do resto
  5. Sempre arme rollback antes de aplicar regras remotas
  6. Validar (nova sessão SSH, apps respondem)
  7. Cancelar rollback
  8. Persistir com iptables-save > /etc/iptables/rules.v4
  9. Lembrar do docker restart nginx-proxy se algum upstream foi recreado