Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 35

Build-time Secrets with Docker BuildKit

Recommended resources

Olá! Na nossa última lição, exploramos como gerenciar segredos em runtime usando o Docker Compose secrets, uma técnica essencial para proteger dados como senhas de banco de dados quando seus contêineres já estão em execução.

Hoje, vamos abordar um desafio relacionado, mas distinto: como lidar com segredos necessários durante o processo de build da imagem. Imagine que você precisa baixar um pacote de um repositório NuGet privado ou clonar um repositório Git que exige autenticação. Passar essas credenciais de forma insegura pode incorporá-las permanentemente na sua imagem Docker, criando uma séria vulnerabilidade.

Nesta lição, você aprenderá a fornecer segredos de forma segura durante o processo de build usando o flag --secret do Docker BuildKit. Este é o método moderno e recomendado para garantir que suas credenciais de build-time sejam usadas apenas quando necessário e nunca vazem para a imagem final.

1. O Problema: Como os Segredos Vazam Durante o Build

Antes de saltarmos para a solução, é crucial entender por que as abordagens tradicionais são perigosas. Como desenvolvedor experiente, você pode pensar em usar argumentos de build (ARG) ou simplesmente copiar um arquivo de segredo (COPY) e depois removê-lo. Vamos ver por que essas estratégias são falhas.

O vídeo a seguir demonstra de forma clara e prática o problema de usar ARG.

Mount Secure Build-Time Secrets with Docker and Docker Compose

Para começar, assista a este trecho do vídeo "Mount Secure Build-Time Secrets with Docker and Docker Compose" de Nick Janetakis. Ele mostra como um segredo passado via ARG pode ser facilmente exposto.

Assista do início até 05:00. O apresentador demonstrará a maneira insegura de usar build-arg e, em seguida, usará docker image history para provar que o segredo foi permanentemente incorporado à camada da imagem.

Como o vídeo mostra, o comando docker history revela o segredo em texto plano. Isso ocorre porque cada instrução RUN em um Dockerfile cria uma nova camada na imagem. Mesmo que uma instrução posterior remova o segredo (com rm), a camada anterior que o continha ainda existe e pode ser inspecionada.

Para satisfazer sua curiosidade sobre "como isso funciona" internamente, o artigo a seguir mostra como qualquer pessoa com acesso à imagem pode extrair esses segredos camada por camada.

Handling Docker Secrets the Right Way

O artigo "Handling Docker Secrets the Right Way" fornece uma excelente demonstração técnica de como os segredos vazam. Isso reforçará seu entendimento sobre a imutabilidade das camadas do Docker.

Leia a seção "Why not use just files?". O autor usa docker save para exportar uma imagem e, em seguida, comandos tar para extrair um arquivo de segredo de uma camada intermediária, mesmo depois que o arquivo foi "removido" em uma camada posterior. Você não precisa executar os comandos, mas entender o processo é fundamental.

Essa exploração prática deixa claro que precisamos de um mecanismo que disponibilize os segredos para uma etapa de build sem nunca os gravar no sistema de arquivos da imagem.


2. A Solução: Docker BuildKit e Secret Mounts

A solução para este problema é o BuildKit, o engine de build de nova geração do Docker. Ele é o padrão nas versões recentes do Docker e introduz uma maneira completamente nova e segura de lidar com segredos de build.

Em vez de passar dados para a imagem, o BuildKit permite que uma etapa RUN monte temporariamente um arquivo de segredo de sua máquina host. Esse arquivo existe apenas em memória, apenas durante a execução daquela instrução RUN, e nunca é salvo em nenhuma camada da imagem.

BuildKit Secret Handling Flow
Este diagrama ilustra como o BuildKit atua como um intermediário seguro. Ele pega os segredos da sua máquina e os monta no processo de build, mas garante que eles nunca cheguem à imagem final.

O processo envolve duas partes:

  1. No docker build: Usar o flag --secret para informar ao Docker sobre o segredo e sua origem (um arquivo local ou uma variável de ambiente).
  2. No Dockerfile: Usar a opção --mount=type=secret em uma instrução RUN para montar o segredo e usá-lo.

O vídeo a seguir demonstra o fluxo completo.

Mount Secure Build-Time Secrets with Docker and Docker Compose

Agora, vamos ver a solução em ação. Assista a esta parte do mesmo vídeo de Nick Janetakis, onde ele implementa o método seguro.

Assista de 06:06 a 11:15. Preste atenção aos seguintes pontos: A nova sintaxe RUN --mount=type=secret,id=access_token no Dockerfile. Como o segredo é acessado dentro do contêiner no caminho /run/secrets/access_token. O comando de build correspondente: docker build --secret id=access_token.... A verificação final com docker history --no-trunc, que prova que o segredo não está presente na imagem.

Resumindo a sintaxe:

Dockerfile:

# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:8.0

# O segredo só estará disponível dentro desta instrução RUN
RUN --mount=type=secret,id=my_nuget_token \
    SECRET_VALUE=$(cat /run/secrets/my_nuget_token) && \
    echo "O token secreto é: $SECRET_VALUE"
  • --mount=type=secret: Especifica que estamos montando um segredo.
  • id=my_nuget_token: Define um identificador para o segredo. Este id será usado para referenciá-lo no comando build.

Comando de Build (passando de um arquivo):

# Crie um arquivo local com seu segredo
echo "my-super-secret-nuget-token" > .secret-file

# Construa a imagem, apontando para o arquivo
docker build --secret id=my_nuget_token,src=.secret-file -t my-app .
  • --secret id=my_nuget_token,src=.secret-file: Informa ao BuildKit para usar o conteúdo do arquivo .secret-file para o segredo com o ID my_nuget_token.

Comando de Build (passando de uma variável de ambiente):

# Exporte o segredo como uma variável de ambiente
export NUGET_TOKEN="my-super-secret-nuget-token"

# Construa a imagem
docker build --secret id=my_nuget_token,env=NUGET_TOKEN -t my-app .
  • --secret id=my_nuget_token,env=NUGET_TOKEN: Informa ao BuildKit para usar o valor da variável de ambiente NUGET_TOKEN para o segredo.

3. Cenários de Uso e Melhores Práticas

Agora que você entende o mecanismo, vamos ver como aplicá-lo em cenários realistas, especialmente em conjunto com multi-stage builds.

Usando Segredos com Multi-Stage Builds

A combinação de build secrets e multi-stage builds é a prática recomendada para máxima segurança e otimização de imagem. A ideia é usar o segredo apenas no estágio de build para buscar dependências ou compilar código, e depois copiar apenas os artefatos necessários para um estágio final limpo.

Aqui está um exemplo relevante para você, como um desenvolvedor .NET, que usa um token para restaurar pacotes de um feed NuGet privado.

# syntax=docker/dockerfile:1

# ====== Estágio de Build ======
# Usa o SDK completo e terá acesso ao segredo
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copie os arquivos de projeto primeiro
COPY ["MyApi/MyApi.csproj", "MyApi/"]
COPY ["nuget.config", "./"]

# Monte o segredo do NuGet e configure o feed privado
RUN --mount=type=secret,id=nuget_password \
    dotnet nuget add source "https://pkgs.dev.azure.com/my-org/_packaging/MyFeed/nuget/v3/index.json" \
    --name "MyFeed" \
    --username "user" \
    --password $(cat /run/secrets/nuget_password) \
    --store-password-in-clear-text \
    --configfile "nuget.config"

# Restaure as dependências
RUN dotnet restore "MyApi/MyApi.csproj"

# Copie o resto do código e publique
COPY . .
WORKDIR "/src/MyApi"
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish

# ====== Estágio Final ======
# Usa a imagem de runtime mais leve, sem SDK e sem segredos
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

Neste exemplo:

  • O segredo nuget_password só é montado no estágio build.
  • Ele é usado para autenticar com o feed privado durante o dotnet nuget add source.
  • O estágio final apenas copia os artefatos compilados (/app/publish). Ele não contém o SDK .NET, o nuget.config com credenciais, nem tem qualquer vestígio do segredo.
Teste seu entendimento!

Imagine que você precisa clonar um repositório Git privado usando uma chave SSH durante o build. O arquivo da chave privada está em ~/.ssh/id_rsa_git. Como você modificaria a instrução RUN e qual seria o comando docker build correspondente para realizar um git clone de forma segura?

Dockerfile Snippet:

# syntax=docker/dockerfile:1
FROM alpine/git as cloner
WORKDIR /app

# Modifique a linha abaixo
RUN git clone git@github.com:my-org/private-repo.git .
Show answer

A solução usa um tipo de montagem especial, type=ssh, que encaminha de forma segura seu agente SSH local.

Dockerfile Modificado:

# syntax=docker/dockerfile:1
FROM alpine/git as cloner
WORKDIR /app

# A montagem do tipo 'ssh' dá acesso temporário ao agente SSH do host
RUN --mount=type=ssh git clone git@github.com:my-org/private-repo.git .

Comando de Build:
O comando build precisa especificar qual chave SSH usar ou simplesmente usar o agente padrão.

# Assumindo que sua chave já está carregada no agente SSH (ssh-add)
docker build --ssh default -t my-app .

# Ou, para especificar uma chave
docker build --ssh git_key=$HOME/.ssh/id_rsa_git -t my-app .
# (Neste caso, o Dockerfile precisaria ser `RUN --mount=type=ssh,id=git_key ...`)

Este método é ainda mais seguro do que montar o arquivo da chave, pois ele apenas encaminha o soquete do agente, e a chave privada em si nunca sai do host.


4. Integração com Docker Compose

Naturalmente, você vai querer gerenciar isso dentro dos seus projetos com docker-compose.yml. O Docker Compose se integra perfeitamente com os segredos de build.

Você define os segredos na seção build do seu serviço.

Docker Build Secrets Guide

Para concluir, vamos ver como o Docker Compose simplifica o gerenciamento de segredos de build para múltiplos serviços. Este artigo do DataCamp mostra a sintaxe de forma clara.

Leia a seção "Integrating Build Secrets With Docker Compose". Observe a estrutura do docker-compose.yml: uma seção secrets de nível superior define as fontes dos segredos (arquivo ou variável de ambiente), e a seção build.secrets dentro do serviço os disponibiliza para o processo de build.

Aqui está um exemplo de como o docker-compose.yml ficaria para o nosso cenário de NuGet:

# docker-compose.yml
version: '3.8'

services:
  my-api:
    build:
      context: .
      dockerfile: MyApi.Dockerfile
      secrets:
        # Disponibiliza o segredo 'nuget_password' para o build deste serviço
        - nuget_password
    ports:
      - "8080:80"

# Definição de nível superior dos segredos
secrets:
  nuget_password:
    # A fonte do segredo é o arquivo local ./.secrets/nuget_token
    file: ./.secrets/nuget_token

Com esta configuração, você só precisa executar docker-compose build, e o Compose cuidará de passar o segredo para o BuildKit da maneira correta.


Conclusão

Parabéns! Você agora domina a técnica moderna e segura para gerenciar credenciais durante o processo de build de imagens Docker. Isso não apenas protege seus segredos, mas também leva a imagens finais mais limpas, menores e mais seguras.

Principais Pontos:

  • Nunca use ARG ou COPY para injetar segredos de build, pois eles persistem nas camadas da imagem.
  • Use o Docker BuildKit com RUN --mount=type=secret para montar segredos temporariamente em memória.
  • Passe segredos para o comando de build com o flag --secret, especificando um arquivo (src) ou variável de ambiente (env) como fonte.
  • Combine sempre os segredos de build com multi-stage builds para garantir que a imagem final não contenha nem os segredos nem as ferramentas que os utilizaram.
  • Integre o gerenciamento de segredos de build em seus fluxos de trabalho de desenvolvimento usando a seção build.secrets no seu docker-compose.yml.

Na próxima lição, começaremos nossa jornada no .NET Aspire. Você verá como o Aspire abstrai e simplifica muito do que aprendemos sobre a orquestração de contêineres, redes e configurações, proporcionando uma experiência de desenvolvimento de aplicações nativas para a nuvem muito mais fluida.