Onde acabam as variáveis de ambiente de um container
Suba um container com -e DB_PASSWORD=hunter2 e rode docker inspect nele:
| |
| |
A senha fica ali em texto plano, legível por qualquer um com acesso ao socket do Docker, por docker exec api env e por qualquer processo rodando dentro do container. Isso não é um bug — é exatamente para isso que as variáveis de ambiente existem. O problema é usá-las para valores que nunca deveriam ficar tão visíveis.
Onde definir uma variável de ambiente no Docker: build time vs runtime
Quatro mecanismos, três ciclos de vida. Onde você define uma variável decide por quanto tempo ela dura e quem consegue lê-la de volta:
| Mecanismo | Definida onde | Fica na imagem? | Visível depois de docker inspect? |
|---|---|---|---|
ARG no Dockerfile | Só no build | Não (a menos que seja copiada para ENV) | Não, mas fica registrada no histórico de build |
ENV no Dockerfile | No build | Sim, embutida em cada camada seguinte | Sim |
-e / --env no docker run | Ao iniciar o container | Não | Sim |
--env-file no docker run | Ao iniciar o container | Não | Sim |
Em tempo de execução você as passa uma a uma, ou a partir de um arquivo:
| |
.env.production tem essa cara:
| |
--env-file passa a ser a melhor opção assim que você tem mais de duas ou três variáveis: mantém o comando de execução legível e os valores em um único lugar que dá para comparar com um diff.
Variáveis de ambiente no Docker Compose: environment, env_file e .env
O Compose tem três lugares para colocar variáveis, e dois deles são arquivos com nomes quase idênticos. .env e env_file: fazem trabalhos completamente diferentes.
.envna raiz do projeto é lido pelo próprio Compose, para substituir os placeholders${VARIAVEL}dentro decompose.yaml. Nunca chega ao container a menos que você também o referencie emenvironment:.env_file:lista arquivos cujo conteúdo é injetado no ambiente do container, exatamente como--env-filenodocker run.environment:define variáveis diretamente no arquivo Compose, inline.
| |
Quando uma variável é definida em mais de um lugar, o Compose a resolve nesta ordem, da maior prioridade para a menor: um -e passado ao docker compose run, depois environment:, depois env_file:, depois o ENV que já vem embutido na imagem. Se NODE_ENV aparece tanto em environment: quanto em .env.api, vence o valor de environment:.
Por que variáveis de ambiente não são o lugar certo para secrets
Nenhum dos mecanismos acima esconde um valor de quem tem acesso ao container ou ao host:
docker inspectimprime cada variável em tempo de execução, como mostrado acima.docker exec <container> envas imprime de dentro.- Um processo filho herda todo o ambiente, incluindo um debugger, um crash reporter ou uma dependência que você não auditou.
- Qualquer coisa que despeje seu ambiente ao iniciar manda o valor para o seu agregador de logs, e um número surpreendente de frameworks faz exatamente isso quando o debug logging está ativo.
- Variáveis de ambiente definidas no build com
ENVsão permanentes:docker history --no-trunc my-apimostra o valor exato, e ele fica na imagem em todo registry para onde você a envia.
Nada disso exige que um atacante comprometa o container. Basta o acesso que muita gente já tem: o socket do Docker, os logs da CI, o registry de imagens.
Como funcionam os secrets do Docker: o valor é montado como arquivo
Pare de entregar credenciais ao container como variáveis de ambiente e monte-as como arquivos. O Compose faz isso sozinho, sem precisar de Swarm:
| |
O Compose monta db_password.txt em /run/secrets/db_password dentro do container, somente leitura e em nenhum outro lugar: nunca aparece no docker inspect, no docker exec ... env, nem na imagem. O sufixo _FILE é uma convenção que as imagens oficiais do Postgres, MySQL e MongoDB já suportam: ao iniciar, o script de entrypoint lê o arquivo em vez de esperar o valor diretamente.
Se sua aplicação não suporta essa convenção, leia o arquivo você mesmo ao iniciar: uma linha na maioria das linguagens, por exemplo open('/run/secrets/db_password').read().strip() em Python.
Em um cluster Swarm, o secret equivalente é criado e distribuído pelo orquestrador em vez de um arquivo local:
| |
O Swarm guarda o secret criptografado em repouso e em trânsito, e o monta num filesystem em memória em cada réplica: ele nunca é escrito na camada gravável do container.
Mantendo secrets fora da imagem no build
ARG e ENV num Dockerfile têm o mesmo problema um passo antes: um valor passado como argumento de build fica registrado no histórico de build da imagem mesmo que você nunca o transforme numa ENV.
| |
| |
Esse comando docker history encontra o token. Ele some do filesystem final se você não fizer COPY do arquivo de configuração adiante, mas fica legível para sempre nos metadados da imagem, que viajam com ela para todo registry.
A flag --secret do BuildKit evita isso montando o valor em uma única etapa RUN, como um arquivo em memória que nunca vira camada:
| |
| |
npm_token existe só durante essa instrução RUN e não fica registrado em lugar nenhum que o docker history consiga ver.
Quando usar uma variável de ambiente e quando usar um secret
| Situação | Use |
|---|---|
| Configuração não sensível (porta, log level, feature flag) | -e, --env-file, ou environment:/env_file: do Compose |
| Senha de banco de dados, API key, chave TLS em tempo de execução | Um secret do Docker, montado como arquivo |
Token de autenticação necessário só durante docker build | --secret do BuildKit com RUN --mount=type=secret |
| Valor embutido na imagem de propósito (versão do app, commit de build) | ARG copiado para ENV — tudo bem, não é secret |
Uma pergunta decide tudo: você se incomodaria se esse valor vazasse? Se sim, ele não passa por -e, --env-file, environment:, nem por um ARG/ENV no Dockerfile. Ele passa por um secret montado como arquivo. As configurações que seu app imprime de boa nos próprios logs continuam sendo variáveis de ambiente normais, mais fáceis de sobrescrever por ambiente.
Então vá conferir. Rode docker inspect --format '{{json .Config.Env}}' nos containers que você tem rodando agora, e leia a saída como se fosse o diff de um pull request. Qualquer coisa ali que você não gostaria de ver revisada em público deve ir para um bloco secrets: no seu arquivo Compose, com uma variável _FILE no lugar da senha. Se o arquivo precisa sobreviver ao container em vez de vir do contexto de build, coloque-o num volume Docker.