Isso aconteceu no meu laboratório de estudo, no primeiro deploy do app piloto. Os números e os arquivos são os reais.
01 / IMAGEM BARRADA
O pipeline fez exatamente o que devia: parou.
A esteira da API tem um passo antes de mandar a imagem para o ECR: o Trivy procura vulnerabilidades e reprova se achar alguma HIGH ou CRITICAL que já tenha correção. No primeiro deploy, ele reprovou.
A tentação nessa hora é grande. Desliga o scanner, coloca tudo num arquivo de ignorar e segue a vida. Só que aí o scanner vira enfeite. Resolvi entender de onde vinha cada uma.
A REGRA QUE EU SEGUIScanner que a gente desliga quando incomoda não protege nada.
02 / DE ONDE VINHA
A aplicação estava limpa. A imagem, não.
A API do piloto é pequena e não usa nenhuma biblioteca externa. Então as vulnerabilidades não podiam ser do código. Olhando a tabela do Trivy com calma, apareceram três culpados:
npm dentro da imagem
A imagem oficial do Node já vem com npm, yarn e corepack. A aplicação não usa nenhum deles em produção, mas as dependências deles estavam lá.
Sistema desatualizado
Bibliotecas do Alpine, como o openssl, tinham correções que ainda não estavam na imagem base.
Node fora de suporte
A versão 20 do Node chegou ao fim do suporte em abril de 2026. Imagem sem suporte para de receber correção.
Tudo num estágio só
Ferramenta de build e aplicação moravam na mesma imagem final.
03 / A CORREÇÃO
Levar para produção só o que a aplicação usa.
Multi-stage: um estágio para instalar, outro para rodar
O primeiro estágio instala as dependências com o npm. O segundo copia só o resultado. O npm nunca chega na imagem final.
Tirar o que sobra e atualizar o sistema
Na imagem final, removi npm, yarn e corepack e rodei apk upgrade para aplicar as correções do Alpine. Também subi para o Node 22, que tem suporte.
De quebra, mais segurança
Já que estava mexendo, a aplicação passou a rodar com o usuário node (nunca root) e ganhou um healthcheck.
# Estágio 1: instala as dependências (o npm fica só aqui)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund
# Estágio 2: só o Node + o app
FROM node:22-alpine AS runtime
ENV NODE_ENV=production PORT=3000
# correções do Alpine + remove ferramentas de build
RUN apk upgrade --no-cache \
&& rm -rf /usr/local/lib/node_modules/npm \
/usr/local/lib/node_modules/corepack \
/usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \
/usr/local/bin/yarn /usr/local/bin/yarnpkg /opt/yarn-*
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node package.json ./
COPY --chown=node:node src ./src
USER node
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:3000/health || exit 1
CMD ["node", "src/server.js"]04 / O SCANNER
Também confiei menos no próprio scanner.
Um detalhe que pouca gente olha: de onde vem o scanner. Em vez de usar uma action de terceiros, o pipeline baixa o binário oficial do Trivy com versão fixa e confere o checksum antes de rodar. Se alguém trocar o arquivo no caminho, o pipeline falha.
- name: Instalar Trivy (versão fixa + checksum)
run: |
FILE="trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
curl -sSfL -o "/tmp/${FILE}" \
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${FILE}"
echo "${TRIVY_SHA256} /tmp/${FILE}" | sha256sum -c -
tar -xzf "/tmp/${FILE}" -C /tmp trivy
- name: Trivy scan
run: |
trivy image --input /tmp/image.tar \
--severity "CRITICAL,HIGH" --ignore-unfixed --exit-code 105 / RESULTADO
Zero, e o scanner continua ligado.
26 → 0 vulnerabilidades na imagem da API, sem nenhuma exceção no arquivo de ignorar.
Imagem menor sem ferramentas de build que a aplicação não usa.
Node com suporte 22 LTS no lugar do 20, que já tinha saído de suporte.
Sem root a aplicação roda com o usuário node.
Scanner confiável versão fixa e checksum conferido a cada execução.
06 / APRENDIZADOS
O que eu levo dessa.
Antes de discutir com o scanner, leia de onde vem cada vulnerabilidade. Na maioria das vezes ela não está no seu código, está em algo que veio junto e ninguém precisava.
E arquivo de ignorar existe, mas cada linha nele precisa de um motivo escrito. Se não dá para explicar por que aquela vulnerabilidade não importa, ela importa.
william@devops:~$ echo $LESSON_LEARNED
"Na imagem de produção vai só o que a aplicação usa. O resto é superfície de ataque de graça."