---
title: "Containers não são VMs: como reduzir o impacto de um escape de kernel"
description: "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."
date: "2026-09-24"
tags: ["containers", "seguranca", "namespaces", "seccomp", "kata-containers", "microvm", "devops", "kubernetes"]
url: "https://abjr.dev/noticias/containers-nao-sao-vms-escape-kernel"
---

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:

1. **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.
2. **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/kcore` e afins) via condição de corrida no `/dev/null` do 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](https://gvisor.dev/docs/architecture_guide/security/) — containers não são sandbox; redução da superfície de System API do host.
- [Firecracker design](https://github.com/firecracker-microvm/firecracker/blob/main/docs/design.md) — microVM, KVM, jailer, seccomp e superfície mínima de dispositivos.
- [Kata Containers](https://katacontainers.io/) — runtime OCI com isolamento por VM / guest kernel.
- [Hardening com Kata microVM no AKS (Microsoft)](https://techcommunity.microsoft.com/blog/coreinfrastructureandsecurityblog/hardening-openclaw-on-aks-mitigating-container-escapes-with-kata-microvm-isolati/4516030) — contraste prático host vs guest em `/proc`.
- [CVE-2024-21626 / Leaky Vessels (Tenable)](https://www.tenable.com/cve/CVE-2024-21626) — escape via file descriptor no runc.
- [CVE-2022-0492 e cgroups (Unit 42)](https://unit42.paloaltonetworks.com/cve-2022-0492-cgroups/) — por que seccomp/LSM matam a exploração em configs default.
- [CVE-2025-31133 (masked paths / runc)](https://notcve.org/cve/CVE-2025-31133) — race em maskedPaths e superfície do runtime OCI.
- [Comparativo runc vs gVisor vs Kata vs Firecracker](https://safeguard.sh/resources/blog/container-runtime-comparison-security) — matriz de isolamento, overhead e casos de uso.
- [Firecracker vs Cloud Hypervisor vs Kata (OpenComputer)](https://opencomputer.dev/guides/firecracker-vs-cloud-hypervisor-vs-kata/) — assimetria namespace/seccomp vs fronteira de hypervisor.
- [Kata vs gVisor (Northflank)](https://northflank.com/blog/kata-containers-vs-gvisor) — guest kernel + KVM versus Sentry em userspace.
