Segurança de containers e imagens
LidoEsta aula aborda práticas essenciais para assegurar containers e imagens em ambientes DevOps, incluindo o uso de imagens mínimas, execução como usuário não-root, varredura de vulnerabilidades (scanning) e políticas de segurança. O conteúdo inclui exemplos práticos, boas práticas e exercícios para fixação.
A segurança de containers é um pilar fundamental em qualquer pipeline DevOps moderno. À medida que as organizações adotam arquiteturas baseadas em microsserviços e orquestradores como Kubernetes, a superfície de ataque cresce, tornando imperativo adotar medidas que minimizem riscos desde a construção da imagem até a execução do container. Nesta aula, vamos explorar quatro pilares críticos: a escolha de imagens mínimas, a execução com usuários não-root, a varredura de vulnerabilidades (scanning) e a implementação de políticas de segurança.
Nosso foco é fornecer um guia prático e aprofundado, com exemplos que você pode aplicar imediatamente em seus projetos. Ao final, você terá uma compreensão sólida de como fortalecer a segurança dos seus containers, reduzindo a probabilidade de exploração e garantindo conformidade com as melhores práticas da indústria.
Imagens mínimas
Imagens mínimas são aquelas que contêm apenas o necessário para executar a aplicação, sem pacotes extras, shells desnecessários ou ferramentas de depuração. Essa abordagem reduz a superfície de ataque, pois há menos componentes que podem conter vulnerabilidades. Além disso, imagens menores são mais rápidas de transferir e iniciar, otimizando o uso de banda e armazenamento.
Uma imagem base comum é o Alpine Linux, que possui cerca de 5 MB, enquanto distribuições completas como Ubuntu podem passar de 50 MB. No entanto, é importante verificar a compatibilidade da aplicação com a libc do Alpine (musl) versus a glibc tradicional. Alternativas como distroless (do Google) fornecem imagens ainda mais enxutas, sem gerenciador de pacotes ou shell, ideais para produção.
Para criar uma imagem mínima, você pode usar multi-stage builds, onde o estágio de build contém as ferramentas necessárias para compilar a aplicação, e o estágio final copia apenas o artefato compilado para uma imagem base enxuta. Veja um exemplo de Dockerfile para uma aplicação Go:
# Estágio de build
FROM golang:1.21 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server
# Estágio final
FROM scratch
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]
Neste exemplo, a imagem final é baseada em scratch, que é um alvo vazio, contendo apenas o binário estático. Isso garante que não haja bibliotecas extras ou utilitários. Para a maioria das aplicações, essa é uma prática recomendada, mas lembre-se de incluir certificados CA se a aplicação fizer chamadas HTTPS externas, usando por exemplo FROM alpine:latest e copiando os certificados.
Além disso, evite instalar pacotes desnecessários e remova caches de gerenciadores de pacotes dentro da mesma camada para reduzir o tamanho. A combinação de imagens mínimas com multi-stage builds é uma das estratégias mais eficazes para segurança e eficiência.
Usuário não-root
Executar containers como root é um dos erros mais comuns e perigosos. Se um atacante explorar uma vulnerabilidade na aplicação, ele obterá acesso com privilégios de root dentro do container, o que pode facilitar a escalada de privilégios no host ou o comprometimento de dados. A boa prática é criar um usuário sem privilégios no Dockerfile e executar o processo com esse usuário.
Em imagens base como Ubuntu, você pode criar um usuário e definir o USER no Dockerfile. Veja um exemplo:
FROM node:20-slim
# Criar usuário sem privilégios
RUN groupadd -r nodejs && useradd -r -g nodejs -m -s /sbin/nologin nodeuser
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Mudar proprietário dos arquivos para o usuário não-root
RUN chown -R nodeuser:nodejs /app
# Definir usuário para executar o container
USER nodeuser
EXPOSE 3000
CMD ["node", "server.js"]
Nesse Dockerfile, criamos o usuário nodeuser sem shell e definimos que o container deve executar os comandos como esse usuário. Isso limita o impacto de uma possível invasão. Para imagens baseadas em Alpine, o comando é similar: addgroup -S app && adduser -S -G app app.
Além disso, é possível usar o recurso de namespaces e capabilities do Docker para reforçar a segurança, como remover capacidades desnecessárias (--cap-drop all) e adicionar apenas as necessárias (--cap-add). Em Kubernetes, você pode definir o securityContext com runAsNonRoot: true e especificar o runAsUser.
Lembre-se de que, se você usar imagens mínimas como distroless, elas já executam como não-root por padrão, o que é uma vantagem adicional. Sempre teste sua aplicação para garantir que ela não precise de privilégios administrativos.
Scanning
A varredura (scanning) de imagens é o processo de analisar uma imagem em busca de vulnerabilidades conhecidas, usando bancos de dados como CVE (Common Vulnerabilities and Exposures). Essa prática deve ser integrada ao pipeline de CI/CD para que imagens vulneráveis não sejam implantadas em produção. Ferramentas como Trivy, Clair, Anchore e Snyk são amplamente utilizadas.
O Trivy é uma ferramenta de código aberto que verifica vulnerabilidades em sistemas operacionais e dependências de linguagens de programação. Você pode executá-lo localmente ou como parte de um estágio no pipeline. Exemplo de uso com Docker:
# Instalar Trivy (exemplo em Linux)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Escanear uma imagem
trivy image --severity HIGH,CRITICAL minha-imagem:latest
O Trivy retorna uma lista de vulnerabilidades com severidade, descrição e referências. É importante definir políticas de falha no pipeline: se uma imagem tiver vulnerabilidades críticas, o build deve falhar. Em GitLab CI, por exemplo, você pode adicionar um job:
scan:
stage: test
script:
- trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Além da varredura estática, é recomendável realizar varreduras em tempo real dos containers em execução, para detectar vulnerabilidades que possam surgir após a implantação. Ferramentas como Falco podem ajudar a detectar comportamentos anômalos em runtime, mas isso faz parte de uma estratégia mais ampla.
A integração do scanning em cada estágio do pipeline, desde o desenvolvimento até a produção, garante que nenhuma imagem vulnerável seja promovida. Automatizar esse processo é essencial para manter a segurança sem sobrecarregar a equipe.
Políticas
Políticas de segurança definem regras que controlam quais imagens podem ser executadas, quem pode acessar os registries e quais ações são permitidas. Em Kubernetes, o Admission Controller (como o OPA/Gatekeeper ou Kyverno) pode impor políticas que validam as imagens antes de serem executadas. Isso impede, por exemplo, que imagens com usuário root ou com vulnerabilidades conhecidas sejam implantadas.
Um exemplo de política com Kyverno, que é uma ferramenta nativa do Kubernetes, pode exigir que os containers não rodem como root:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: enforce
rules:
- name: check-user
match:
resources:
kinds:
- Pod
validate:
message: "Containers não podem rodar como root"
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
Esse exemplo de política impede a criação de Pods que não definam runAsNonRoot: true. Políticas podem ser usadas para restringir o uso de imagens de registries não confiáveis, exigir que as imagens tenham sido escaneadas e aprovadas, e até mesmo bloquear o uso de tags latest (imutabilidade).
Além do Kubernetes, você pode implementar políticas no nível de registro de containers (como Harbor ou JFrog Artifactory), que podem bloquear o push de imagens vulneráveis com base em resultados de scan. Isso cria uma barreira adicional.
É importante que as políticas sejam claras, testáveis e versionadas como código (policy-as-code). Isso permite revisão e auditoria. Ferramentas como OPA (Open Policy Agent) oferecem uma linguagem flexível (Rego) para expressar políticas mais complexas.
Em um ambiente DevOps, a segurança deve ser uma preocupação contínua, e políticas são uma forma de automatizar a conformidade, reduzindo o risco de erro humano.
Boas Práticas e Observações Finais
Além dos tópicos abordados, considere as seguintes boas práticas:
- Imagens imutáveis: Use tags únicas (ex.: hash do commit) e evite
latestpara garantir rastreabilidade. - Atualização regular: Reconstrua imagens periodicamente para incorporar correções de segurança.
- Menor privilégio: Conceda apenas as permissões necessárias para a aplicação e o container.
- Segredos: Nunca armazene segredos em imagens; use variáveis de ambiente ou gerenciadores de segredos.
- Monitoramento: Monitore os logs e o comportamento dos containers para detectar atividades suspeitas.
A segurança de containers é uma responsabilidade compartilhada entre desenvolvedores, operações e segurança. Integrar essas práticas desde o início do ciclo de desenvolvimento reduz custos e riscos. Lembre-se de que a segurança é um processo contínuo, não um evento único.
Referências
- Dockerfile best practices - Docker Docs
- Dockerfile USER instruction - Docker Docs
- Trivy Documentation
- Pod Security Standards - Kubernetes
- Kyverno Documentation
- Open Policy Agent Documentation
- Multi-stage builds - Docker Docs
- Distroless Images - GoogleContainerTools
Exercícios
-
Crie um Dockerfile para uma aplicação Node.js que use uma imagem mínima (por exemplo, node:20-slim) e execute como usuário não-root. Explique cada etapa.
✓ Resposta: Exemplo de Dockerfile:FROM node:20-slim # Criar usuário não-root RUN groupadd -r nodeuser && useradd -r -g nodeuser -m -s /sbin/nologin nodeuser WORKDIR /app COPY package*.json ./ RUN npm install COPY . . # Ajustar permissões RUN chown -R nodeuser:nodeuser /app USER nodeuser EXPOSE 3000 CMD ["node", "server.js"]Explicação: A imagem base é mínima (slim). Criamos um grupo e usuário sem privilégios. Copiamos os arquivos, instalamos dependências, ajustamos o proprietário e definimos o usuário para execução.
-
Qual é a diferença entre usar uma imagem Alpine e uma imagem baseada em Ubuntu para segurança? Cite vantagens e desvantagens.
✓ Resposta: Alpine é menor e tem menos pacotes, reduzindo a superfície de ataque, mas usa musl, o que pode causar incompatibilidade com binários compilados para glibc. Ubuntu é maior, mas tem suporte mais amplo e glibc. Para segurança, Alpine é geralmente preferida, mas é necessário testar a compatibilidade da aplicação. -
Escreva um comando Trivy para escanear uma imagem e falhar se houver vulnerabilidades críticas. Explique as opções usadas.
✓ Resposta: Comando:trivy image --exit-code 1 --severity CRITICAL minha-imagem:latest
As opções:--exit-code 1faz o comando retornar código 1 se vulnerabilidades forem encontradas, permitindo falhar no pipeline.--severity CRITICALfiltra apenas vulnerabilidades críticas. -
Usando Kyverno, escreva uma política que exija que as imagens venham de um registro confiável (por exemplo, registry.exemplo.com).
✓ Resposta: Exemplo de política Kyverno:apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-trusted-registry spec: validationFailureAction: enforce rules: - name: check-registry match: resources: kinds: - Pod validate: message: "Imagem deve ser do registro registry.exemplo.com" pattern: spec: containers: - image: "registry.exemplo.com/*"Essa política bloqueia Pods cujas imagens não comecem com o prefixo especificado.
-
Quais são as vantagens de usar multi-stage builds em relação à segurança? Dê um exemplo concreto.
✓ Resposta: Multi-stage builds permitem que a imagem final contenha apenas o artefato necessário, sem fontes, compiladores ou ferramentas. Isso reduz a superfície de ataque. Exemplo: para uma aplicação Go, o estágio de build usa golang, e a imagem final éscratchoudistroless, contendo apenas o binário. Assim, não há shell, gerenciador de pacotes ou bibliotecas desnecessárias.