Skip to main content

Docker para desenvolvedores Java: 5 coisas que você precisa saber para não comprometer sua segurança

Escrito por
docker java feature

20 de novembro de 2020

0 minutos de leitura

Docker é a forma mais usada de empacotar aplicações em contêineres. Com o Docker Hub, é fácil criar e baixar imagens prontas. Isso é muito conveniente: você pode usar essas imagens do Docker Hub para criar rapidamente uma imagem para sua aplicação Java. No entanto, criar imagens Docker personalizadas para sua aplicação Java de maneira ingênua traz diversos riscos de segurança. Então, como tornar a segurança parte essencial do uso do Docker por desenvolvedores Java?

Antes de vermos como criar uma ótima imagem Docker Java para sua aplicação, vamos conferir algumas perguntas frequentes sobre o assunto.

Como colocar aplicações Java em contêineres Docker?

Você pode executar sua aplicação Java em um contêiner Docker simplesmente copiando o arquivo .jar ou .war para uma imagem base JRE, mas há alguns pontos a considerar. Escolher os argumentos corretos da JVM e compatibilizar as configurações de execução do contêiner é apenas metade do desafio. A escolha da imagem base é essencial do ponto de vista da segurança, pois uma escolha inadequada pode introduzir vulnerabilidades.

Este artigo ajudará você a entender melhor o impacto da escolha da imagem base e a encontrar a imagem mais segura disponível para sua aplicação.

Como o Docker ajuda desenvolvedores Java?

Ao empacotar sua aplicação Java em um contêiner, você pode definir a aplicação inteira — incluindo a JRE, as configurações, as dependências do sistema operacional e os artefatos de build — em artefatos autossuficientes e implantáveis, chamados imagens de contêiner. Essas imagens são definidas em software, o que permite reproduzir sua criação com precisão e dá aos desenvolvedores uma maneira de executar a mesma plataforma em todos os ambientes. Por fim, os contêineres permitem que os desenvolvedores experimentem com mais facilidade novas versões da plataforma ou outras mudanças diretamente em seus computadores, sem precisar de permissões especiais.

Escolha a imagem base Docker certa para sua aplicação Java

Ao criar uma imagem Docker, usamos como base uma imagem baixada do Docker Hub. É o que chamamos de imagem base. Ela é a fundação da nova imagem que você vai criar para sua aplicação Java. A imagem base escolhida é essencial porque permite aproveitar tudo o que ela oferece. Mas isso tem um custo: se a imagem base tiver uma vulnerabilidade, ela também estará na imagem que você criar.

Nas imagens base, muitas vulnerabilidades estão na camada do sistema operacional (SO) usada por elas. Em nossa pesquisa de 2019, Levando a segurança do Docker para as etapas iniciais, já mostramos que as vulnerabilidades introduzidas pela camada do SO podem variar bastante conforme a distribuição escolhida.

Gráfico de barras intitulado “Vulnerabilidades em imagens de SO”, mostrando Debian 55, Debian stretch-slim 42, Ubuntu 31, CentOS 1, Fedora 0 e Alpine 0.

Relatório de 2019 — Levando a segurança do Docker para as etapas iniciais

Vamos analisar um conjunto popular de imagens base Docker Java do Adoptopenjdk, openjdk11. Com a tag padrão, essa imagem é criada sobre uma distribuição Ubuntu. No entanto, também podemos escolher tags de versões específicas baseadas, por exemplo, em Debian, CentOS ou Alpine (atenção: o Alpine não é baseado em glibc e pode não ser compatível com aplicações que fazem chamadas JNI nativas).

Gráfico de barras com vulnerabilidades por tag: Debian 75, CentOS 27, Latest (Ubuntu) 25 e Alpine 0.

Podemos concluir que escolher a imagem base certa é fundamental para a segurança. Provavelmente, você não precisa de todos os binários que vêm com um sistema operacional completo. É preferível criar a nova imagem Docker Java da sua aplicação com base em uma imagem mínima. Os binários que você não tem não podem causar danos.

Além de melhorar a segurança, uma imagem base mínima reduz o tamanho da imagem criada. Uma imagem Docker menor também ocupa menos espaço e, provavelmente, inicia mais rápido. Outra opção é usar jib, que cria uma imagem Java mínima sem exigir um Dockerfile.

Use uma JRE, não uma JDK

Ao criar uma imagem Docker, devemos incluir apenas os recursos necessários para que ela funcione corretamente. Isso significa começar com um ambiente de execução Java (JRE) adequado para sua imagem de produção, em vez de incluir o kit de desenvolvimento Java completo (JDK). Além disso, sua imagem de produção não deve incluir um sistema de build como Maven ou Gradle. O artefato gerado pelo build — por exemplo, seu arquivo jar — deve ser suficiente.

Mesmo que você queira compilar sua aplicação dentro de um contêiner Docker, é fácil separar a imagem de build da imagem de produção usando um build em várias etapas.

Por exemplo:Quero criar uma imagem Docker Java para minha aplicação java-code-workshop. Ela é baseada em Spring Boot, foi compilada com Maven e precisa da versão 8 do Java.

A maneira ingênua de criar essa imagem Docker Java seria algo assim:

Trecho de código que mostra um Dockerfile para compilar um projeto Maven com Spring Boot e OpenJDK 8.

Escolhi uma imagem base que inclui Maven e OpenJDK 8, copio meu código-fonte para a imagem e uso o Maven para compilar e executar a aplicação. Esse exemplo funciona perfeitamente. Minha aplicação inicia e roda sem problemas. No entanto, a imagem Docker que acabei de criar tem 631 MB.

Vamos alterar esse Dockerfile e usar um build em várias etapas:

Trecho de código no estilo de terminal mostrando um Dockerfile multiestágio que compila e executa uma aplicação Java com Maven e OpenJDK.

Agora continuo usando a imagem maven-openjdk8 para compilar meu projeto, mas ela não será a imagem final. Crio uma nova imagem baseada em uma imagem JRE Java 8 bem menor e copio apenas o jar executável do Spring Boot. Agora basta executar o jar-file e pronto! O resultado é uma imagem Docker que não inclui JDK nem Maven, apenas a JRE. O tamanho da imagem cai drasticamente para 132 MB.

Imagens menores não são apenas mais fáceis de enviar e mais rápidas para iniciar: também são muito mais seguras. Imagine o que poderia acontecer se, por algum motivo, um invasor conseguisse acesso a um contêiner em execução que tivesse a JDK, seu código-fonte e uma ferramenta de build disponíveis.

Você também pode usar essa técnica quando precisar incluir segredos para acessar um repositório privado. Você não quer esse tipo de segredo no cache da imagem de produção. Como a imagem de build não é usada em produção, é perfeitamente aceitável usar os segredos nela. Com essa técnica, você pode selecionar o que precisa de outras imagens e criar uma imagem Docker de produção que contenha apenas os recursos necessários.

Não execute seu contêiner Docker como root

Por padrão, um contêiner Docker é executado como root. Embora isso seja conveniente durante o desenvolvimento, você não deve fazer o mesmo nas imagens de produção. Suponha que, por algum motivo, um invasor consiga acesso a um terminal ou executar código. Nesse caso, ele terá privilégios significativos no contêiner em execução e poderá até acessar sistemas de arquivos do host por meio de montagens bind com permissões de acesso excessivamente altas.

A maneira mais fácil de evitar isso é criar um usuário específico, como neste exemplo:

Janela do terminal exibindo um Dockerfile que cria um diretório para o app, adiciona um usuário brianvermeer, copia arquivos e executa o app como esse usuário

Na terceira linha, crio um novo grupo e adiciono um usuário. Esse usuário é um usuário do sistema (-r), sem senha nem diretório pessoal. Também o adiciono ao grupo recém-criado.

Em seguida, dou ao usuário permissão para acessar a pasta da aplicação na linha 6. Não se esqueça da linha 7: nela, defino o usuário que quero usar. Assim, o comando da última linha é executado pelo novo usuário com privilégios restritos.

Analise sua imagem Docker e sua aplicação Java durante o desenvolvimento

Criar uma imagem Docker a partir de um Dockerfile — e até mesmo reconstruí-la — pode introduzir novas vulnerabilidades no seu sistema. A análise das imagens Docker durante o desenvolvimento deve fazer parte do seu fluxo de trabalho para detectar vulnerabilidades o quanto antes.

Você pode analisar sua imagem Docker facilmente com a Snyk CLI. Use-a na sua máquina local, como parte do pipeline ou das duas formas. Depois de instalar e autenticar a Snyk CLI, basta executar este comando para analisar uma imagem:

$ snyk container test <imageName>

Se eu quiser analisar uma imagem adoptopenjdk, como mencionei na primeira seção, os comandos serão estes:

$ docker pull adoptopenjdk/opendjdk11:latest
$ snyk container test adoptopenjdk/opendjdk11:latest

Saída:

Terminal mostrando uma análise da Snyk de uma imagem Docker do OpenJDK, com vulnerabilidades de gravidade média e 21 problemas encontrados entre 132 dependências.

Você pode testar e monitorar a imagem Docker. Para monitorá-la, use snyk container monitor <image>. O monitoramento captura um instantâneo e verifica ao longo do tempo se surgem novas vulnerabilidades ou correções para sua imagem.

Ao analisar uma imagem e ter o Dockerfile (ou seja, ao criar uma nova imagem Docker Java), adicione a flag --Dockerfile=<dockerfile> a snyk container test ou snyk container monitor. Assim, você recebe recomendações de correção mais precisas. Por exemplo, você saberá se existe uma imagem base disponível que reduz o número de vulnerabilidades.

Exemplo:

$ snyk container test myImage:mytag --Dockerfile=path/Dockerfile
$ snyk container monitor myImage:mytag --Dockerfile=path/Dockerfile

Analise sua aplicação Java

A imagem Docker Java que você está criando também contém sua aplicação. É claro que ela também pode ser alvo de ataques. Você precisa garantir que sua aplicação Java não tenha vulnerabilidades de segurança para que o uso do Docker por desenvolvedores Java seja seguro desde o início. Imagine que sua aplicação contenha uma biblioteca que permita a execução remota de código ao chamar um endpoint REST. Mesmo que o restante da imagem não tenha vulnerabilidades, isso pode ser desastroso.

A maior parte do código binário Java incluído na imagem Docker provavelmente é código importado. Pense nas bibliotecas e estruturas que sua aplicação usa como dependências. É fácil verificar as dependências com a Snyk CLI. É a mesma CLI que usamos antes para analisar a imagem. Execute snyk test ou snyk monitor na pasta raiz para analisar ou monitorar sua aplicação em busca de vulnerabilidades de segurança nas bibliotecas.

Para o código que você escreveu, é recomendável usar uma ferramenta de análise de código ou um linter, como SonarLint, PMD ou SpotBugs. Essas ferramentas têm uso geral para ajudar a criar código melhor, mas também ajudam a evitar erros de segurança evidentes.

Compile para poder reconstruir

Compile sua aplicação Java para a imagem Docker de modo que você possa descartá-la e reconstruí-la a qualquer momento. Digamos que você perceba que há algo errado com seu contêiner em execução. Seria ótimo poder simplesmente encerrá-lo e iniciar uma nova instância. Para isso, você precisa criar aplicações Java sem estado, armazenando os dados fora do contêiner. Algumas opções que você pode considerar:

  • não execute um banco de dados ou outro armazenamento de dados no contêiner.

  • não armazene arquivos (de log) no contêiner.

  • garanta que o cache se recupere automaticamente, se aplicável.

Se você compilar sua aplicação para poder descartá-la e iniciar uma nova instância a qualquer momento, também poderá reconstruir com segurança toda a imagem Docker. Sabia que, em 20% das imagens Docker vulneráveis, é possível corrigir um ou mais problemas de segurança simplesmente reconstruindo a imagem? Em muitos casos, as imagens Docker usam a tag “latest” de uma imagem base. Essas versões “latest” mudam com o tempo e são substituídas por versões mais novas e aprimoradas. O mesmo vale para binários importantes instalados no contêiner por gerenciadores de pacotes como apt ou yum. Do ponto de vista da segurança, usar a versão mais recente é bom, pois você recebe automaticamente as correções de segurança mais recentes. No entanto, é preciso considerar que sua imagem base mudará com o tempo, dificultando a recriação da imagem a partir de um instantâneo de um momento específico.

Mesmo que sua aplicação não tenha mudado, reconstrua a imagem Docker regularmente, talvez usando uma tag de versão mais nova ou a mais recente para a imagem base. Melhorias nas camadas subjacentes, como a do SO, podem elevar a qualidade da imagem e reduzir as vulnerabilidades de segurança.

https://www.youtube.com/watch?v=v2SkWn-ZRDg

Para concluir, se você quer acompanhar as práticas recomendadas de segurança para criar imagens Docker ideais, em geral ou para aplicações Java:

  1. 10 práticas recomendadas para criar um contêiner Java com Docker — um guia detalhado, passo a passo, que mostra como criar imagens Docker seguras e com bom desempenho para suas aplicações Java

  2. 10 práticas recomendadas de segurança para Java — práticas de segurança que você deve seguir ao criar aplicações Java para qualquer ambiente.

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.