Quem tem medo de vulnerabilidades de container?
Imagine que você recebeu um relatório de vulnerabilidades da sua aplicação, e que grande parte delas, estão relacionadas ao seu container que executa num determinado cluster Kubernetes. Imagine também que existe uma política de segurança recém implementada na empresa que irá “quebrar” a pipeline que não estiver em dia com todos os requisitos exigidos nessa política. Bem, isso impacta diretamente na entrega do produto e consequentemente no negócio.
Então, como corrigir vulnerabilidades de containers?
Não existe uma fórmula mágica e nem bala de prata para essa pergunta, porém, o que existe são algumas técnicas e melhores práticas que podem ser seguidas para evitar que os nossos containers fiquem vulneráveis e consequentemente afetem a segurança do nosso cluster.
Por onde começar?
Vamos ver abaixo alguns ajustes e melhores práticas que podemos seguir:
#1 Analise o relatório de vulnerabilidades
Parece óbvio, e é. A primeira coisa que devemos fazer, é ter noção das vulnerabilidades que temos que tratar. Vejamos o seguinte relatório abaixo:
293 VULNERABILIDADES.
É realmente um número assustador e desanimador na maioria dos casos. Mas vamos ver abaixo como podemos começar a mexer nesse “vespeiro”.
#2 Procure por baselines
Existem alguns baselines específicos para construção de imagens de containers. Vale a pena conferir as melhores práticas recomendadas, isso pode ser um bom ponto de partida para que você consiga corrigir as vulnerabilidades da sua imagem.
Abaixo alguns baselines específicos de segurança:
- https://dev-sec.io/baselines/docker/
- https://docs.sysdig.com/en/dockerfile-gate-and-triggers-deep-dive.html
- https://www.aquasec.com/cloud-native-academy/docker-container/docker-cis-benchmark/
#3 Ajuste seu Dockerfile seguindo a recomendação dos baselines
O relatório traz vulnerabilidades específicas da imagem do nosso container. Faz todo o sentido voltar o nosso olhar para o Dockerfile, visto que esse é o arquivo declarativo da imagem que “da vida” ao nosso container.
Vamos observar nosso arquivo atual:
# Dockerfile app covidtrackerFROM node:10
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 4000
CMD ["node", "server.js"]
É um Dockerfile simples, porém uma série de vulnerabilidades foram apontadas para esse arquivo. Vamos ver abaixo o que podemos alterar para diminuir a nossa quantidade de vulnerabilidades.
#3 Utilize muilt-stage building
É uma prática recomendada manter as imagens mínimas. Devemos evitar incluir pacotes desnecessários, reduzindo a nossa superfície de ataque.
Quando usamos multi-stage Building criamos um container intermediário com todos os componentes necessários para compilar ou gerar os artefatos para a imagem final. Uma vez que temos esses artefatos, uma nova imagem, muito reduzida de componentes, é utilizada para executar somente o necessário do nosso container.
Isso é uma prática segura e também reduz o tamanho da imagem. Então vamos modificar o nosso Dockerfile:
# Dockerfile app covidtracker#satege1 build
FROM node:lts-alpine AS buildapp
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .#stage 2 run
FROM node:lts-alpine
EXPOSE 4000
WORKDIR /usr/src/app
COPY --from=buildapp /usr/src/app .
CMD ["node", "server.js"]
Visando otimizar ainda mais a nossa imagem, iremos utilizar uma imagem base do tipo Alpine. As imagens Alpine são bem mais leves quando comparadas as imagens base padrão e possuem muito menos componentes o que vai de encontro a nossa abordagem.
# Dockerfile app covidtracker#satege1 build
FROM node:lts-alpine AS buildapp
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .#stage 2 run
FROM node:lts-alpine
EXPOSE 4000
WORKDIR /usr/src/app
COPY --from=buildapp /usr/src/app .
CMD ["node", "server.js"]
#4 Use imagens base de fontes confiáveis
Sempre prefira utilizar imagens verificadas e de repositórios confiáveis ao invés de imagens criadas por usuários desconhecidos, por mais performáticas que elas pareçam ser. Na dúvida faça você mesmo a sua imagem.
#5 Crie um usuário para o seu container
Quando não definimos um usuário no nosso Dockerfile, o nosso container é executado por default como usuário ROOT. Não queremos um container vulnerável e com poderes de ROOT no nosso cluster. Essa combinação pode ser catastrófica!
Então, vamos definir um usuário para o nosso container.
# Dockerfile app covidtracker#satege1 build
FROM node:lts-alpine AS buildapp
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .#stage 2 run
FROM node:lts-alpine
EXPOSE 4000
WORKDIR /usr/src/app
COPY --from=buildapp /usr/src/app .
USER node
CMD ["node", "server.js"]
#6 Defina instruções de HEALTHCHECK
Como o nosso container recebe tráfego na porta 4000, nossa verificação de integridade deve ter certeza de que isso está acontecendo.
Uma verificação de integridade é configurada no Dockerfile usando a instrução HEALTHCHECK.
A utilização da instrução além de garantir a integridade do nosso container, evita falsos positivos com relação ao status do container.
Vamos adicionar a instrução do HEALTHCHECK no nosso Dockerfile:
#Dockerfile app covidtracker#satege1 build
FROM node:lts-alpine AS buildapp
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .#stage 2 run
FROM node:lts-alpine
EXPOSE 4000
WORKDIR /usr/src/app
COPY --from=buildapp /usr/src/app .
USER node
HEALTHCHECK CMD curl --fail http://localhost:4000/ || exit 1
CMD ["node", "server.js"]
Lembrando que a instrução de healthcheck é muito particular de cada aplicação, você deve personalizar de acordo com as características do seu workload.
#7 Utilize Lint no seu Dockerfile
Existem algumas ferramentas que permitem que façamos lint no nosso Dockerfile com o intuito de nos ajudar a construir uma imagem que segue as melhores práticas.
Dentre essas ferramentas podemos destacar a Haskell Dockerfile Linter, ou simplesmente Hadolint. Vou deixar aqui o link do repositório da solução no github:
#8 Utilize o arquivo oculto .dockerignore
A ideia com .dockerignore é muito simples, um arquivo onde podemos indicar artefatos, pastas, arquivos, extensões, dentre outros que não devem fazer parte da construção da nossa imagem.
#9 Valide sua imagem localmente
Ter condições de validar a existência de vulnerabilidades e/ou configurações incorretas na construção do nosso Dockerfile antes de enviá-lo para o pipeline dá a condição de sermos muito mais assertivos na correção de vulnerabilidades, pois não precisamos aguardar até que a política de segurança quebre a nossa pipeline.
Existe uma ferramenta chamada Dockle que nos permite rodar localmente e fazer uma espécie de benchmark de segurança no nosso Dockerfile.
Vamos ver abaixo a saída dessa solução na versão da imagem que falha com a nossa política de segurança:
Algumas das recomendações dessa ferramenta batem com as recomendações desse post inclusive.
Sério, sempre que possível procure validar localmente a sua imagem, isso poupa muito tempo na correção.
Vamos ver como fica essa análise na nossa nova imagem com as alterações de segurança que fizemos:
Essa foto já esta bem melhor quando comparada a nossa análise anterior. Aqui temos somente alerta informacional, algo que deve ser corrigido claro, mas que normalmente não requer a mesma urgência que outros alertas como container executando como ROOT.
Neste link você pode encontrar mais detalhes sobre o Dockle.
Ok, vamos ver se melhorou?
Depois que procuramos por baselines de segurança para imagens de container, alteramos o nosso Dockerfile seguindo as melhores práticas e validamos localmente nossa imagem vamos finalmente comitar as nossas alterações para o nosso repositório e rodar nossa pipeline.
Vamos ver como ficou nosso Dockerfile:
#Dockerfile app covidtracker#satege1 build
FROM node:lts-alpine AS buildapp
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .#stage 2 run
FROM node:lts-alpine
EXPOSE 4000
WORKDIR /usr/src/app
COPY --from=buildapp /usr/src/app .
USER node
HEALTHCHECK CMD curl --fail http://localhost:4000/ || exit 1
CMD ["node", "server.js"]
Finalmente o resultado da nossa pipeline:
Bem, não é o que a gente esperava. Vamos dar uma olhada no novo relatório de segurança, para ver se avançamos em alguma correção:
Se levarmos em consideração que começamos o jogo com 293 vulnerabilidades, tivemos um gigantesco avanço, porém ainda estamos falhando na nossa política de segurança. Por quê?
#10 Sempre atualize as suas imagens
A última dica desse post é: sempre atualize a sua imagem, ou seja, seu Dockerfile.
Precisamos garantir que os componentes que estamos utilizando estão na sua versão mais estável. Muita atenção a dica aqui não é simplesmente para usar a tag latest, está longe disso. Navegue no repositório oficial da imagem base que está usando na guia de tags e veja quais são as mais estáveis para uso.
Vamos corrigir isso:
Alterando o nosso Dockerfile:
#Dockerfile app covidtracker#satege1 build
FROM node:lts-alpine AS buildapp
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .#stage 2 run
FROM node:lts-alpine3.13
EXPOSE 4000
WORKDIR /usr/src/app
COPY --from=buildapp /usr/src/app .
USER node
HEALTHCHECK CMD curl --fail http://localhost:4000/ || exit 1
CMD ["node", "server.js"]
Vamos comitar a nossa alteração e verificar o resultado da nossa pipeline:
Vamos ver como ficou o relatório de segurança agora:
Além de passar na política de segurança, garantimos que o nosso container está seguro e em Compliance de acordo com a política de segurança.
É importante lembrar que isso não é uma receita de bolo, e sim algumas técnicas que podemos adotar para garantir a entrega segura de containers e consequentemente de aplicações.
E o mais importante desmistificar a correção de vulnerabilidades, vimos que com poucos ajustes podemos entregar um produto seguro e com qualidade.
Recomendação:
Mais recentemente o João Freire, também conhecido na comunidade como p0ssuidao, fez uma palestra no DevOps Extreme onde ele mostra o impacto que um container vulnerável pode causar num cluster K8S.
A palestra está sensacional e você pode conferir o conteúdo completo neste link.
Por esse post é isso pessoal. Críticas e/ou sugestões são sempre bem-vindas. Abraço!