A regra que você escreveu, e a regra que o Docker escreveu
Dois programas estão editando o mesmo firewall com premissas diferentes, e só um deles avisa você sobre isso. O ufw grava as suas regras na chain INPUT da tabela filter — o caminho que um pacote percorre quando o destino é o próprio host. O Docker grava na tabela nat e na FORWARD — o caminho que um pacote percorre quando o destino é outro lugar.
Siga um único pacote e a brecha fica óbvia. Alguém em outro país abre uma conexão com o seu endereço na porta 5432. Ela chega, e a primeira coisa que encontra é a nat PREROUTING, que a envia para a chain DOCKER do Docker. Ali uma regra DNAT reescreve o destino para 172.17.0.2:5432 — o contêiner. O pacote deixou de estar endereçado ao seu servidor, então o kernel o roteia em vez de entregá-lo localmente: ele passa pela FORWARD, onde o Docker já instalou um ACCEPT para o tráfego destinado a uma porta publicada. Em nenhum momento dessa jornada o pacote passa pela INPUT, que é a única chain que o ufw filtra. A sua regra nunca esteve errada. Ela simplesmente nunca foi consultada.
É também por isso que o sintoma é tão desorientador. O ufw status verbose continua relatando Default: deny (incoming), com nada liberado além de 80, 443 e a sua porta SSH, enquanto um scanner em outro continente mantém uma sessão aberta com o seu banco de dados. As duas afirmações são verdadeiras. O firewall está fazendo exatamente o que você configurou, sobre o tráfego que recebeu — e o tráfego que importa não está chegando até ele.
Nada disso é um bug, e nada disso é descuido do Docker. Um motor de contêineres precisa programar regras de NAT e de encaminhamento, ou os contêineres simplesmente não alcançam a rede, e ele não tem como adivinhar com segurança quais regras do firewall do seu host deveriam se aplicar a eles. Então ele faz a coisa honesta: gerencia as próprias chains e te entrega uma chain dedicada — a DOCKER-USER, avaliada antes de tudo o mais na FORWARD — que promete nunca sobrescrever. A brecha não é a existência do mecanismo. A brecha é que -p 5432:5432 parece dizer "deixe isso disponível" e na verdade significa "publique isso em todo endereço que esta máquina responde, para além do firewall em que você passou uma hora trabalhando".
Como uma porta exposta se parece vista de fora
A internet percebe mais rápido do que as pessoas esperam. Faixas de hospedagem são escaneadas de forma contínua e exaustiva — não por alguém que se interessou especificamente por você, mas por crawlers comerciais, projetos de pesquisa e botnets oportunistas que varrem todo endereço roteável em toda porta interessante e publicam ou vendem os resultados. O intervalo entre um docker compose up -d e a primeira conexão não solicitada a uma porta de banco de dados recém-publicada se mede em horas. Ninguém precisou adivinhar o seu hostname. Ninguém precisou saber o seu nome. O endereço estava na faixa.
O que eles encontram depende inteiramente do que você publicou, e os casos comuns são sombrios. Um contêiner PostgreSQL ou MySQL iniciado a partir do snippet de início rápido de uma imagem, com a senha trivial desse snippet ainda em vigor. Uma instância de Elasticsearch ou MongoDB que nunca foi configurada para exigir autenticação, porque só deveria ser alcançável a partir do contêiner da aplicação. Um memcached que responde a qualquer um — o que não é só um vazamento de dados, mas um amplificador que pode ser apontado contra outra pessoa, transformando o seu servidor em participante do tipo de inundação descrito no nosso runbook da primeira hora de DDoS. Um painel de administração ou dashboard de fila de mensagens que presumia uma rede privada. Um endpoint de métricas listando tranquilamente cada serviço interno, hostname e versão que você roda.
Vale nomear o dano de segunda ordem, porque o dano de primeira ordem não é a conta inteira. Um banco de dados alcançável pela internet não é apenas legível — na maioria dos motores ele também é gravável, o que significa que o invasor não precisa de mais nenhum exploit para instalar persistência, e vários motores podem ser induzidos a escrever arquivos ou executar comandos no host a partir de uma sessão privilegiada. Em uma máquina que você escolheu justamente por não carregar identidade nenhuma, um acesso não autenticado também é um elo de volta para tudo o mais que essa máquina toca: o destino de backup que ela alcança, as chaves nas suas variáveis de ambiente, os outros contêineres na sua bridge.
A parte incômoda é que nada disso se anuncia. Não existe uma linha de log que diga "o seu firewall foi contornado". O serviço funciona, a aplicação conecta, o site está no ar, e o único sinal externo é uma contagem de conexões que ninguém está observando. A exposição é descoberta por você, deliberadamente, nos próximos dez minutos — ou por outra pessoa, no tempo dela.
SP·03Leia o que você está realmente publicando
Comece pelo que o motor acha que está fazendo. O docker ps imprime uma coluna PORTS, e a distinção que ela mostra é o assunto inteiro deste guia: 0.0.0.0:5432->5432/tcp significa todo endereço da máquina, 127.0.0.1:5432->5432/tcp significa somente loopback, e um simples 5432/tcp sem seta significa que a porta está exposta a outros contêineres e não é publicada em lugar nenhum. Leia cada linha dessa coluna em cada contêiner antes de mudar qualquer coisa.
Depois olhe os sockets com ss -tulpen. Em uma instalação padrão você vai ver o docker-proxy segurando as portas publicadas, porque o Docker ainda inicia um pequeno processo userland por porta publicada. Aqui está a armadilha que custa uma tarde inteira a muita gente: se o userland-proxy estiver desativado no seu daemon — é um ajuste de configuração comum, e algumas distribuições já vêm assim — não existe socket nenhum escutando no host. O ss não mostra nada, o lsof não mostra nada, e a porta continua escancarada, porque a regra DNAT do kernel faz o trabalho sem que processo nenhum precise segurar o endereço. Uma saída silenciosa do ss não é prova de que a porta está fechada.
Então leia as regras propriamente ditas. O iptables -t nat -S DOCKER lista uma linha DNAT por porta publicada, e cada linha carrega a resposta que você quer: uma regra com -d 127.0.0.1/32 é uma publicação em loopback, e uma regra sem restrição de destino se aplica a todo endereço que a máquina possui. Faça o mesmo com o ip6tables, porque as duas famílias são configuradas de forma independente, e uma máquina pode estar fechada em uma e aberta na outra.
Por fim — e este é o único passo que realmente prova alguma coisa — olhe para a máquina a partir de outro lugar. Todo comando acima roda no host e herda a própria visão do host sobre a sua rede. O tráfego de loopback pula as chains que importam, então o sucesso de um curl 127.0.0.1:5432 não diz nada sobre se um estranho consegue fazer o mesmo, e o seu fracasso diz ainda menos. O teste que realmente vale é um escaneamento a partir de outra máquina, em outra rede, nas duas famílias de IP. Tudo antes disso é hipótese.
Vínculo em loopback, e a diferença entre ports e expose
O menor ajuste útil tem onze caracteres. O -p 127.0.0.1:5432:5432 diz ao Docker para escrever a sua regra DNAT com uma restrição de destino, de modo que a reescrita só se aplica a tráfego que já era local. Um pacote remoto dirigido ao seu endereço público deixa de corresponder, não é encaminhado ao contêiner, e finalmente chega onde você sempre presumiu que chegaria: a INPUT, onde o ufw o nega. Em um arquivo Compose a mesma coisa é ports: ["127.0.0.1:5432:5432"], e as aspas importam — um valor sem aspas com dois-pontos é um acidente de parsing esperando para acontecer.
Mas a pergunta melhor é por que a porta está publicada, para começo de conversa. Contêineres ligados à mesma rede definida pelo usuário se alcançam diretamente, pelo nome do serviço, na própria porta do contêiner, sem publicação nenhuma. A sua aplicação não se conecta a 127.0.0.1:5432; ela se conecta a postgres:5432, resolvido pelo DNS embutido do Docker para um endereço na bridge privada. Um banco de dados nesse arranjo não precisa de linha ports: nenhuma — nem em loopback, nem de nenhum outro tipo. A porta publicada mais segura é a que você apagou. Mantenha ports: só para os um ou dois serviços que realmente encaram o público, e deixe tudo o mais conversar na rede privada.
É também aqui que o expose: costuma ser mal interpretado. Ele não publica nada e não abre nada; é documentação que registra em qual porta um serviço escuta, e não tem efeito nenhum sobre o firewall, em nenhuma das duas direções. As pessoas o adicionam esperando que seja a versão segura de ports:, e de fato é — no mesmo sentido em que um comentário é a versão segura do código. Se você quer um serviço alcançável só pelos vizinhos, não precisa de expose:; precisa da ausência de ports:.
Dois limites honestos do vínculo em loopback. Primeiro, ele protege a fronteira do host, não a vizinhança: contêineres na mesma rede bridge continuam se alcançando livremente entre si, então um contêiner de front-end comprometido tem um caminho livre até um banco de dados que não publica nada. Separe os serviços em redes distintas e marque a de back-end como internal: true quando o raio de impacto importar. Segundo, 127.0.0.1 é um endereço IPv4 e restringe somente IPv4; se o host tiver um /64 roteável — todo plano aqui inclui um — raciocine sobre o v6 separadamente, e teste-o separadamente.
DOCKER-USER: a chain que existe exatamente para isso
O vínculo em loopback resolve os contêineres de que você se lembra. A DOCKER-USER é o que te tira da posição de estar sempre a um -p de distância do próximo incidente. O Docker a instala como o primeiro salto na FORWARD, à frente das suas próprias regras de aceitação, e — diferente de tudo mais nas suas chains — deixa o conteúdo intocado entre reinicializações, upgrades e novos contêineres. É o lugar reconhecido para a política que o motor não tem como inferir sozinho: quais origens têm permissão para alcançar contêineres neste host, ponto.
O padrão é três regras na interface pública, e a ordem é tudo. Primeiro, devolva o tráfego estabelecido e relacionado, para que as respostas às conexões que os seus contêineres abriram continuem fluindo. Segundo, devolva as origens que você realmente quer deixar entrar — um endereço do escritório, um host de monitoramento, um servidor parceiro. Terceiro, descarte tudo o mais que chegar da internet. Errar a regra de conntrack é a falha clássica aqui: coloque o DROP primeiro e toda conexão de saída de todo contêiner morre no pacote de retorno, o que se apresenta como "o Docker quebrou o DNS e a instalação de pacotes" e manda as pessoas procurarem exatamente no lugar errado.
Dois detalhes operacionais decidem se isso sobrevive ao contato com a realidade. Precisa estar restrito à interface pública, pelo nome — -i eth0, ou o que quer que ip route get 1.1.1.1 reporte na sua máquina — ou você também vai descartar tráfego entre as suas próprias bridges. E precisa ser reaplicado depois de cada reinicialização, depois que o daemon do Docker já tiver criado a chain. Uma unidade systemd oneshot ordenada com After=docker.service é a forma confiável; salvar com netfilter-persistent também funciona, desde que você aceite que restaurar um conjunto de regras inteiro enquanto o Docker está simultaneamente reconstruindo o dele é uma corrida que você deveria verificar, não presumir.
Depois a metade do IPv6. Se ip6tables -S DOCKER-USER imprimir uma chain, espelhe cada regra nela. Se der erro, o seu daemon simplesmente não está gerenciando regras de v6 — o que não diz nada sobre se o tráfego v6 alcança os seus contêineres, só que o Docker não está filtrando esse tráfego. Não tente chegar a uma resposta por dedução aí; os caminhos variam por versão, por configuração do daemon e por distribuição, e uma conclusão errada e confiante é pior do que nenhuma conclusão. Escaneie a si mesmo com nmap -6 e acredite no resultado.
Uma coisa a não fazer: recorrer a "iptables": false no /etc/docker/daemon.json. Isso impede o Docker de tocar no firewall, e também impede que o NAT de contêineres, o masquerading de saída e o isolamento entre redes sejam configurados por qualquer coisa. Você não eliminou o problema, você herdou o trabalho — manualmente, para cada contêiner, para sempre. Em um único VPS, a escolha honesta é deixar o Docker gerenciar as suas chains e assumir a DOCKER-USER por conta própria.
A outra porta: o socket, e o que roda como root
Tudo acima é sobre pacotes chegando. Esta seção é sobre o que eles encontram, e existe um item tão pior que o resto que merece ser dito à parte: montar o /var/run/docker.sock dentro de um contêiner equivale a dar root no host para esse contêiner. Não "chega perto". Equivale. Qualquer coisa capaz de falar com esse socket consegue iniciar um novo contêiner privilegiado com o sistema de arquivos do host montado dentro dele, e a partir daí ler cada chave, escrever cada arquivo e instalar o que quiser. Muitas imagens convenientes pedem isso — dashboards, auto-atualizadores, proxies reversos com service discovery. Trate esse pedido como uma decisão de confiar na imagem tanto quanto você confia no seu próprio shell root, e tenha o mesmo cuidado com a API do Docker sobre TCP: um daemon sem autenticação na porta 2375 é a mesma porta, só que escancarada para a internet inteira.
Além disso, o runtime de contêineres te dá quatro reduções baratas, e nenhuma delas exige reformular arquitetura nenhuma. Rode como usuário não-root com user: "1000:1000", porque o padrão é root dentro do namespace, e esse é o ponto de partida de toda fuga de contêiner. Defina security_opt: ["no-new-privileges:true"], o que impede um binário setuid dentro da imagem de ganhar mais privilégio do que o processo tinha ao iniciar. Remova todas as capacidades com cap_drop: [ALL] e devolva só o que o serviço comprovadamente precisa — a maioria das aplicações web não precisa de nenhuma. Monte o sistema de arquivos raiz com read_only: true e dê a ele um tmpfs pequeno para uso temporário, o que transforma "deixar um web shell na pasta da aplicação" de um passo em um beco sem saída.
A mesma lógica vale para o que é entregue ao contêiner. Segredos passados como variáveis de ambiente ficam visíveis para qualquer coisa capaz de ler o ambiente do processo, e são fielmente copiados para a saída do docker inspect e para qualquer log ou relatório de falha que despeje a configuração; um arquivo montado como somente leitura em um caminho conhecido é menos conveniente e consideravelmente menos vazador. E um contêiner não precisa de mais rede do que o seu trabalho exige: um worker que só fala com o banco de dados não tem motivo nenhum para conseguir abrir conexões com a internet.
Se tudo isso parece estar lutando contra o padrão, é uma leitura justa, e é o argumento a favor do Docker ou do Podman rootless — onde o daemon e os contêineres rodam como um usuário sem privilégios, uma fuga de contêiner te deixa como esse usuário em vez de como root, e as portas publicadas ficam nas mãos de um processo userland comum no host, o que significa que as suas regras do ufw se aplicam a elas do jeito normal. A troca é real: algumas capacidades, alguns drivers de armazenamento e alguns truques de rede se comportam de forma diferente, ou nem existem. Vale a pena saber que essa escolha existe e o que ela compra, em vez de descobrir depois de um incidente que o padrão também era uma escolha.
SP·07Imagens que você não escreveu, em uma máquina que você não pode se dar ao luxo de perder
Uma imagem de contêiner é o sistema de arquivos de outra pessoa, rodando na sua máquina, montado a partir de camadas que você não leu. Isso não é um argumento contra usá-las — é um argumento a favor de saber quais você está rodando e há quanto tempo. As duas falhas que realmente acontecem em infraestrutura pequena são ambas banais: a imagem nunca foi confiável, ou era confiável em março e ninguém a reconstruiu desde então.
A primeira se resolve principalmente com disciplina sobre a origem das imagens. Prefira repositórios oficiais ou publicados pelo próprio fornecedor a um fork conveniente com três estrelas, e desconfie especialmente de imagens cujo apelo é empacotar seis serviços em uma linha de YAML. Onde importa, fixe por digest em vez de por tag: postgres:17 é um alvo móvel que pode mudar debaixo de você entre duas execuções de docker compose pull, enquanto postgres@sha256:… é exatamente o sistema de arquivos que você testou. Fixar por digest troca correções automáticas por reprodutibilidade, o que é a troca certa quando você tem o hábito de reconstruir, e a errada quando não tem.
A segunda é a que morde em silêncio. Atualizações automáticas não tocam nos seus contêineres. As atualizações de segurança automáticas que você configurou na primeira hora corrigem os pacotes do host e não têm visibilidade nenhuma sobre o userland dentro de uma imagem — então uma máquina que se reporta totalmente corrigida pode estar rodando um servidor web a partir de uma imagem-base com um ano de vulnerabilidades não corrigidas. Contêineres não são atualizados, são substituídos: faça pull, recrie, e remova o que não é mais referenciado, em uma cadência que você realmente cumpre. Uma vez por mês, anotado, vale mais do que uma intenção perfeita.
O que faz do arquivo Compose o objeto mais valioso do servidor. É a única descrição completa do que aquela máquina é, e reconstruir a partir dele deveria ser uma operação de rotina, não um projeto de arqueologia — mantenha-o sob controle de versão, mantenha os arquivos de ambiente ao lado dele, e mantenha os dois no backup criptografado fora do site que guarda os seus dados. O teste de um host de contêineres não é se ele está rodando. É se você conseguiria colocá-lo de pé de novo, identicamente, em um VPS novo em cerca de 15 min.
SP·08Logs, volumes, e o estado que sobrevive ao contêiner
Contêineres são anunciados como descartáveis, o que é verdade para o processo e falso para tudo que ele deixa para trás. Dois tipos de estado se acumulam em um host Docker, e os dois pegam as pessoas de surpresa no pior momento.
O primeiro são os logs. O driver padrão json-file captura cada linha que os seus contêineres escrevem em stdout e stderr, e — a menos que você diga o contrário — nunca os rotaciona. Uma aplicação tagarela pode encher um disco assim em semanas, e um disco cheio em um host de banco de dados é um incidente à parte. Defina um teto uma única vez, em /etc/docker/daemon.json, para que ele valha para tudo que você iniciar depois.
Existe uma leitura de privacidade para essa mesma configuração, e em um servidor que você escolheu por privacidade ela é sem dúvida a mais importante. Um log de acesso não rotacionado é um registro permanente e não indexado do endereço IP, do user agent e do caminho de requisição de cada visitante, sentado em um disco que você não controla fisicamente. Você não decidiu manter isso. O padrão decidiu por você. Decidir de propósito significa limitar a retenção, e — melhor ainda — cortar o que é escrito desde o início: um formato de log no proxy que omite ou trunca endereços de cliente preserva o valor operacional e descarta o passivo. Dados que você nunca escreveu não podem ser apreendidos, intimados ou vazados, o mesmo raciocínio que leva as pessoas a manter o próprio nome fora do servidor, para começo de conversa.
O segundo são os volumes, e a lâmina afiada é uma flag. docker compose down para e remove os contêineres e deixa os volumes nomeados intocados; docker compose down -v os apaga, permanentemente, com a mesma confiança. Bancos de dados moram nesses volumes. Assim como qualquer outra coisa que faria falta. Mantenha os seus dados em volumes nomeados, não anônimos, saiba qual comando você está digitando às duas da manhã, e lembre-se de que o docker system prune existe para liberar espaço e tem prazer em liberar o seu.
O que deixa o backup em si, onde a abstração de contêiner engana uma última vez. Copiar o diretório de volume de um banco de dados ativo não é um backup; é uma cópia de arquivos que estavam sendo escritos enquanto você os lia, e ela restaura como um banco de dados corrompido bem na hora em que você mais precisa. Faça backup através do motor — pg_dump, mysqldump, a própria API de snapshot do motor — depois criptografe o resultado e mande para um lugar que o seu servidor não alcança. Essa última cláusula é a que importa quando o invasor já está dentro do contêiner: um backup que o seu host comprometido consegue apagar também não é um backup.
Passo a passo
-
01
Faça o inventário do que a máquina está publicando agora
Antes de mudar qualquer coisa, anote o estado atual — você vai querer comparar com ele depois. Leia a coluna
PORTSde cada contêiner, depois leia as regras de NAT que o Docker realmente instalou, nas duas famílias de IP. Tudo que aparecer sem um destino127.0.0.1é alcançável pela internet, não importa o que oufw statusdiga.docker ps --format 'table {{.Names}}\t{{.Ports}}' ss -tulpen | grep -E 'docker|LISTEN' sudo iptables -t nat -S DOCKER # -d 127.0.0.1/32 = loopback only sudo ip6tables -t nat -S DOCKER # may not exist; that is an answer too sudo ufw status verbose # what you believed was trueAproveite e anote o nome da interface pública — as regras de firewall mais adiante vão precisar dele, e nem toda imagem usa
eth0.ip route get 1.1.1.1 | awk '{print $5; exit}' -
02
Escaneie a si mesmo a partir de outro lugar
O host não consegue auditar a si mesmo: o tráfego de loopback nunca atravessa as chains que você está tentando testar. Rode isso a partir de outra máquina, em outra rede — um segundo VPS a partir de $8.00/mês em outra região é a bancada de testes honesta, e continua útil depois. Escaneie as duas famílias, porque são configuradas separadamente.
# from ANOTHER machine, against your server nmap -Pn -sS -p- --min-rate 1000 203.0.113.10 nmap -Pn -6 -p 22,80,443,3306,5432,6379,9200,27017 2001:db8::1 # no nmap? one port at a time is enough to settle an argument nc -zv 203.0.113.10 5432
Toda porta aberta que não seja 80, 443 ou a sua porta SSH é um achado. Guarde a saída; ela é a metade "antes" da prova que você vai querer no final.
-
03
Pare de publicar o que não precisa ser público
Agora corrija isso na origem, no arquivo Compose. Os serviços de back-end migram para uma rede privada e perdem a linha
ports:por completo — a aplicação os alcança pelo nome do serviço. Tudo que precisa ser alcançável a partir do próprio host recebe um vínculo explícito em loopback em vez de uma porta nua. Marque a rede de back-end comointernalpara que nada nela consiga falar com a internet sem permissão.services: db: image: postgres:17 # ports: ["5432:5432"] # deleted: the app reaches it as db:5432 networks: [back] volumes: [dbdata:/var/lib/postgresql/data] app: image: myapp:1.4 environment: DATABASE_URL: postgres://app@db:5432/app ports: ["127.0.0.1:8080:8080"] # loopback only; proxy sits in front networks: [back, front] networks: front: {} back: internal: true volumes: dbdata: {}Recrie a stack e confirme que as regras de NAT mudaram de forma — a linha DNAT da aplicação agora deve carregar um destino em loopback, e o banco de dados não deve ter linha nenhuma.
docker compose up -d docker ps --format 'table {{.Names}}\t{{.Ports}}' sudo iptables -t nat -S DOCKER -
04
Coloque exatamente uma coisa na frente do público
Com tudo em loopback, um único proxy reverso vira a única superfície pública — o único lugar onde o TLS é terminado, o único lugar onde um endereço de cliente chega a ser visto, e o único lugar onde um formato de log é decidido. Rode-o no host ou em um contêiner que publique 80 e 443 e nada mais; se rodar em um contêiner, ele entra na rede
fronte faz proxy para nomes de serviço, não para loopback.# in the http{} block of /etc/nginx/nginx.conf — no client address recorded log_format privacy '- - [$time_local] "$request" $status $body_bytes_sent'; # /etc/nginx/conf.d/app.conf (proxy on the host) server { listen 443 ssl; listen [::]:443 ssl; server_name example.com; access_log /var/log/nginx/app.log privacy; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }Se o objetivo do exercício é que ninguém descubra onde a máquina está, o proxy pertence a uma máquina totalmente diferente — essa é a configuração em esconda o seu IP de origem atrás de um proxy reverso, e ela combina perfeitamente com tudo daqui.
-
05
Feche a brecha com a DOCKER-USER, nas duas famílias
Vínculos em loopback resolvem os contêineres de hoje; isto resolve os que você ainda não escreveu. Três regras na interface pública, conntrack primeiro para que a saída dos contêineres continue funcionando, depois as suas origens permitidas, depois um descarte. Teste a conectividade de saída de um contêiner logo depois de aplicar — essa é a regra que as pessoas costumam inverter.
PUB=$(ip route get 1.1.1.1 | awk '{print $5; exit}') sudo iptables -I DOCKER-USER 1 -i "$PUB" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN sudo iptables -I DOCKER-USER 2 -i "$PUB" -s 203.0.113.7/32 -j RETURN # your admin address sudo iptables -I DOCKER-USER 3 -i "$PUB" -j DROP sudo iptables -S DOCKER-USER docker run --rm alpine sh -c 'apk update' >/dev/null && echo 'egress OK'Espelhe isso para IPv6 se o daemon gerenciar regras de v6, depois faça isso sobreviver a uma reinicialização com uma unidade que roda depois que o Docker já reconstruiu as suas chains.
sudo ip6tables -S DOCKER-USER || echo 'no v6 chain: verify v6 reachability by scanning' # /etc/systemd/system/docker-user-rules.service [Unit] After=docker.service Requires=docker.service [Service] Type=oneshot ExecStart=/usr/local/sbin/docker-user-rules.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target
-
06
Reduza privilégios dentro dos contêineres, e limite os logs
Reduza o que uma imagem comprometida consegue fazer, e pare o host de se encher com um registro que ninguém escolheu manter. As quatro configurações de contêiner não custam nada em um serviço web normal; o teto de log vale para todo contêiner iniciado depois que o daemon recarrega.
services: app: image: myapp:1.4 user: "1000:1000" read_only: true tmpfs: [/tmp] cap_drop: [ALL] security_opt: ["no-new-privileges:true"] # never: volumes: ["/var/run/docker.sock:/var/run/docker.sock"]# /etc/docker/daemon.json { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }sudo systemctl reload docker docker inspect --format '{{.HostConfig.LogConfig}}' app -
07
Verifique de fora, depois escreva o runbook
Reinicie de propósito, enquanto nada depende da resposta, e prove que o estado que você construiu é o estado que volta. Depois repita o escaneamento externo do passo dois e compare com a saída que você guardou — esse diff é o que importa entregar, não os comandos.
sudo reboot # after it returns: sudo iptables -S DOCKER-USER # rules reapplied? docker ps --format 'table {{.Names}}\t{{.Ports}}' # from ANOTHER machine again: nmap -Pn -p- 203.0.113.10 nmap -Pn -6 -p- 2001:db8::1Anote cinco linhas em algum lugar onde você vá encontrá-las depois: a qual interface as regras estão vinculadas, onde mora a unidade, quais serviços são deliberadamente públicos, quando você reconstruiu as imagens pela última vez, e como restaurar os volumes. Um host de contêineres só é tão bom quanto a descrição que permite reconstruí-lo depois do pior dia.


