Blog
Quais clientes realmente precisam de sua própria VM? Um plano de isolamento Docker em camadas
Uma VM para cada cliente é exagero. Veja como decidir quanto isolamento cada locatário precisa—e automatizar a decisão.
Sumário
Agências muitas vezes entram em pânico quando um cliente pergunta quão verdadeiramente isolados estão seus dados de outros locatários. Os namespaces e cgroups do Docker oferecem isolamento real, mas não são o mesmo que uma barreira de hardware. Em vez de rodar cada cliente em uma VM—ou pior, tratar todos os clientes da mesma forma—crie um pequeno conjunto de camadas de isolamento e associe cada cliente a uma delas com base em sensibilidade de dados, confiança e conformidade. Um contêiner travado (não-root, capabilities removidas, seccomp, root somente leitura) cobre a maioria dos sites; cargas de trabalho reguladas ou hostis recebem uma VM ou um híbrido de contêiner em VM. Este post oferece um fluxo de decisão repetível, uma tabela de comparação e uma visão honesta de quando mais isolamento é exagero.
Você já chegou ao ponto em uma ligação de vendas em que o novo cliente diz: "somos da área da saúde, mostre-me que nossos dados estão isolados dos seus outros clientes" e você preferiria falar sobre qualquer outra coisa?
Este é o problema da agência: não uma implantação perfeita, mas a mesma implantação confiável repetida em uma dúzia de clientes com orçamentos, perfis de risco e exigências de conformidade diferentes. Aqui está a versão honesta. O isolamento do Docker é real, mas específico. Namespaces dão a cada contêiner sua própria visão de processos, rede e sistema de arquivos; cgroups limitam CPU, memória e I/O de disco para que os locatários não possam prejudicar uns aos outros. O que isso não lhe compra é uma parede de hardware entre o contêiner e o kernel do host. Se um atacante escapar do contêiner, ele estará dentro do único kernel que você tem. O restante deste artigo transforma esse fato desconfortável em uma decisão repetível: classifique cada cliente por sensibilidade de dados e confiança, aplique um perfil de endurecimento básico e só recorra a uma VM quando o custo de uma violação for maior do que o custo da VM.
Espere, os contêineres já não são isolados?
Docker roda em namespaces e cgroups do Linux, e essas palavras estão fazendo trabalho real. Namespaces separam IDs de processo, pilhas de rede, pontos de montagem e usuários para que um processo em um contêiner não possa ver a tabela de processos de outro. Cgroups definem limites: dê a um contêiner 0,5 CPU, 512 MB de memória e um peso fixo de I/O de bloco, e é exatamente isso que ele recebe. Um loop descontrolado em um locatário é limitado em vez de derrubar o vizinho. Se você não configurou limites, você pulou a coisa mais básica para a qual os cgroups existem.
Considere um aplicativo PHP simples no contêiner A. Ele vê seu próprio sistema de arquivos, sua própria interface de rede, seu próprio PID 1. O contêiner B tem o mesmo, mas uma visão diferente. Isso são namespaces. Agora, afaste-se e pule o limite de memória: o contêiner A pode encher a RAM do host e fazer o contêiner B rastejar. É para isso que os cgroups existem. Mas dois contêineres podem ser isolados um do outro por namespaces e ainda assim compartilhar o kernel do host, que é a parte sobre a qual toda história de escape de contêiner fala. Uma exploração que atinge o kernel pode potencialmente alcançar todos os locatários nesse host.
"Docker é isolado" é uma frase meia-verdade. A versão precisa é "Docker isola com namespaces e cgroups, e uma vulnerabilidade de kernel é o raio de impacto". Antes de confiar a um locatário a execução de código não confiável, reflita sobre isso por um minuto. A resposta não é "nunca use contêineres"—esse é o pânico fácil. A resposta é um sistema de camadas.
Então por que alguns clientes precisam de mais do que namespaces?
A resposta honesta é que isolamento não é um interruptor, é um espectro. Em uma extremidade, você tem um contêiner totalmente compartilhado onde todos estão efetivamente em um aplicativo. Na outra extremidade, você tem uma VM separada por locatário com seu próprio kernel. A maior parte do trabalho de agência vive no meio desconfortável, e o meio não é uma escolha binária entre "Docker é suficiente" e "rode uma VM para todos".
O que empurra um cliente para a direita não é o tamanho dele. São quatro perguntas:
- Eles armazenam dados regulados? Registros de saúde, detalhes de cartão de pagamento, qualquer coisa que um regulador chame de sensível.
- Uma violação no locatário deles tem um caminho realista para outro locatário? Se eles podem executar código arbitrário, sim.
- Você confia no código e nas pessoas que o implantam? Um cliente que contrata o freelancer mais barato não tem o mesmo nível de confiança que um cliente cujo time de desenvolvimento você conhece.
- O contrato deles diz "dedicado", "isolado" ou "privado"? Se diz, você já prometeu uma camada; o único trabalho agora é escolher a certa.
Se você ainda não consegue responder a essas perguntas, coloque o cliente em uma camada base e anote as premissas. Isso não é uma auditoria de segurança; é uma verificação de sanidade que você repete em cada integração.
Como decidir por cliente sem rodar uma auditoria de segurança toda vez?
Faça uma tabela pequena e comprometa-se com ela. Você não precisa de uma matriz com quarenta células. Quatro camadas cobrirão quase todos os clientes que uma agência vê.
| Posição do cliente | O que realmente os separa | Use quando |
|---|---|---|
| Camada 1: Aplicativo/contêiner compartilhado | Somente lógica do aplicativo | Utilitários internos, dados de baixo risco, projetos em que todos estão explicitamente em um sistema de login |
| Camada 2: Mesmo host, contêineres separados | Namespaces e cgroups | A maioria dos sites de marketing, formulários de contato, sem dados sensíveis |
| Camada 3: Contêiner travado | Camada 2 + não-root, capabilities removidas, seccomp, root somente leitura, segmentação de rede | E-commerce, PII, código personalizado que você não confia totalmente |
| Camada 4: VM por locatário | Hipervisor e um kernel separado | Saúde, finanças, documentação de conformidade, código não confiável, vizinhos barulhentos |
Veja como isso funciona na prática. Um cliente de padaria com formulário de contato e link do Instagram vai para a Camada 2: um contêiner em um host compartilhado, rede Docker padrão, limites de recursos, pronto. Uma loja online que armazena nomes, endereços e redirecionamentos de pagamento vai para a Camada 3: mesmo host compartilhado, mas o contêiner roda como um usuário não-root, não tem capabilities extras de kernel, usa um perfil seccomp e expõe apenas a porta 443. Um portal de triagem médica que armazena informações de saúde protegidas vai para a Camada 4: uma VM por locatário, porque o custo de uma violação não é "vamos limpar isso", mas "não podemos mostrar ao cliente que o levamos a sério".
O truque é que você não está repensando a arquitetura para cada cliente. Você está escolhendo uma linha de uma tabela que já concordou. É assim que uma agência de cinco pessoas pode administrar cem sites sem cem obsessões de segurança separadas. Isso também significa que o próximo cliente não recebe uma resposta que depende de qual membro da equipe atendeu o telefone. Para o debate mais profundo sobre arquitetura por trás dessas escolhas, este guia sobre como projetar níveis de isolamento multi-locatário aborda as compensações em mais detalhes.
Como é realmente um contêiner travado?
Vamos parar de dizer "travado" e ser concretos. É isso que a Camada 3 significa para um cliente típico de WordPress ou PHP.
Primeiro, mude o usuário. A maioria das imagens oficiais ainda roda como root por padrão; no seu Dockerfile, crie um usuário não-root e rode o aplicativo como esse usuário. Isso remove imediatamente a maneira mais comum de um comprometimento do contêiner se tornar um comprometimento do host. Segundo, remova as capabilities que você não precisa. Rode com --cap-drop ALL e adicione de volta apenas uma, geralmente NET_BIND_SERVICE para que o aplicativo possa escutar na porta 80. Só isso já é uma mudança maior do que a maioria das pessoas espera. Terceiro, torne o sistema de arquivos raiz somente leitura com --read-only e monte diretórios graváveis (uploads, o diretório de dados do banco) como volumes ou tmpfs. Quarto, aplique um perfil seccomp e, se o seu host suportar, AppArmor ou SELinux. Finalmente, coloque o contêiner em uma rede Docker dedicada e exponha apenas as portas que realmente precisam ser alcançáveis.
Vamos percorrer um exemplo de WordPress. A imagem base provavelmente roda como root, então você adiciona uma etapa useradd e uma diretiva USER. Você roda o contêiner com um limite de memória e um limite de CPU, para que uma rajada de tráfego de plugins não prejudique o vizinho. Você monta /var/www/html/wp-content/uploads como um volume gravável. Você define --read-only. Você o anexa a uma rede que não tem nenhuma flag --privileged por perto. O resultado é um contêiner que costumava ser "um site WordPress" e agora é "um site WordPress que por acaso está mais travado do que a maioria dos servidores privados virtuais".
Se fazer tudo isso manualmente parece frágil, há um caminho intermediário mais fácil: o Enhanced Container Isolation do Docker, que usa isolamento de namespace de usuário e um runtime de contêiner seguro. É um atalho legítimo, mas não é um passe livre para pular o não-root ou a remoção de capabilities. O locatário ainda precisa de uma imagem sensata. A diferença é que a superfície de ataque voltada para o kernel fica menor sem que você se torne um especialista em seccomp da noite para o dia. Se você quiser a sequência exata para um único locatário, o guia passo a passo de endurecimento de isolamento transforma esta seção em comandos de copiar e colar.
Quando eu paro de adicionar camadas e simplesmente entrego uma VM?
Aqui está a parte contrária: mais isolamento não é automaticamente melhor. VMs oferecem isolamento em nível de hardware, um kernel separado e uma superfície de ataque muito menor se o kernel convidado cair. É exatamente isso que clientes de saúde e finanças esperam quando dizem "queremos ser isolados". Mas cada VM adiciona custo de correção, backup e computação, e multiplica o trabalho de manter uma frota atualizada. Se você usa VM para todo cliente porque um cliente uma vez te disse que o Docker o assustava, você comprou teatro de segurança com dinheiro real.
Uma VM é a resposta certa quando o risco por locatário é maior do que o custo operacional de uma VM por locatário. Isso significa dados regulados, requisitos escritos de conformidade, código de terceiros não confiável ou um cliente que precisa que um vizinho barulhento seja removido. Também é a resposta certa quando o contrato do cliente promete literalmente um ambiente dedicado, porque "contêiner" não é o que eles imaginam quando assinam "dedicado".
Mas uma VM não desculpa um contêiner descuidado. Uma armadilha comum é colocar o cliente em uma VM e pular o endurecimento porque "a VM os protege". A VM protege o host do locatário, não o locatário de sua própria imagem ruim. Você ainda quer não-root, capabilities removidas e seccomp dentro dessa VM. A abordagem híbrida—contêineres dentro de uma VM—muitas vezes é o ponto ideal: a VM fornece o limite para conversas de conformidade, e o contêiner fornece o fluxo de trabalho de implantação que você já conhece. Há uma versão mais longa desse debate em Todo locatário deve ter sua própria VM?, mas a resposta curta é que a VM é para o contrato, não para o medo.
Como tornar isso repetível em todos os clientes?
Você torna repetível fazendo do sistema de camadas um modelo, não uma memória. Mantenha um diretório de arquivos Compose, um por camada: tier2-baseline, tier3-locked, tier4-vm-hybrid. Quando um novo cliente aparece, copie o modelo, altere as variáveis de ambiente e você já sabe a forma de isolamento antes de escrever uma linha de nova infraestrutura.
Em seguida, anote a decisão. Não um relatório de segurança de 400 páginas, mas um parágrafo curto no repositório do cliente: quais dados eles armazenam, em qual camada estão, por que e o que os moveria para uma camada superior. Esse parágrafo vale mais do que cem regras de firewall, porque é a coisa que você pode mostrar ao próximo auditor ou ao próximo cliente preocupado. Também evita que você tenha que lembrar por que a padaria recebeu a Camada 2 e a loja de e-commerce a Camada 3 depois que a chamada de vendas original esmoreceu.
Automatize as verificações chatas. Faça seu CI escanear cada imagem de cliente e falhar uma compilação se ela rodar como root, se tiver todas as capabilities ou se tentar publicar uma porta diferente daquelas que a camada permite. Nada disso é exótico; é apenas garantir que o modelo não seja quebrado acidentalmente por um desenvolvedor bem-intencionado. Se você está construindo o fluxo de trabalho de hospedagem ao redor de qualquer maneira, o artigo sobre estratégias de hospedagem Docker prontas para produção cobre a parte que vem depois que os contêineres são definidos.
Nada disso é glamoroso. Nenhum post de blog vai fazer "isolamento de locatário" parecer tão empolgante quanto um diagrama de arquitetura greenfield. Mas esta é a diferença entre uma agência que responde "quão isolados estamos?" com um "totalmente" de dedos cruzados e uma que pode mostrar uma camada, uma configuração e um motivo. Contêineres não são uma parede mágica. VMs não são uma bala de prata. Um sistema de camadas é apenas uma decisão que você escreve e reutiliza—e para uma agência, repetível é o jogo todo.

