O Docker é ferramenta open-source que permite a segregação de processos linux em instâncias independentes e isoladas denominadas contêineres. Tais contêineres são criados pela engine do Docker por meio de chamadas aos recursos do kernel linux de namespaces e cgroups, provendo isolamento e recursos a esses processos que acreditam estar executando de forma independente.
Tendo em vista a praticidade que o Docker proporciona em termos de reprodução de ambientes de produção ou até mesmo de desenvolvimento, tal tecnologia tem se mostrado uma forte aliada dos engenheiros de sistemas na implantação e compartilhamento de aplicações.
Todavia, sendo o compartilhamento de aplicações uma das principais virtudes do Docker, manter a segurança do ambiente de contêineres também torna-se preocupação constante.
Neste artigo, exploraremos algumas medidas que irão melhorar a segurança dos contêineres Docker visando garantir a integridade e confiabilidade dos mesmos.
Utilização de imagens mínimas
Em via de regra, quanto mais robusta e carregada for uma imagem de container, maiores são as chances de estar presente nela alguma biblioteca ou pacote com vulnerabilidade. Nesse sentido, é considerado uma boa prática optar por imagens mínimas e instalar somente o necessário para executar a aplicação internamente no container, reduzindo assim o raio de ação de atacantes através do mesmo.
Para reduzir ao máximo o tamanho da imagem gerada em build, é possível fazer a junção de diversas técnicas. A primeira delas é utilizar o multi stage build. Por meio dessa técnica, diversos estágios de build são processados de forma encadeada tendo como resultante uma imagem mínima e otimizada para a aplicação alvo. O processo de multi stage pode ser definido por meio de tags no Dockerfile juntamente com declarações do tipo FROM, permitindo definir o escopo específico da stage, seja ele baixar dependências, buildar código ou apenas executar um artefato resultante da build. No exemplo de Dockerfile abaixo, é realizado uma multi stage build de uma aplicação python. Nota-se a criação de três estágios de build que resultam em três micro imagens ao final do processo. Staging, estágio que recebe o código da aplicação. Build, estágio que parte da Staging e é onde será compilado o código. Production, estágio que recebe o artefato resultante do estágio de Build e é a micro imagem responsável pela execução da aplicação de fato.
No Dockerfile exemplo, ainda é possível observar que os estágios partem de uma imagem python:3.11-slim. O slim dessa imagem também indica que ela é enxuta em termos de pacotes e bibliotecas. Quando o foco for segurança, sempre dê preferências por estas imagens.
Outro recurso importante a ser utilizado é a iniciativa DockerSlim. DockerSlim é uma ferramenta que tem por objetivo reduzir ainda mais o tamanho dos contêineres em execução, otimizando ainda mais as camadas resultantes das builds de Dockerfiles. Com essa ferramenta é possível analisar Dockerfiles, diminuir o tamanho de imagens e realizar testes que permitem observar o desempenho de contêineres otimizados. Todo esse processo pode convergir em imagens até 30 vezes mais compactas. Mais informações podem ser encontradas no repositório do projeto no Github (https://github.com/slimtoolkit/slim).
Evitar utilização de contêineres com privilégios
O Docker permite que o usuário execute contêineres em modo privilegiado. Em suma, tal procedimento não é recomendado pois permite que o usuário dentro do contêiner tenha acesso à recursos do host. Deste modo, um contêiner comprometido pode permitir que atacante consiga acesso a partes do sistema hospedeiro que não deveria como dispositivos de disco, por exemplo.
A opção de executar contêineres em modo privilegiado é desabilitado por padrão, e você pode verificar se determinado contêiner executa em modo privilegiado ou não com o seguinte trecho de código:
Se o retorno for “false”, logo seu contêiner executa em modo não privilegiado.
Evitar utilizar usuário de root
Uma das principais vantagens da utilização dos contêineres Docker frente a utilização de máquinas virtuais convencionais é que contêineres usufruem partes do kernel do sistema linux hospedeiro. Isso torna os contêineres mais leves e permitem uma performance melhor se comparado com máquinas virtuais que necessitam de hipervisores como camada adicional para virtualização.
O fato de compartilhar rotinas do kernel linux hospedeiro para execução traz um risco inerente a contêineres com aplicações que podem ser exploradas. Isto ocorre pois um atacante que conseguiu acesso ao contêiner pode tentar explorar vulnerabilidades do próprio kernel, escalando privilégio de dentro para fora do ecossistema Docker, conseguindo acesso ao sistema operacional hospedeiro.
Para evitar que isso ocorra, é possível declarar em Dockerfile o usuário padrão não root de execução do container. Desta maneira reduz-se as capacidades do usuário dentro do contêiner. Abaixo um trecho de código onde o usuário não root de execução é declarado explicitamente.
Outra medida importante é utilizar a flag –security-opt no-new-privileges. Esta flag tem por objetivo prevenir que o usuário definido consiga escalar para root, mantendo assim o contêiner mais seguro. Um exemplo de execução de container com esta flag pode ser observado a seguir.
Limite as capacidades dos contêineres
Por padrão, contêineres são inicializados com diversas capacidades que em teoria só deveriam ser acessados por usuários autorizados, i.e., com permissões de root. Considerando isso, também recomenda-se limitar as capacidades dos contêineres no momento em que os mesmos são inicializados.
Para limitar as capacidades do contêiner é possível utilizar a flag –cap-drop ALL. O trecho de código a seguir exemplifica a utilização da flag:
Utilize a Docker Content Trust
Uma medida crucial na hora de criar imagens customizadas para se utilizar em contêineres é sempre preferir partir de imagens garantidamente autenticadas por alguma entidade. Usando imagens autênticas, mitiga-se a possibilidade de haver software malicioso já instalado previamente na imagem base.
Para sempre dar preferência a imagens autênticas, é possível configurar uma variável de ambiente que verifica a precedência da imagem antes de realizar o pull junto a Docker Content Trust (DCT). A DCT é uma entidade validadora que garante por meio de assinaturas digitais que uma imagem está segura.
Para habilitar essa opção, basta executar o seguinte trecho de código:
Com essa variável de ambiente definida em 1, a engine do Docker irá sempre verificar a precedência de uma nova imagem antes de efetuar um pull.
Dica bônus: Evite armazenar senhas e variáveis de ambiente no contêiner
É muito comum que variáveis de ambiente armazenem informações sensíveis à aplicação e que não deveriam ser facilmente acessadas. Sendo assim, evitar enviar essas variáveis para dentro dos contêineres no momento de build acaba se tornando uma boa prática muito útil para aumentar a dificuldade de acesso à essas informações.
Uma opção viável é utilizar o Docker Secrets para armazenar senhas, variáveis de ambiente e informações sensíveis. Este recurso interno do Docker permite armazenar informações em memória e estas se perdem quando o contêiner for paralisado.
Conclusão
A versatilidade proporcionada pelos contêineres Docker têm instigado uma adoção em massa à essa tecnologia tanto no ambiente acadêmico quanto no ambiente empresarial. Muito embora essa adoção seja benéfica na grande maioria dos casos, o paradigma de utilização de aplicações em contêineres traz consigo diversos novos desafios relacionados à segurança dos contêineres. Um dos principais desafios trata de garantir que a aplicação encapsulada esteja devidamente protegida e, em casos onde a mesma demonstra-se comprometida, impossibilitar que atacantes consigam acesso ao sistema operacional hospedeiro.
Neste artigo elencamos cinco medidas que devem ser adotadas por engenheiros de sistemas, profissionais de DevOps e analistas de segurança na hora de colocar contêineres com aplicações em produção. Uma vez que tais medidas são colocadas em prática, consegue-se assegurar com mais propriedade a confiabilidade e integridade dos contêineres independente da aplicação.