Notícias9 min de leitura
Containers não são VMs: como reduzir o impacto de um escape de kernel
Namespaces, seccomp, microVMs e Kata Containers: uma matriz prática de isolamento para reduzir o blast radius quando o kernel do host é o elo fraco.
- containers
- seguranca
- namespaces
- seccomp
- kata-containers
- microvm
- devops
- kubernetes
Você sobe um container como root, monta um volume e sente que “está isolado”. Na prática, o processo do container e o processo do host falam com o mesmo kernel. Namespaces escondem a vista. Cgroups limitam o apetite. Seccomp corta syscalls. Nada disso vira uma máquina virtual.
Eu não escrevo isso para espalhar pânico de CVE. Escrevo porque o modelo mental errado — “container = VM leve” — leva a decisões ruins de multi-tenancy, de sandbox de código não confiável e de hardening de cluster. O objetivo útil não é “nunca haver escape”. É reduzir o blast radius quando o kernel ou o runtime falham.
O que um container isola de verdade
Um container OCI típico (runc sob Docker, containerd ou CRI-O) é um processo Linux com um conjunto de namespaces:
- PID: o processo “vê” a própria árvore de processos.
- Mount: filesystem próprio (overlay + mounts).
- Network: stack e interfaces próprias (ponte, veth, CNI).
- UTS / IPC / User: hostname, IPC e, quando habilitado, mapeamento de UIDs.
Isso resolve isolamento lógico: IDs de processo, rotas, arquivos aparentes. Não resolve isolamento de kernel. Toda chamada open, ioctl, bpf, mount, clone ainda entra no mesmo código que serve o nó. Se esse código tem um bug explorável e o container consegue exercitar o caminho vulnerável, o escape deixa de ser ficção.
Cgroups entram no outro eixo: quanto de CPU, memória, I/O e PIDs o workload pode consumir. São limites de recurso, não fronteira de segurança contra aritmética de ponteiro no kernel.
A frase que eu uso com time: namespaces escondem o mapa; o kernel continua sendo o terreno compartilhado.
Seccomp, capabilities e LSM: a superfície que você ainda controla
Antes de falar em microVM, dá para cortar muito da superfície sem trocar o runtime.
Capabilities. Containers não precisam de CAP_SYS_ADMIN, CAP_NET_ADMIN ou CAP_SYS_PTRACE no dia a dia. Docker já dropa um conjunto amplo por padrão. Kubernetes, se você não declarar securityContext, deixa o pod mais permissivo do que a maioria imagina — especialmente se alguém cola privileged: true “só para o sidecar funcionar”.
Seccomp. Filtra quais syscalls o processo pode fazer. O perfil default do Docker bloqueia caminhos que aparecem com frequência em exploits de escape (por exemplo, certos usos de bpf ou unshare em cenários clássicos). No Kubernetes, o default histórico é não aplicar seccomp até você configurar RuntimeDefault ou um perfil customizado. Essa assimetria explica por que o mesmo CVE “não funciona no Docker padrão” e “funciona num pod solto”.
LSM (AppArmor / SELinux). Políticas mandatórias em cima do que o processo pode tocar no host. Não substituem seccomp; fecham outro eixo (arquivos, labels, transições).
Um exemplo didático: a CVE-2022-0492 (escape via cgroups v1) exigia, na prática, uma combinação feia — sem seccomp, sem AppArmor/SELinux úteis, com capacidade de criar user namespace, etc. Com hardenings default, o caminho morria. Sem eles, o “container root” ganhava uma porta para o host. A lição não é “aquele CVE específico”. É: filtros de syscall e política mandatória transformam muitos escapes de kernel em dead ends.
Regra prática: em todo cluster, trate seccompProfile: RuntimeDefault, usuário não-root, capabilities dropadas e filesystem read-only como o piso. Elevação de privilégio vira exceção documentada, não atalho de debug que ficou em produção.
Escapes reais: runtime e kernel no mesmo desenho
Quando alguém diz “container escape”, misturam duas classes:
- Bug do runtime (runc, containerd, cri-o): falha na montagem, no file descriptor, no masked path, no pivot_root. O atacante não precisa de zero-day de kernel; precisa de um erro no código que monta o isolamento.
- Bug do kernel: o processo já está “dentro” e encontra um caminho privilegiado via syscall.
Dois casos públicos úteis para calibrar o medo, sem tutorial de exploit:
- CVE-2024-21626 (Leaky Vessels): vazamento de file descriptor no runc permitia que um processo do container enxergasse caminhos do host filesystem. Patchear o runc importa tanto quanto “confiar no kernel”.
- CVE-2025-31133: abuse de masked paths (montagens que deveriam esconder
/proc/kcoree afins) via condição de corrida no/dev/nulldo container. De novo: a superfície é o runtime OCI, não só “o Linux é inseguro”.
O desenho mental correto: container = processo + política + runtime. Kernel compartilhado é o pior caso; runtime com bug é o caso frequente o bastante para aparecer em bulletin. Hardenings (seccomp, caps, LSM, não-privileged) elevam o custo do ataque. Não zeraram a classe de risco.
gVisor: kernel em userspace, sem virar VM
Entre “runc no host” e “microVM completa” existe o gVisor (runsc). A ideia: o workload não fala direto com o kernel do host. Fala com o Sentry, um kernel reimplementado em userspace que interpreta a maior parte das syscalls. Só um conjunto estreito de operações chega ao host, tipicamente sob filtro seccomp agressivo.
O que isso compra:
- Redução real da superfície de System API do host (o modelo de segurança do gVisor é explícito: container sozinho não é sandbox).
- Bom encaixe para código semi-confiável, agentes que executam shell, workloads multi-tenant “amigáveis”.
O que isso custa:
- Compatibilidade incompleta de syscalls — aplicações exóticas quebram ou precisam de workaround.
- Overhead em I/O e caminhos quentes de syscall.
- Continua existindo um host por baixo; bypass do Sentry + bug no que resta ainda é uma cadeia possível, só que mais longa.
Eu trato gVisor como sandbox de syscall, não como substituto de hypervisor. Se o modelo de ameaça é “código arbitrário de cliente desconhecido no mesmo nó que outros clientes”, a conversa sobe para microVM.
MicroVMs e Kata Containers: mudar a fronteira
Uma VM clássica (ou microVM) dá ao workload um guest kernel. O processo comprometido “escapa” para dentro da VM. Para tocar o host, precisa de uma segunda classe de bug: escape de hypervisor / KVM. Essa assimetria é o ponto.
Firecracker é um VMM mínimo (Amazon): poucos dispositivos virtio, boot rápido, superfície pequena, pensado para multi-tenant serverless. O próprio processo do Firecracker ainda deve rodar sob jailer (namespaces, cgroups, seccomp) — defesa em profundidade no host.
Kata Containers não é um hypervisor. É um runtime OCI compatível com Kubernetes/containerd: para o orquestrador parece container; por baixo, sobe uma VM por pod (ou sandbox) com guest kernel. O VMM pode ser QEMU, Cloud Hypervisor ou Firecracker, conforme a config. Para o time de plataforma, isso importa: você ganha isolamento de hardware-assisted virtualization sem reescrever o manifesto do Deployment.
Demonstrações úteis (como hardenings com Kata em AKS) mostram o contraste: em container clássico, brincar com /proc/1/root pode apontar para o filesystem do host. Em Kata, o PID 1 que você vê é o da microVM — o nó e o /etc/kubernetes do host simplesmente não existem naquele universo. Escape de container vira “sou root na minha VM pequena”, não “sou root no nó”.
Custo típico: mais RAM por sandbox, boot um pouco maior, operação de runtime adicional, e a necessidade de KVM (ou nested virt) no nó. Para workload confiável da própria empresa, muitas vezes é overkill. Para CI de código de terceiros, funções multi-tenant, ou agentes que executam o que o modelo inventar, o custo começa a fazer sentido.
Matriz prática de isolamento
| Camada | Fronteira | Se o workload “escapa”, o dano típico vira… | Overhead | Quando usar |
|---|---|---|---|---|
| runc + namespaces/cgroups | Kernel do host | Compromisso do nó e dos outros pods | Mínimo | Código confiável, mesmo tenant |
| + seccomp, drop caps, LSM, não-root | Syscalls e política filtradas | Escape mais difícil; muitos CVEs morrem aqui | Baixo | Piso para todo cluster |
| gVisor (runsc) | Kernel em userspace (Sentry) | Bypass do Sentry + o que resta no host | Médio; compat. de syscall | Sandbox sem KVM; código semi-confiável |
| Kata / microVM (Firecracker, etc.) | Hypervisor (KVM) + guest kernel | Escape de hypervisor (classe mais rara) | Boot/RAM maiores | Multi-tenant, código adverso, forte blast-radius |
Leitura correta da tabela: as linhas somam, não substituem. Rodar Kata e dropar capabilities e aplicar seccomp no guest é defesa em profundidade. Trocar só o runtime e deixar o pod privileged é teatro.
Checklist de produção
- Pods sem
privileged;allowPrivilegeEscalation: false; usuário não-root. -
capabilities.drop: ["ALL"]e add só o mínimo documentado. -
seccompProfile.type: RuntimeDefault(ou perfil mais restrito) no cluster inteiro. - Root filesystem read-only; volumes explícitos para o que precisa escrever.
- Sem montar socket do Docker/containerd,
/var/run, ou hostPath sensível “porque o debug pediu”. - NetworkPolicy de egress/ingress; o isolamento de processo não substitui lateral movement na rede.
- Runtime e node image com patch recente (runc/containerd/kernel) — escapes de FD e mount não esperam o seu roadmap.
- Classificar workloads: confiável → runc endurecido; semi-confiável → gVisor ou sandbox equivalente; multi-tenant / adverso → Kata ou microVM.
- Para agentes de IA ou CI que executam código de PR: assumir código hostil até prova em contrário; isolamento de kernel (ou VM) faz parte do desenho, não do “fase 2”.
Escolher pelo modelo de ameaça
“Container seguro” é frase de marketing. O desenho sério pergunta: se este processo ganhar execução arbitrária, o que ainda é possível no nó e nos vizinhos?
Se a resposta for “quase tudo no host”, você está no modelo runc clássico — ótimo para app própria com imagem assinada e política dura. Se a resposta precisar ser “quase nada no host”, namespaces não bastam: mude a fronteira para guest kernel / hypervisor, e continue aplicando o piso de seccomp e capabilities.
Containers não são VMs. Dá para aproximar o efeito de uma VM quando o risco pede. A matriz acima é o atalho que eu uso para não misturar os dois no mesmo slide — e no mesmo nó.
Se amanhã você só pudesse endurecer uma coisa no cluster, seria RuntimeDefault + não-root em massa, ou já migrar o workload mais hostil para Kata?
Referências
- gVisor Security Model — containers não são sandbox; redução da superfície de System API do host.
- Firecracker design — microVM, KVM, jailer, seccomp e superfície mínima de dispositivos.
- Kata Containers — runtime OCI com isolamento por VM / guest kernel.
- Hardening com Kata microVM no AKS (Microsoft) — contraste prático host vs guest em
/proc. - CVE-2024-21626 / Leaky Vessels (Tenable) — escape via file descriptor no runc.
- CVE-2022-0492 e cgroups (Unit 42) — por que seccomp/LSM matam a exploração em configs default.
- CVE-2025-31133 (masked paths / runc) — race em maskedPaths e superfície do runtime OCI.
- Comparativo runc vs gVisor vs Kata vs Firecracker — matriz de isolamento, overhead e casos de uso.
- Firecracker vs Cloud Hypervisor vs Kata (OpenComputer) — assimetria namespace/seccomp vs fronteira de hypervisor.
- Kata vs gVisor (Northflank) — guest kernel + KVM versus Sentry em userspace.