Blog

A Falácia da Confiança: Como Nossa Configuração Docker Multi-inquilino Vazou Dados (E Como Consertamos)

Saiba como a configuração ingênua de Docker de uma equipe levou a um vazamento de dados entre inquilinos e a estratégia de isolamento em camadas que o evitou.

Resumo

Os containers Docker não são isolados por padrão—eles compartilham o kernel do host e, sem configuração deliberada, os inquilinos podem interferir uns com os outros. Este artigo aborda um cenário real onde um provedor de hospedagem multi-inquilino descobriu que os containers dos clientes podiam acessar os bancos de dados uns dos outros devido à rede compartilhada e às configurações de segurança fracas. Mostramos as alterações passo a passo que corrigiram a brecha: redes definidas pelo usuário por inquilino, usuários não root, capacidades removidas, sistemas de arquivos somente leitura e perfis seccomp. Uma suposição comum é que os containers fornecem inerentemente um forte isolamento; desafiamos isso explicando por que as VMs ainda oferecem um limite mais rígido e quando considerar uma abordagem híbrida. A conclusão reforça que o isolamento é um exercício em camadas, não uma única caixa de seleção.

O Incidente: Quando os Containers Falam Demais

Você configurou o Docker em um único host para executar vários sites de clientes. Cada cliente tem seu próprio container—um ambiente limpo e isolado, certo? Foi o que pensamos. Até que uma auditoria de segurança de rotina revelou que o container do Cliente A estava lendo o socket MySQL do container do Cliente B no mesmo host. Eles compartilhavam a rede bridge padrão. Pior, os containers eram executados como root, então um invasor que comprometesse um poderia adulterar o socket Docker do host ou o sistema de arquivos de outro container. A brecha não foi um exploit sofisticado; foi uma configuração básica incorreta. Dados vazaram. A confiança se desfez.

O cenário de falha não é incomum. Muitas equipes assumem que os namespaces e cgroups do Docker automaticamente isolam os inquilinos, mas subestimam quantas portas de fuga permanecem abertas por padrão. As redes bridge padrão não oferecem isolamento de rede entre containers. Executar como root dá ao container mais poder do que o necessário. E sem limites explícitos de recursos, um vizinho barulhento pode privar outros de CPU ou memória.

Passo 1: Parar de Compartilhar uma Única Rede

Nossa primeira correção foi dar a cada inquilino sua própria rede Docker definida pelo usuário. Isso impede que os containers se alcancem, a menos que você os conecte explicitamente. Criamos um script que, para cada inquilino, cria uma rede dedicada e anexa seu container de aplicação a ela. O container do banco de dados vive na mesma rede do inquilino, mas também adicionamos uma rede interna apenas para comunicação intra-inquilino. Sem mais intromissões entre inquilinos.

Também isolamos os bancos de dados executando-os em containers separados na mesma rede do inquilino, usando volumes de dados separados. Isso garantiu que, mesmo que um invasor invadisse o container da aplicação, não pudesse bisbilhotar o tráfego do banco de dados de outro inquilino.

Para um mergulho mais profundo nas estratégias de isolamento de rede, veja a Uma Checklist Prática de Segurança de Isolamento Docker para Hospedagem Multi-inquilino.

Passo 2: Remover Privilégios Desnecessários

Por padrão, os containers Docker são executados com um conjunto limitado de capacidades Linux, mas ainda têm mais do que a maioria das aplicações precisa. Nossos containers eram executados como root, o que permitia que processos dentro deles realizassem ações como montar sistemas de arquivos ou alterar parâmetros do kernel. Mudamos para executar a aplicação como um usuário não root dentro do container (usando a diretiva USER no Dockerfile) e removemos todas as capacidades, exceto as absolutamente necessárias. Para uma aplicação web típica, isso pode ser apenas NET_BIND_SERVICE (para vincular a portas abaixo de 1024) e CHOWN (para escrever em diretórios). Também adicionamos --security-opt no-new-privileges para evitar escalada de privilégios.

Este passo sozinho eliminou muitos vetores comuns de escape de container. Um invasor que comprometa o servidor web não pode instalar pacotes, modificar binários do sistema ou acessar o socket Docker do host, porque o processo carece das capacidades CAP_SYS_ADMIN ou CAP_DAC_OVERRIDE.

Passo 3: Bloquear o Sistema de Arquivos

Sistemas de arquivos graváveis são uma superfície de ataque comum. Tornamos o sistema de arquivos raiz somente leitura (--read-only) para todos os containers e, em seguida, montamos sistemas de arquivos temporários (tmpfs) para diretórios que precisam de acesso de gravação, como /tmp e o diretório de cache da aplicação. Isso impede que um invasor modifique o código da aplicação ou persista binários maliciosos.

Além disso, usamos a opção --mount do Docker para montar bind de diretórios sensíveis, como o socket Docker, apenas quando absolutamente necessário—e nunca em containers de produção. O princípio: se o container não precisa gravar em um caminho, torne-o somente leitura.

Passo 4: Aplicar Perfis Seccomp e AppArmor

Os perfis seccomp padrão já bloqueiam muitas chamadas de sistema perigosas, mas os personalizamos ainda mais para permitir apenas as chamadas de sistema que nossa aplicação realmente precisa. Isso é uma troca porque requer a criação de perfil da aplicação. Uma abordagem mais simples é usar o perfil seccomp padrão do Docker e depois adicionar --security-opt seccomp=path/to/profile.json se precisar de regras mais rígidas. Da mesma forma, os perfis AppArmor podem confinar processos de container a caminhos de arquivo e capacidades específicas. Ativamos o AppArmor e usamos um perfil personalizado que restringia o acesso apenas aos diretórios de dados da aplicação.

Para um guia abrangente sobre essas etapas de endurecimento, consulte Fortalecendo Containers Docker para Hospedagem Multi-inquilino: Um Guia de Isolamento Passo a Passo.

A Visão Contrária: Às vezes Você Precisa de VMs

Por mais endurecidos que sejam, os containers compartilham o kernel do host. Uma vulnerabilidade do kernel pode quebrar todo o isolamento de uma vez. É por isso que muitas plataformas preocupadas com a segurança executam containers dentro de VMs leves—cada inquilino tem seu próprio kernel. Isso adiciona sobrecarga, mas fornece um limite no nível de hardware que os containers sozinhos não podem. Se seus inquilinos lidam com dados de cartão de crédito ou registros de saúde, uma abordagem híbrida (containers dentro de VMs) pode ser a escolha certa. Não presuma que o isolamento de container é suficiente para seu modelo de ameaça; avalie a sensibilidade dos dados e os requisitos regulatórios.

Para uma comparação mais profunda dos níveis de isolamento, leia Projetando uma Arquitetura Docker Multi-inquilino: Escolhendo o Nível de Isolamento Correto.

Conclusão: O Isolamento é uma Pilha, Não um Interruptor

A correção não foi uma única mudança—foi um empilhamento: isolamento de rede, privilégios limitados, sistemas de arquivos somente leitura e filtragem de chamadas de sistema. Mesmo assim, aceitamos que o isolamento perfeito é impossível com containers de kernel compartilhado. Para nossos inquilinos de maior segurança, os movemos para hosts dedicados. A lição: não confie em padrões. Audite sua configuração Docker como se uma violação já tivesse acontecido. A hora de bloquear é antes do vazamento, não depois.

Sources (5)