Boas práticas para isolamento de contêineres
Maryann Agofure
29 de agosto de 2022
0 minutos de leituraContêineres são um formato padronizado de empacotamento de software que oferece uma maneira previsível e replicável de executar aplicações. O isolamento é um dos principais benefícios das aplicações conteinerizadas. Com contêineres, podemos isolar nosso software do ambiente em que ele é executado, aumentando a consistência e a confiabilidade nos ambientes de desenvolvimento e preparação.
Você provavelmente conhece — ou usa — os contêineres Docker. Eles alcançam o isolamento aproveitando recursos do Linux, como grupos de controle (geralmente chamados de cgroups), filtros do modo de computação segura (seccomp) e namespaces do kernel. Também existem outros tipos de contêineres, incluindo contêineres em sandbox, como o gVisor, e contêineres virtualizados, como o AWS Firecracker.
Embora os contêineres sejam isolados por definição, ainda precisamos adotar boas práticas para garantir que esse isolamento seja eficaz e mantê-los seguros. Este artigo explora as implicações do isolamento de contêineres e apresenta boas práticas de segurança e metodologia para contêineres Linux, em sandbox e virtualizados.
O que é isolamento de contêineres e como funciona?
Como o nome indica, o isolamento de contêineres consiste em separar o ambiente de execução de uma aplicação conteinerizada do sistema operacional host e dos outros processos executados nele.
Esse isolamento pode ocorrer de várias formas, incluindo isolamento do sistema de arquivos, da rede, das chamadas de sistema e do uso de recursos, como CPU e memória.
Os detalhes técnicos de como funciona o isolamento dependem do tipo de contêiner usado. Como veremos, há várias opções.
Diferentes abordagens para o isolamento de contêineres
Como mencionamos, este artigo explora o isolamento de contêineres e sua relação com três tipos diferentes: contêineres Linux, como o Docker; contêineres em sandbox, como o gVisor; e virtualização leve baseada em KVM, como o AWS Firecracker.
Cada tipo de contêiner aborda o isolamento de uma maneira diferente e isola partes distintas do sistema. Cada um também tem boas práticas específicas a serem seguidas para isolar os contêineres.
Vamos conhecer as boas práticas para isolar cada tipo de contêiner.
Contêineres Linux
Contêineres Linux, como os do Docker, usam cgroups, filtros seccomp e namespaces do kernel para isolar contêineres. Os cgroups permitem definir limites de uso de recursos para um grupo de processos. Por exemplo, eles podem limitar o uso de vários recursos, como E/S de disco, memória, rede, tempo de CPU e até CPUs individuais em um sistema multinúcleo. O seccomp permite filtrar todas as chamadas de sistema feitas por um processo. Essa filtragem funciona em conjunto com a técnica de namespaces do kernel, permitindo que as funções dentro de um contêiner tenham uma visão isolada do sistema.
Essa abordagem é a mais simples, mas também significa que o Docker precisa garantir que todas as aplicações conteinerizadas configurem corretamente os sinalizadores de namespace durante a criação.
A abordagem do Docker é relativamente simples e fácil de usar. No entanto, essa simplicidade significa que seu isolamento não é tão robusto quanto o de alternativas como o gVisor. O Docker precisa permitir algumas chamadas de sistema através do limite do namespace. Isso significa que uma aplicação ainda pode acessar informações no host se houver um argumento válido para determinada chamada no filtro seccomp.
Felizmente, há algumas boas práticas que podemos seguir para aproveitar melhor o isolamento de contêineres no Docker:
Use um usuário dedicado para cada contêiner, especificando uma conta de usuário no Dockerfile. Isso garante que um contêiner não possa acessar nem modificar os recursos de outro, como arquivos e diretórios. Ao criar imagens Docker e configurações runc, use usuários com o mínimo de privilégios possível.
Limite os recursos dos contêineres usando os recursos do Linux mencionados anteriormente. Siga o princípio do menor privilégio: conceda ao contêiner somente o necessário para realizar suas tarefas. Assim, você limita o que uma aplicação dentro do contêiner pode ver ou fazer fora do ambiente isolado.
Bloqueie a configuração de dispositivos de rede desnecessários em cada contêiner. Isso limita o que um contêiner pode ver e fazer na infraestrutura de rede do host.
Use restrições de cgroups para limitar os recursos disponíveis para cada contêiner, como cotas de CPU, páginas de memória, largura de banda de E/S de bloco e outros.
Configure parâmetros do kernel, como o número de PIDs, o tamanho máximo da pilha e o número máximo de threads que um contêiner pode criar. Isso ajuda a impedir que um contêiner assuma o namespace de PIDs de outro ou cause uma falha no kernel do host ao disparar um panic.
Em conjunto, essas práticas reduzem a superfície de ataque dos contêineres, limitando as possibilidades de exploração por um invasor. É claro que a melhor defesa é nunca executar código não confiável nos contêineres. No entanto, isso exige saber exatamente o que está sendo executado em cada contêiner, algo que raramente é viável para equipes de desenvolvimento e DevOps com prazos apertados.
Se soubermos que precisamos executar um contêiner não confiável, a próxima boa prática a considerar é usar um runtime de contêiner com um modelo de segurança mais robusto.
Contêineres em sandbox
Contêineres em sandbox oferecem os mesmos mecanismos de isolamento que os contêineres Linux tradicionais, com camadas extras de proteção.
O gVisor, uma excelente opção de contêiner em sandbox, implementa um minikernel personalizado no espaço de usuário, entre as aplicações conteinerizadas e o kernel do host. Ele intercepta todas as chamadas de sistema do contêiner e verifica cada uma de acordo com uma política antes de encaminhá-la ao kernel do host. O gVisor também implementa uma pilha TCP/IP personalizada para controlar melhor como as cargas de trabalho conteinerizadas interagem com a rede. Além disso, implementa um proxy de sistema de arquivos entre o contêiner e o sistema de arquivos do host. Essas verificações em camadas permitem que o gVisor reduza a superfície de ataque do contêiner, impondo limites rígidos e mantendo a compatibilidade com a maioria das aplicações. Como o gVisor é escrito em Go, uma linguagem segura para memória, ele tem muito menos chances de sofrer com estouros de buffer e outras explorações do que aplicações escritas em C, como o kernel do Linux.
No entanto, a abordagem do gVisor tem algumas desvantagens. Uma delas é que pode ser difícil depurar aplicações executadas dentro dele, já que não podemos usar a maioria das ferramentas criadas para o kernel do Linux. Outra desvantagem é que, como o gVisor não usa um kernel Linux padrão, talvez seja necessário reimplementar recursos para oferecer suporte a algumas cargas de trabalho executadas nele.
Embora o gVisor e outros contêineres em sandbox sejam uma evolução em relação aos contêineres Linux tradicionais, talvez a melhor prática para o seu caso seja adotar um modelo de contêiner com isolamento ainda mais robusto.
Máquinas virtuais leves
Ao contrário dos contêineres Linux e dos contêineres em sandbox, os contêineres baseados em máquinas virtuais (VMs) leves, como o AWS Firecracker, adotam uma abordagem totalmente diferente. Eles usam um hipervisor, como o KVM ou o qemu, para criar VMs leves (geralmente chamadas de microVMs) para cada contêiner. Esse isolamento robusto reduz ao mínimo a superfície de ataque dentro das microVMs convidadas e facilita seu controle.
Essa técnica dificulta bastante o acesso de um invasor a informações privilegiadas no host, mas também oferece menos opções para que um contêiner interaja com ele. Por exemplo, contêineres executados em microVMs não podem compartilhar arquivos diretamente com o host.
As microVMs têm uma superfície de ataque menor que a dos contêineres executados em VMs tradicionais, pois precisam oferecer suporte apenas a um subconjunto dos dispositivos de hardware compatíveis com os kernels Linux convencionais.
Em geral, as microVMs são a melhor opção quando precisamos de um isolamento muito robusto entre contêineres, como ao executar código não confiável de vários locatários no mesmo servidor.
Como abordar o isolamento de contêineres
Independentemente do tipo de contêiner escolhido, não existe uma solução única para o isolamento. Por isso, precisamos equilibrar os fatores a seguir de acordo com os requisitos específicos de cada projeto.
Desempenho. Os contêineres Linux oferecem o melhor desempenho porque têm menos camadas de abstração entre o contêiner e o sistema operacional host. Os contêineres em sandbox são comprovadamente mais lentos devido às camadas adicionais entre o contêiner e o host. As microVMs têm o maior impacto no desempenho, pois precisam fornecer uma interface de hardware virtualizada para cada contêiner.
Segurança. Quanto mais recursos de isolamento adicionamos aos contêineres, mais seguros eles ficam. Como vimos, os contêineres em sandbox são mais seguros que os Linux, e os contêineres baseados em microVMs são mais seguros que ambos.
Complexidade e tempo de desenvolvimento. Os contêineres Linux são relativamente simples e representam a maioria dos contêineres em produção. Há muitas ferramentas e documentações excelentes disponíveis, o que reduz o tempo de desenvolvimento e a complexidade da implantação de aplicações conteinerizadas. Contêineres em sandbox e microVMs são bem menos comuns, e muitas ferramentas conhecidas não oferecem suporte a eles. Por serem menos populares, há menos ferramentas e plataformas compatíveis, o que aumenta o tempo de desenvolvimento e a complexidade: as equipes de desenvolvimento e DevOps precisam fazer mais tarefas manualmente, em vez de contar com ferramentas.
No fim das contas, precisamos equilibrar desempenho, segurança e complexidade. Priorizar o desempenho, por exemplo, pode exigir abrir mão de parte da segurança. Por outro lado, priorizar a segurança pode significar sacrificar o desempenho e lidar com um tempo de desenvolvimento maior.
Segurança de contêineres
Como desenvolvedores e profissionais de DevOps, precisamos saber como nossos contêineres são isolados das máquinas host para entender os limites desse isolamento. Os contêineres são ótimos para acelerar os ciclos de desenvolvimento, mas é importante adotar boas práticas para manter seguros tanto os contêineres quanto as aplicações.
Todas as equipes de desenvolvimento e DevOps devem adotar uma estratégia de defesa em profundidade. O isolamento de contêineres é apenas uma parte desse conjunto. Você pode aumentar a segurança dos seus sistemas usando um scanner de código para garantir que seu código não tenha vulnerabilidades e um scanner de contêineres para manter o conteúdo deles seguro. Afinal, o isolamento de contêineres deve ser uma das linhas de defesa — não a única.
Segurança de contêineres que prioriza os desenvolvedores
O Snyk encontra e corrige automaticamente vulnerabilidades em imagens de contêiner e workloads do Kubernetes.
