Todo pod Linux do cluster compartilha o mesmo kernel do nó onde foi agendado. Namespaces e cgroups separam arquivos, rede e processos entre containers, mas uma falha no kernel, no runtime ou em um privilégio mal concedido coloca o atacante no plano de execução comum a todos os workloads daquele host. Para equipes que executam código de terceiros, processam arquivos enviados por usuários ou rodam múltiplos tenants no mesmo cluster, a decisão prática não é apenas endurecer o perfil padrão do container: é isolar os pods críticos em um runtime com fronteira própria. Este guia mostra como usar RuntimeClass e gVisor para isso, quando o custo compensa e quais sinais monitorar depois da mudança.
O limite do kernel compartilhado
A arquitetura de containers é eficiente porque elimina a camada extra: em vez de um hypervisor por máquina virtual, todos os containers do nó enxergam o mesmo kernel do host. Essa economia é também o limite de segurança. A publicação Application Container Security Guide do NIST descreve o problema com precisão: containers, por compartilharem kernel e rodarem com capabilities e privilégios variáveis, não oferecem uma fronteira de segurança tão concreta quanto uma VM. O grau de segmentação entre eles é bem menor do que o hypervisor oferece a máquinas virtuais separadas.
Na prática, isso muda a leitura do risco. Um pod que processa upload de arquivos, executa código gerado por usuários, roda bibliotecas de terceiros em pipelines de CI ou serve múltiplos clientes não deveria dividir kernel com o pod do banco de dados financeiro no mesmo nó. O NIST recomenda segmentar containers por propósito, sensibilidade e postura de ameaça, garantindo que um dado kernel de host execute apenas containers de um mesmo nível de sensibilidade — com pools separados de VMs quando os níveis forem distintos.
Essa segmentação resolve a coabitação, mas não resolve a superfície do próprio kernel: mesmo em um nó dedicado, código não confiável continua falando diretamente com o kernel do host por meio de chamadas de sistema. É aí que entra o sandbox de runtime.
RuntimeClass escolhe o runtime por pod
O Kubernetes tem o mecanismo nativo para essa escolha: o recurso RuntimeClass, estável desde o Kubernetes v1.20, permite que pods diferentes usem configurações distintas de runtime no mesmo cluster. A documentação oficial cita exatamente o caso de uso de segurança: quando parte da carga merece alto nível de garantia de isolamento, o pod pode ser agendado em nós cujo runtime usa virtualização de hardware, ganhando isolamento extra em troca de alguma sobrecarga adicional.
O caminho operacional é direto. No containerd, cada handler de runtime é configurado em /etc/containerd/config.toml, na seção de runtimes. O objeto RuntimeClass do cluster aponta para esse handler e, com o campo scheduling, garante que o pod só seja agendado em nós que suportam o runtime escolhido. O campo overhead, estável desde o Kubernetes v1.24, declara o custo extra em CPU e memória para que o scheduler contabilize o sandbox no dimensionamento — sem ele, nós com sandbox dedicado tendem a ficar superlotados e a degradar o desempenho percebido.
Como o gVisor isola as syscalls
O gVisor, projeto mantido pelo Google, ataca o problema na origem: em vez de deixar o container chamar o kernel do host, ele interpõe um kernel de aplicação escrito em Go que roda em userspace. A documentação do gVisor explica que as interfaces de sistema normalmente implementadas pelo kernel do host são movidas para um kernel de aplicação distinto, por sandbox, justamente para minimizar o risco de exploração de container escape.
O componente central é o Sentry: ele implementa as chamadas de sistema, a entrega de sinais, o gerenciamento de memória e o modelo de threading que a aplicação espera. O acesso a sistema de arquivos passa pelo Gofer, um processo do host que conversa com o Sentry pelo protocolo 9P. O resultado é que a aplicação roda sem alterações — o runtime runsc implementa a especificação OCI e se integra a Docker e Kubernetes —, mas o kernel real do host passa a ter superfície de ataque muito menor, porque as syscalls da carga sandboxada são atendidas pelo Sentry, em userspace, e não pelo kernel do nó.
Isso não é o mesmo que seccomp, SELinux ou AppArmor. Esses mecanismos definem políticas sobre o que o container pode pedir ao kernel; o gVisor remove o kernel real do caminho das chamadas. Os dois níveis são complementares: perfis restritivos continuam necessários para o restante do cluster, e o sandbox cria a fronteira dura para as cargas que justificam o custo.
Onde aplicar sandbox na prática
Não vale rodar tudo em sandbox: a interposição de syscalls tem custo, e o próprio projeto alerta que nem toda syscall, arquivo de /proc ou /sys está implementada, o que pode gerar incompatibilidades pontuais. A regra prática é aplicar o sandbox onde a entrada é não confiável ou onde o impacto de um escape é alto.
| Carga | Risco típico | Ação recomendada |
|---|---|---|
| Processamento de arquivos enviados por usuários | Parsers e decodificadores são alvo clássico de escape | Sandbox via RuntimeClass com handler runsc (P1) |
| CI/CD executando código de pull requests | Pipeline roda código de terceiros dentro do cluster | Pool dedicado com sandbox e nós separados (P1) |
| Multi-tenancy entre clientes | Um tenant comprometido não pode enxergar os demais | Nós por nível de sensibilidade com sandbox (P1) |
| API pública com dependências de terceiros | RCE em biblioteca vira acesso ao nó inteiro | Perfil restrito primeiro; sandbox em nós sensíveis (P2) |
| Banco de dados interno crítico | Alvo de maior valor após um escape | Nó dedicado, sem coabitação com cargas expostas (P1) |
Priorização sugerida: em P1, inventariar quais nós recebem carga não confiável hoje e habilitar o runsc nesses pools, criando RuntimeClass com scheduling e overhead declarados; em P2, estender o sandbox para cargas sensíveis que dividem nó com workloads expostos e revisar as políticas de admissão que impedem pods privilegiados de chegar a esses nós. A combinação com a detecção de runtime no kernel com Falco e Tetragon cobre o outro lado do problema: o sandbox reduz a probabilidade do escape e a telemetria do nó reduz o tempo de detecção quando algo foge do comportamento esperado.
Escala também pela identidade. Um pod isolado que continua com credenciais de serviço amplas apenas muda o eixo do ataque, como discutido no guia de identidade de serviço entre clusters e nuvens. Sandbox de runtime e identidade de carga mínima são controles que se reforçam: um limita o que o atacante alcança no nó, o outro limita o que ele alcança no resto da plataforma.
Sinais a verificar no runtime
Depois de habilitar o sandbox, monitore três frentes. Compatibilidade: logs de syscalls não suportadas ou arquivos de /proc ausentes indicam carga que precisa de ajuste ou de runtime diferente. Overhead real: compare o consumo declarado no overhead do RuntimeClass com o medido em produção e recalibre o dimensionamento dos pools. Tentativas de escape: eventos do Sentry e alertas de runtime nos nós com sandbox devem ser tratados com a mesma prioridade que alertas de host, porque indicam carga tentando atingir interfaces que o sandbox deveria conter.