Pular para o conteúdo
10 min de leitura

NetworkPolicy no Kubernetes: Segmentar a Rede Antes Que Alguém Ande de Lado

Por Equipe Nebular ·

Por padrão, todo pod fala com todo pod no seu cluster. Veja como usar NetworkPolicy para aplicar zero-trust de rede, isolar cargas e conter movimento lateral.

Neste artigo

Existe um detalhe do Kubernetes que assusta muita gente na primeira vez que descobre: por padrão, qualquer pod pode abrir conexão com qualquer outro pod do cluster, em qualquer namespace. O banco de dados do time de pagamentos está, no nível de rede, tão acessível a partir de um pod de um blog institucional quanto a partir da própria aplicação de pagamentos. Não há firewall interno. O cluster nasce chapado.

Enquanto tudo funciona, ninguém nota. O problema aparece no dia em que um único container é comprometido — uma dependência vulnerável, um upload malicioso, uma CVE em uma biblioteca. A partir dali, o atacante não está preso ao pod que invadiu: ele varre a rede interna, encontra o Redis sem senha, o Postgres na porta padrão, o endpoint de métricas que vaza tokens. Isso se chama movimento lateral, e é justamente o que a segmentação de rede existe para conter.

NetworkPolicy é o objeto do Kubernetes que transforma esse cluster plano em um conjunto de zonas isoladas. Bem usado, ele implementa zero-trust de rede: nada conversa com nada até que uma regra explícita autorize. Este guia mostra o modelo mental, os manifestos que importam e as armadilhas que quebram produção quando você aperta o cerco sem cuidado.

O modelo mental: políticas são aditivas e "puxam" o deny#

A primeira coisa a entender é contraintuitiva. Uma NetworkPolicy não é uma regra de firewall no sentido tradicional, onde você escreve "bloqueie X". Ela funciona por seleção e permissão.

O comportamento é o seguinte: enquanto nenhuma política seleciona um pod, esse pod continua no modo default do cluster — aberto, aceitando tudo. No instante em que ao menos uma política passa a selecioná-lo para uma direção (ingress ou egress), aquele pod vira "isolado" naquela direção, e a partir daí o que as políticas permitirem explicitamente é aceito. Todo o resto passa a ser negado.

Isso tem duas consequências práticas. Primeiro: políticas são aditivas — se duas políticas selecionam o mesmo pod, o tráfego permitido é a união das duas. Você nunca escreve uma regra de negação; você escolhe o que abrir, e o fechamento vem de graça. Segundo: para chegar a um estado de zero-trust real, o passo inicial é aplicar uma política default-deny por namespace, que seleciona todos os pods sem permitir nada. A partir desse chão negado, você libera exceção por exceção.

O default-deny como fundação#

O manifesto que estabelece o chão de um namespace é este:

```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: pagamentos spec: podSelector: {} # {} = TODOS os pods do namespace policyTypes:

  • Ingress
  • Egress

```

O podSelector: {} casa com todos os pods. Como o spec não declara nenhuma regra ingress nem egress, o resultado é: todo pod do namespace pagamentos fica isolado nas duas direções e nada entra nem sai. É o cadeado fechado. A partir daqui, qualquer comunicação que a aplicação precise volta a ser possível apenas com políticas de exceção.

Muita gente aplica só o default-deny de ingress e esquece o egress. Isso deixa metade do problema em aberto: um pod comprometido continua conseguindo abrir conexões para fora — exfiltrar dados, baixar um segundo estágio, escanear a rede. Zero-trust de verdade fecha as duas mãos.

Anatomia de uma regra de permissão#

Com o chão negado, você libera o que a aplicação legitimamente precisa. Digamos que a API de pagamentos precise receber tráfego apenas do gateway e falar apenas com o Postgres. As duas exceções ficam assim:

```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: api-pagamentos-ingress namespace: pagamentos spec: podSelector: matchLabels: app: api-pagamentos policyTypes:

  • Ingress

ingress:

  • from:
  • podSelector:

matchLabels: app: gateway ports:

  • protocol: TCP

port: 8080 ```

Leia com atenção o bloco from. Ele diz: aceite ingress apenas de pods com o label app: gateway, e apenas na porta TCP 8080. Qualquer outro pod, qualquer outra porta, continua negado pelo default-deny. O podSelector dentro de from é avaliado no mesmo namespace da política, a menos que você combine com um namespaceSelector.

Essa distinção entre seletor de pod e seletor de namespace é onde mora boa parte dos erros. Um from pode conter três tipos de origem: podSelector (pods no namespace da política), namespaceSelector (todos os pods de namespaces que casam com o label) e ipBlock (faixas de IP, úteis para tráfego externo). E há uma sutileza crítica: quando você escreve podSelector e namespaceSelector no mesmo item da lista from, eles funcionam como E lógico (pods com aquele label E naquele namespace). Quando estão em itens separados da lista, funcionam como OU. Trocar um pela outra abre ou fecha portas que você não pretendia.

Egress e a armadilha do DNS#

O egress segue a mesma gramática, mas tem uma pegadinha que derruba aplicações no primeiro deploy de default-deny: o DNS. Quase toda aplicação resolve nomes (postgres.pagamentos.svc.cluster.local) antes de abrir a conexão. A resolução é uma consulta ao kube-dns/CoreDNS, na porta 53, TCP e UDP, em um pod que vive no namespace kube-system. Se o seu default-deny de egress não liberar isso, a aplicação passa a falhar com erros de "name resolution" que parecem qualquer coisa menos um problema de rede.

A liberação correta do DNS costuma ser:

```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-dns-egress namespace: pagamentos spec: podSelector: {} policyTypes:

  • Egress

egress:

  • to:
  • namespaceSelector:

matchLabels: kubernetes.io/metadata.name: kube-system ports:

  • protocol: UDP

port: 53

  • protocol: TCP

port: 53 ```

Guarde essa regra: sempre que você aplicar default-deny de egress, libere o DNS junto, no mesmo commit. Do contrário, você vai passar a próxima meia hora depurando um "bug misterioso" que é apenas a resolução de nomes bloqueada.

Sem o CNI certo, nada disso funciona#

Aqui está o detalhe que mais surpreende quem escreve a primeira política e vê que ela não faz efeito nenhum: o Kubernetes não implementa NetworkPolicy sozinho. O objeto é apenas uma especificação. Quem a aplica de verdade — programando iptables, eBPF ou equivalente nos nós — é o plugin de rede, o CNI.

E nem todo CNI implementa NetworkPolicy. O Flannel puro, por exemplo, historicamente ignora o objeto: você aplica um default-deny, o cluster aceita o manifesto sem reclamar, e o tráfego continua passando como se nada tivesse acontecido. É um falso senso de segurança perigosíssimo. Já CNIs como Calico e Cilium implementam o padrão de forma robusta.

Antes de confiar a segurança do cluster a políticas, valide que o CNI as respeita. Um teste direto: aplique um default-deny em um namespace de teste, suba dois pods e tente um curl de um para o outro.

``bash kubectl -n teste run cliente --image=nicolaka/netshoot -it --rm -- \ curl -m 3 http://servidor:8080 ``

Se o comando trava e expira (em vez de responder), a política está sendo aplicada. Se responde normalmente, o seu CNI está ignorando NetworkPolicy e você precisa de outro plugin ou de outra camada de enforcement.

O que o Cilium adiciona além do nativo#

O NetworkPolicy nativo do Kubernetes opera nas camadas 3 e 4: endereços, portas, protocolos. Ele não entende o que trafega dentro da conexão. Você consegue dizer "o gateway pode falar na porta 8080 da API", mas não "o gateway só pode fazer GET /health".

CNIs baseados em eBPF, como o Cilium, estendem isso com objetos próprios (CiliumNetworkPolicy) capazes de aplicar regras de camada 7: filtrar por método e caminho HTTP, por tópico Kafka, por consulta DNS específica. É a diferença entre segmentar por porta e segmentar por intenção da requisição. Para a maioria dos clusters, o NetworkPolicy nativo já é um salto enorme de postura; regras L7 entram quando o modelo de ameaça exige granularidade fina dentro de conexões permitidas.

Padrões que resolvem 80% dos casos#

Na prática, você não escreve uma política nova e artesanal para cada par de serviços. Alguns padrões cobrem a imensa maioria das necessidades reais, e vale tê-los no repertório.

O primeiro é o isolamento por namespace: permitir que pods de um namespace conversem livremente entre si, mas negar tudo que venha de fora. É o modelo de "cada time no seu quintal", útil quando namespaces representam equipes ou domínios distintos. Ele se expressa liberando ingress apenas de origens do próprio namespace:

```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: permitir-mesmo-namespace namespace: app spec: podSelector: {} policyTypes:

  • Ingress

ingress:

  • from:
  • podSelector: {} # qualquer pod do PRÓPRIO namespace

```

O segundo é o namespace de utilidade compartilhada: um namespace de monitoração (com Prometheus, por exemplo) precisa fazer scrape das métricas de todos os outros. Em vez de listar cada serviço, você libera ingress a partir do namespace de monitoração usando um namespaceSelector que casa com o label dele. Assim, a fonte da conexão é identificada pela origem inteira, não por pod.

O terceiro é o acesso à internet de forma controlada. Muitos workloads precisam falar com APIs externas, mas quase nunca precisam falar com a rede interna do cluster ou com os IPs de metadados do provedor de nuvem — um alvo clássico de ataques de SSRF. Com ipBlock no egress, você libera o tráfego para fora e ao mesmo tempo exclui faixas sensíveis:

```yaml egress:

  • to:
  • ipBlock:

cidr: 0.0.0.0/0 except:

  • 169.254.169.254/32 # endpoint de metadados da nuvem
  • 10.0.0.0/8 # rede interna

```

Esse padrão de "internet sim, rede interna e metadados não" fecha um dos vetores mais explorados quando um pod é comprometido: o acesso ao serviço de metadados que, em muitas nuvens, entrega credenciais temporárias da instância.

Adotando sem quebrar produção#

A tentação de aplicar um default-deny em todos os namespaces numa sexta à noite é real, e o resultado costuma ser um incidente. Segmentação de rede quebra o que estava implicitamente aberto, e quase sempre há mais dependências ocultas do que o diagrama de arquitetura mostra. Um caminho seguro:

  • Rotule tudo primeiro. Políticas dependem de labels consistentes em pods e

namespaces. Antes de escrever uma regra, garanta que cada workload tenha labels estáveis de app, tier ou team, e que os namespaces carreguem o label kubernetes.io/metadata.name (adicionado automaticamente em versões recentes).

  • Comece pelo egress observando, não bloqueando. Alguns CNIs oferecem modo de

auditoria ou logs de fluxo (o Hubble, do Cilium, é um exemplo). Rode primeiro em modo de observação para descobrir o mapa real de quem fala com quem antes de cortar.

  • Aplique namespace por namespace, do menos crítico ao mais crítico. Valide

cada um com testes de conectividade positivos (o que deve passar, passa) e negativos (o que não deve, expira) antes de seguir.

  • Libere o DNS no mesmo commit do default-deny de egress. Sempre. É o erro

número um.

  • Trate as políticas como código. Elas vivem no Git, passam por revisão e são

aplicadas pelo mesmo pipeline que o resto do manifesto. Uma regra de rede alterada à mão no cluster é uma porta que ninguém sabe que abriu.

A recompensa dessa disciplina é concreta. Num cluster segmentado, comprometer um pod deixa de significar comprometer a rede inteira. O atacante que entra pelo blog não alcança o banco de pagamentos, porque no nível de rede aquele caminho simplesmente não existe — não porque um firewall o negou depois de tentar, mas porque nunca foi autorizado a começar. Essa é a diferença entre um cluster que apenas funciona e um cluster que também contém o dano quando algo dá errado. E algo, uma hora, sempre dá.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly