Blog

Cada inquilino deve ter sua própria VM?

Escolha entre contêineres por inquilino, VMs e configurações híbridas com uma estrutura de decisão baseada em risco e as etapas de endurecimento que tornam cada opção defensável.

Resumo

A hospedagem multi-inquilino obriga você a escolher até onde os inquilinos podem alcançar uns aos outros. Os contêineres usam namespaces do Linux e cgroups para isolar processos e recursos, mas compartilham o kernel do host. As máquinas virtuais adicionam um limite de nível de hardware, ao custo de velocidade e carga operacional. Uma abordagem híbrida — contêineres dentro de VMs — pode oferecer ambos, mas dobra a superfície que você precisa corrigir. Este artigo apresenta uma decisão baseada em risco, uma comparação lado a lado e as etapas de endurecimento do Docker que importam mesmo dentro de uma VM. Ao final, você saberá qual modelo de isolamento se adequa aos seus inquilinos e o que configurar antes do lançamento.

Seu aplicativo multi-inquilino está quase pronto. Você tem um arquivo Docker Compose que cria uma pilha por cliente, e é rápido. Então um amigo que administra uma empresa de hospedagem pergunta: 'Você está dando a cada inquilino sua própria VM?' Você congela. Você não planejou essa pergunta. Este artigo oferece uma maneira de respondê-la hoje, sem uma equipe de segurança. Você faz isso sozinho, então a decisão precisa ser simples o suficiente para ser defendida às 2 da manhã.

Pare de tentar encontrar o modelo 'melhor'. Comece anotando o que acontece se o código de um inquilino assumir o controle do seu host. Defina o raio de explosão antes de escolher qualquer ferramenta. Esse exercício dirá mais do que qualquer benchmark jamais dirá.

O Kernel é o Colega de Quarto que Você Não Pode Despejar

Contêineres são eficientes porque compartilham o kernel do host. Esse compartilhamento é todo o truque, e todo o risco. Os namespaces do Linux dão a cada contêiner sua própria visão dos processos, rede e sistema de arquivos. Os grupos de controle (cgroups) permitem limitar CPU, memória e I/O de disco para que um inquilino não possa privar os outros. Mas nenhum deles cria uma parede de hardware.

Pense em um contêiner como um processo com uma identidade falsa muito boa. Ele acredita que está em sua própria máquina. O kernel, no entanto, é uma cópia do Linux rodando no seu host. Se um inquilino explora uma vulnerabilidade do kernel, os namespaces se tornam metadados e nada mais. Um atacante que possa chamar funções do kernel pode alcançar outros namespaces no mesmo kernel. Essa é a fuga de contêiner da qual você continua ouvindo falar.

Digamos que você hospede uma pequena ferramenta B2B com um contêiner por cliente. Um cliente instala um plugin suspeito com uma falha de execução remota de código. Com as configurações padrão do Docker, esse processo está rodando como root dentro do contêiner. Root em um contêiner ainda é UID 0, e o kernel não distingue esse UID do root do host, a menos que você mapeie usuários explicitamente. O atacante pode tentar escapar, e o kernel compartilhado é o alvo.

A falha não precisa ser dramática. Um único inquilino vazando memória pode empurrar o host para a troca (swap), desacelerando todos os outros inquilinos. Sem limites de cgroup, um loop mal comportado é um ataque de disponibilidade. Com eles, é um processo bloqueado e um alerta.

Isso significa que contêineres são inseguros? Não. Significa que você deve tratar o kernel como uma zona de confiança compartilhada. Antes de escolher, escreva uma declaração de risco de um parágrafo: 'Se o contêiner de um inquilino for comprometido, o atacante pode acessar: [lista]. O custo para o negócio seria: [valor ou impacto].' Se esse parágrafo te assusta, você não é paranóico. Você é honesto.

Para um olhar mais aprofundado sobre o espectro de isolamento, de contêineres compartilhados a pilhas totalmente separadas, consulte nosso guia sobre como projetar uma arquitetura Docker multi-inquilino.

Três Maneiras de Dividir (Escolha Uma Antes de Implantar)

Existem realmente três arquiteturas para isolamento multi-inquilino. Toda 'melhor prática' é uma combinação delas.

AbordagemBarreira de isolamentoMelhor quandoRessalva mais difícil
Contêineres por inquilinoNamespaces do kernel + cgroupsMuitos inquilinos pequenos, baixo risco por inquilino, necessidade de densidadeUma exploração do kernel pode quebrar todos os inquilinos naquele host
Uma VM por inquilinoVirtualização de hipervisor/hardwareDados regulamentados, inquilinos hostis, alto valor por inquilinoMais pesado, mais lento para provisionar, você aplica patches em um SO por inquilino
Contêineres dentro de VMsLimite de VM em torno de cargas de trabalho conteinerizadasDensidade mais uma casca dura entre gruposCustos e sobrecarga operacional quase dobram

Contêineres por inquilino. Este é o padrão para a maioria dos fundadores de SaaS. Cada inquilino recebe seu próprio contêiner ou pequena pilha Compose. O provisionamento é instantâneo, as imagens são pequenas, CI/CD é direto. Limites de recursos impedem que vizinhos barulhentos consumam o servidor. O trade-off é o kernel compartilhado. Se você conseguir manter as cargas de trabalho não privilegiadas e corrigir o host regularmente, essa costuma ser a primeira jogada certa.

Não coloque dois inquilinos no mesmo contêiner. Isso é um kernel compartilhado mais um runtime compartilhado mais um sistema de arquivos compartilhado. Se um inquilino enviar um arquivo que cria um processo, o outro inquilino já está na mesma tabela de processos. Um contêiner é sua unidade de isolamento; faça um inquilino por contêiner.

E o banco de dados? Se todos os inquilinos se conectam a uma instância do MongoDB ou PostgreSQL com as mesmas credenciais, você já adicionou um enorme componente compartilhado. Dê a cada inquilino credenciais separadas e, idealmente, um banco de dados ou esquema separado. Os contêineres isolam o aplicativo; o banco de dados costuma ser a primeira brecha que um atacante testará.

Uma VM por inquilino. Dê a cada inquilino uma máquina virtual completa. O hipervisor adiciona um limite de nível de hardware, que é exatamente o que uma exploração do kernel precisa cruzar para alcançar o host. Isso importa para ambientes regulamentados ou quando os inquilinos não são confiáveis. O custo é densidade e tempo. Agora você gerencia uma frota de sistemas operacionais, não apenas contêineres. Cada VM precisa de atualizações, agentes de segurança e monitoramento. Para um fundador solo, isso é trabalho de verdade.

Padrões que funcionam nesse nível: use infraestrutura como código para criar uma VM a partir da mesma imagem base, incorpore atualizações em novas imagens em vez de corrigir sistemas ativos e encerre cargas de trabalho que você não reconhece. Mantenha a porta de gerenciamento da VM fechada para a internet.

Contêineres dentro de VMs. Esse híbrido raramente é discutido em tutoriais para iniciantes. Você coloca uma VM pequena em torno de cada inquilino (ou pequeno grupo de inquilinos) e então executa contêineres dentro dessa VM. A VM é um contêiner de raio de explosão; os contêineres são apenas unidades implantáveis. Isso oferece a vantagem sólida da virtualização e a reprodutibilidade das imagens. Custa mais, porque você paga pela sobrecarga da virtualização e pela flexibilidade dos contêineres, mas pode ser o modelo mais sensato a longo prazo quando você não pode confiar totalmente nos inquilinos.

Um microexemplo comum: um inquilino executa uma API Node e um worker em segundo plano. Em vez de um contêiner enorme com os dois processos, use uma VM e depois dois contêineres com limites de recursos diferentes, uma rede compartilhada e sem exposição direta à internet para o worker. A VM fornece a borda sólida; os contêineres fornecem estrutura.

Qual você deve escolher? A tabela é sua lista de finalistas. As próximas seções tornam a decisão concreta.

Se Você Escolher Contêineres, Faça Estas Seis Coisas ou Não Se Dê ao Trabalho

Contêineres por inquilino são aceitáveis se você tratar cada contêiner como um potencial atacante. Isso começa com configuração, não com pensamento ilusório.

0. Limite os recursos antes de confiar em qualquer pessoa. Cgroups são um mecanismo de justiça e uma defesa de disponibilidade. Defina --memory e --cpus por contêiner. Um inquilino que vaza memória deve atingir seu próprio limite, não o do seu servidor. Isso não é um limite de segurança, mas um vizinho barulhento é um ataque sem uma única linha de código. Um início prático: --memory 512m --cpus 0.5. Para um processo worker, comece mais baixo e aumente.

1. Execute como um usuário não root. Nunca deixe o processo do contêiner usar UID 0, a menos que você realmente precise. Defina um usuário no Dockerfile e passe --user como uma proteção extra. Uma exploração executando como usuário não privilegiado tem muito menos caminhos para o kernel. No seu Dockerfile, crie um usuário: RUN useradd -u 10001 app e USER app. Não pule isso para economizar tempo.

2. Elimine todas as capabilities que você não precisa. As capabilities do Linux dividem o poder do root em pequenos pedaços. A maioria dos aplicativos web quase não precisa de nenhuma. Comece com --cap-drop=ALL e adicione de volta apenas o que você sabe que precisa. Um contêiner sem CAP_SYS_ADMIN é muito mais difícil de usar para truques de namespace. Se seu aplicativo tentar vincular uma porta privilegiada, execute-o em uma porta alta e coloque um proxy na frente em vez de conceder NET_BIND_SERVICE.

3. Torne o sistema de arquivos somente leitura. Seu aplicativo não deve gravar em sua própria camada de contêiner. Monte um tmpfs para estado. Um atacante que não pode gravar no disco tem muito mais dificuldade para plantar persistência. Um aplicativo PHP comprometido tentando escrever um webshell falhará quando o sistema de arquivos raiz estiver somente leitura. Você pode montar um volume nomeado para um diretório gravável que seu aplicativo realmente precise.

4. Aplique seccomp e AppArmor ou SELinux. Eles enviam chamadas de sistema arriscadas para a pilha de descarte. O Docker acompanha um perfil seccomp padrão; use-o. Adicione um perfil AppArmor para outra camada. Você não precisa dominar todas as syscalls. Você precisa negar o que um worker web normal nunca exige. Nunca execute com --privileged. Essa flag desativa quase todas as defesas que você acabou de configurar.

5. Segmente a rede. Não dê a cada contêiner uma rota para todos os outros contêineres. Negue por padrão e abra apenas as portas que você precisa. Um contêiner de banco de dados comprometido não deve ser capaz de escanear seu painel administrativo. Se os inquilinos estiverem em redes separadas, uma violação em uma rede não pode se espalhar lateralmente.

Um início prático:

docker run --user 10001 --cap-drop=ALL --security-opt no-new-privileges --read-only --tmpfs /tmp:rw,size=64M --security-opt seccomp=default.json --memory 512m --cpus 0.5 myimage

Coloque as mesmas flags em um arquivo Compose e aplique-as a todos os inquilinos. Isso não é completo, mas é um padrão muito mais forte do que o docker run oferece por padrão.

Para um passo a passo mais aprofundado, use nosso guia de endurecimento passo a passo para contêineres Docker em hospedagem multi-inquilino.

O Isolamento Aprimorado de Contêineres do Docker É a Exceção Que Você Deve Conhecer

Se você executa em um ambiente Docker gerenciado, procure o Isolamento Aprimorado de Contêineres (ECI) do Docker. Ele usa isolamento de namespace de usuário e um runtime de contêiner seguro nos bastidores. Root dentro de um contêiner é mapeado para um usuário não privilegiado no host, então até um contêiner executando como root não obtém privilégios de root do host. Ele também bloqueia capabilities e syscalls perigosas por padrão. Isso não é algo que você possa recriar com algumas flags no Docker vanilla. Se sua plataforma suportar, ative-o. Isso não remove a necessidade de usuários não root e limites de recursos, mas muda a matemática de risco.

Você pode aproximar parte disso com o remapeamento de namespace de usuário (userns-remap) no daemon do Docker. Isso não é tão completo quanto um runtime seguro, mas é melhor que nada. Se você usar, verifique se o mapeamento de UID funciona antes de confiar.

A Falácia da VM: Mover-se para Máquinas Virtuais Não É Endurecimento

Aqui está a parte contrária, e é a parte que a maioria das pessoas pula. Se você migrar para uma VM por inquilino e depois implantar seus contêineres normais dentro dela, você não removeu seu problema de segurança de contêiner. Você adicionou uma gaiola maior. A fuga do contêiner ainda funciona; o atacante apenas cai na VM em vez do host. Isso é uma melhoria real, mas você ainda precisa dos seis passos.

A outra armadilha é presumir que a VM em si é segura. Uma imagem padrão com senha SSH fraca, pacotes base sem correção ou uma porta de gerenciamento aberta é um presente. O limite do hipervisor só importa se o convidado estiver endurecido e atualizado. Caso contrário, sua 'VM segura' é um caminho mais rápido para o comprometimento, porque você se sente seguro e para de verificar.

O que uma VM oferece é um raio de explosão redutível. O desastre de um inquilino fica em uma VM. O que custa é seu tempo. Você se torna o administrador de sistemas para tantos sistemas operacionais quantos inquilinos você tiver. Se você é um fundador solo lançando um produto, pergunte-se se tem horas para corrigir e monitorar uma frota. Se sim, a VM por inquilino pode ser a decisão certa. Se não, contêineres com endurecimento forte podem ser mais honestos.

Lembre-se também de que o host do hipervisor é um alvo crítico. Um hipervisor comprometido pode ver todos os convidados. Corrija o host, não apenas os convidados. A VM não o isenta da correção do host; ela aumenta as apostas para não fazê-la.

Uma ressalva sobre o híbrido: não presuma que contêineres dentro de uma VM lhe dão 'duas camadas de segurança' de graça. A VM adiciona um limite; o contêiner ainda precisa de não-root, capabilities e seccomp. Caso contrário, a primeira camada só é tão forte quanto o contêiner mais fraco.

Quatro Perguntas Que Resolvem o Debate em Dez Minutos

Não otimize no abstrato. Faça a si mesmo estas quatro perguntas em ordem. Anote as respostas.

1. Ao que meu inquilino tem acesso? Se um inquilino só pode alcançar seu próprio aplicativo web e banco de dados, contêineres por inquilino com regras de rede estritas são defensáveis. Se os dados de um inquilino são regulamentados ou financeiramente sensíveis, vá em direção a VMs.

2. Quanto me custaria o comprometimento de um inquilino? Some clientes perdidos, exposição legal e confiança. Se o número for maior que o custo de executar VMs, gaste o dinheiro. Se não, contêineres são uma escolha racional.

3. Quantos inquilinos eu tenho e quanto eles pagam? Muitos assinantes pequenos: a densidade de contêineres importa. Um punhado de contas grandes: dê a cada uma uma VM e cobre de acordo. Inquilinos que pagam menos que um café não devem exigir cada um um SO para gerenciar.

4. Posso corrigir as coisas em um cronograma? Contêineres compartilham um kernel de host, então corrigir o host protege a todos. VMs multiplicam seus alvos de correção. Se você sabe que vai pular atualizações, escolha a arquitetura com menos partes móveis e padrões mais rígidos.

Suas respostas se agruparão. Duas ou mais respostas focadas em VM significam que você não deve estar usando contêineres por inquilino por padrão. Três ou mais respostas focadas em contêineres significam que VMs são prematuras. Um resultado contraintuitivo: um inquilino de baixa receita com acesso a dados sensíveis ainda precisa da VM, porque o custo regulatório não tem relação com quanto eles pagam.

Lance o Mínimo em Que Você Pode Confiar, Depois Ganhe Mais Isolamento

Sua primeira arquitetura não precisa ser a final. Comece com a configuração mais restrita que você pode realmente manter e depois adicione isolamento à medida que sua base de inquilinos justificar. Para a maioria dos operadores solo, isso significa contêineres por inquilino com não-root, capabilities limitadas, sistemas de arquivos somente leitura, seccomp e segmentação de rede. Para inquilinos regulamentados ou de alto valor, vá direto para uma VM por inquilino, com contêineres apenas como camada de empacotamento interna.

Seja qual for sua escolha, anote a decisão e revise-a trimestralmente. Quando você receber sua primeira pergunta 'devemos mover este inquilino para uma VM?', você terá uma resposta e terá a lista de verificação para respaldá-la. É isso que isolamento realmente significa: um trade-off que você gerencia, não uma tecnologia que você compra.

Antes do lançamento, percorra nossa lista de verificação prática de segurança de isolamento do Docker — ela transforma essas decisões em uma lista que você pode verificar antes de mostrar uma página a um cliente.

Sources (5)