Skip to main content


Introdução aos impactos do uso de arquivos .env em Containers


Os arquivos .env são frequentemente utilizados para definir variáveis de ambiente fora do código-fonte, facilitando a configuração de aplicações containerizadas. Eles costumam possuir credenciais e configurações sensíveis (como senhas, tokens de API, strings de conexão etc.) que são carregadas no ambiente do container em tempo de execução.


Apesar da conveniência, é crucial entender os potenciais impactos negativos à segurança, tanto em ambientes de desenvolvimento quanto de produção, ao se utilizar arquivos .env para prover variáveis de ambiente. A exposição indevida dessas variáveis pode ter consequências graves, a divulgação de segredos como senhas ou chaves de API pode ser catastrófica, permitindo que agentes maliciosos acessem sistemas privilegiados indevidamente.


Vamos analisar os principais riscos do uso de arquivos .env em containers e comparar essa prática com abordagens mais seguras.



Terminologia e padrões


O termo "segredo" será usado para se referir a qualquer dado sensível que conceda acesso a sistemas ou serviços, incluindo senhas, tokens, chaves de API e credenciais. Sempre que o termo "segredo" for mencionado, ele deve ser entendido nesse contexto.



Risco de vazamento de Credenciais


Colocar credenciais confidenciais em arquivos .env torna o vazamento de segredos um risco real, seja por descuido ou ataque deliberado. O arquivo .env armazenar os dados em texto puro, se o arquivo ou as variáveis de ambiente derivadas dele forem acessíveis, as informações podem ser facilmente obtidas (depende muito do método, vamos descrever por partes).


Em um container Docker, todas as variáveis definidas (inclusive via .env) ficam disponíveis no ambiente do processo da aplicação e muitas vezes a qualquer processo em execução dentro do container, sem isolamento interno. Isso significa que se um invasor conseguir executar código no container (por exemplo, explorando alguma vulnerabilidade da aplicação), ler variáveis de ambiente será muito fácil.


Um estudo de segurança da Trend Micro destaca que esse comportamento viola o princípio de disponibilidade efêmera dos segredos, uma vez carregados, os valores permanecem na memória e são herdados por quaisquer processos filhos, ampliando a superfície de ataque caso haja vazamento.


Portanto, ao definir segredos como variáveis de ambiente, eles tendem a permanecer acessíveis durante todo o tempo de vida do processo, diferente de abordagens onde o segredo é buscado sob demanda e imediatamente descartado da memória.


Outra preocupação é que variáveis de ambiente podem acidentalmente acabar expostas por funcionalidades de debug ou erro na aplicação. Vários frameworks e ferramentas de diagnóstico oferecem endpoints ou logs que exibem informações de ambiente para auxiliar na depuração, algo perigoso se incluir credenciais.


A própria documentação oficial do Docker alerta que usar variáveis de ambiente para senhas e chaves "acarreta risco de exposição não intencional de informações", pois tais variáveis podem ser acessíveis a processos inesperados e até impressas em logs de erro sem o conhecimento do usuário.


Em ambientes de desenvolvimento, é comum habilitar logs verbosos ou ferramentas de inspeção, aumentando o risco de credenciais em .env aparecerem em consoles ou dumps. Em produção, embora práticas de logging sejam mais controladas, o impacto de qualquer vazamento é muito maior.


Vale frisar também que, em containers Docker padrão, qualquer pessoa com acesso ao daemon Docker ou à instância pode inspecionar as variáveis de ambiente ativas. Se segredos forem passados via .env (ou via flags -e do Docker), eles se tornam visíveis através de comandos de inspeção do Docker (como docker inspect).


Por exemplo, ao executar um container com -e MYSQL_ROOT_PASSWORD=foobar, essa senha pode ser facilmente recuperada executando docker inspect no container.

$ docker run --rm -it -e MYSQL_PASSWORD=TESTANDO alpine printenv | grep MYSQL
MYSQL_PASSWORD=TESTANDO


$ docker inspect 09f9b5f44cdf | grep MYSQL
"MYSQL_PASSWORD=TESTANDO",


$ cat /proc/self/environ
HOSTNAME=e0a3ba929d61SHLVL=1HOME=/rootTERM=xtermMYSQL_PASSWORD=TESTANDOPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binPWD=//

Como foi mostrado acima, qualquer processo dentro do container pode ler variáveis de ambiente do próprio processo, após comprometer o container, um invasor pode simplesmente executar comandos como printenv ou examinar /proc/self/environ para coletar todas as credenciais em memória.


Esses vetores mostram que segredos em variáveis de ambiente são de fácil acesso tanto de fora (via ferramentas de inspeção do Docker) quanto de dentro do container, caso ele seja invadido.


Outro erro clássico que causa vazamentos de segredos é deixar arquivos .env serem incluídos no controle de versão (como Git ou outros SCM). Por descuido, desenvolvedores às vezes commitam o .env no repositório (com todas as credenciais) o que pode levar a exposições desastrosas, especialmente em repositórios públicos.


O Git é um bom lugar para profissionais de segurança e pessoas mal-intencionadas procurarem por segredos expostos. Uma vez que um secret é publicado num repositório público, assume-se que ele foi comprometido (possivelmente capturado por scans automáticos) e deve ser rotacionado imediatamente, já que invasores monitoram essas ocorrências.


Uso ideal do .env

O uso ideal do arquivo .env é para definir parâmetros de configuração da aplicação, e não para armazenar dados sensíveis. Em outras palavras, ele deve servir para configuração funcional, não configuração secreta.


Devemos usar ele para definir parâmetros de configuração e ajustes operacionais como:

  • Porta do serviço (PORT=8080)
  • Caminhos ou modos de execução (MODE=production, LOG_LEVEL=debug)
  • Endereços internos (DB_HOST=db, REDIS_HOST=cache)
  • Identificadores públicos (como APP_NAME, REGION, etc.)

Esses valores ajudam a modular o comportamento da aplicação sem exigir mudanças no código-fonte.



Práticas Seguras de Gerenciamento de Segredos


Diante dos riscos mencionados, existem mecanismos para armazenar e injetar segredos em aplicações containerizadas, eliminando a necessidade de mantê-los em arquivos de texto puro ou variáveis de ambiente expostas. As principais alternativas incluem Docker Secrets (nativo do Docker/Compose e Docker Swarm), Kubernetes Secrets (no orquestrador Kubernetes), e segregação de segredos em gerenciadores externos como o HashiCorp Vault, AWS Secrets Manager, etc.



Docker Secrets


O Docker introduz um mecanismo de secrets para suprir as limitações de usar variáveis de ambiente. Em Docker Compose (versão 3 ou superior) e em serviços no modo Swarm, é possível definir segredos que são armazenados de forma separada e montados dentro do container como arquivos em tempo de execução (tipicamente em /run/secrets/<nome_do_secret>).


Os Docker Secrets são criptografados em repouso e transmitidos de forma segura dentro de um cluster Swarm. Cada serviço declara explicitamente quais segredos precisa, evitando a exposição global, apenas containers autorizados montam aqueles arquivos secretos. Isso reduz a chance de acesso inadvertido, por exemplo, um processo secundário no container não verá o secret a menos que leia o arquivo, o que é menos provável de ocorrer acidentalmente do que imprimir uma variável de ambiente.


Além disso, o conteúdo do secret não aparece em comandos como docker inspect (no máximo vê-se que um arquivo foi montado, mas não o valor).


Em ambientes standalone (sem Swarm), o Docker Compose oferece suporte a segredos localmente via arquivos, e mesmo nesses casos evita-se colocar senhas diretamente em variáveis de ambiente. A adoção de Docker Secrets em produção é altamente recomendada para qualquer dado que seria crítico nas mãos de um invasor.



Kubernetes


No orquestrador Kubernetes, segredos são objetos de primeira classe que permitem armazenar credenciais, tokens e outros dados sensíveis de forma centralizada, separados das configurações de ambiente comuns. Por padrão, um objeto Secret no Kubernetes armazena seus dados em formato base64 e, quando possível, pode ser combinado com cifragem em repouso no datastore (etcd) da control plane.


O Kubernetes Secrets oferecem maior segurança e privacidade, incluindo suporte a armazenamento criptografado nativamente no cluster. A aplicação pode consumir esses segredos de duas formas principais: montando-os como arquivos/volumes dentro do container (similar ao Docker Secrets) ou mapeando-os para variáveis de ambiente.


A opção de volume é geralmente mais segura, pois permite controlar permissões de arquivo e reduz a chance de exposição em dumps de ambiente. Ainda que o Kubernetes permita expor segredos como variáveis de ambiente para facilidade, a própria documentação e comunidade alertam que isso pode reintroduzir riscos, por exemplo, se o aplicativo acidentalmente logar todas as variáveis de ambiente, irá expor também as obtidas via Secret (afinal, dentro do container, elas se tornam variáveis normais).


Ao fazer build com BuildKit, existe suporte para build secrets (via --secret), permitindo injetar segredos temporariamente durante o processo de build, sem que eles fiquem incorporados nas camadas finais da imagem. Portanto, é possível, por exemplo, usar credenciais para baixar dependências privadas ou executar comandos autenticados no build, sem registrar essas credenciais no Dockerfile, nas camadas intermediárias ou na imagem final. Tudo depende de como a aplicação está obtendo esses dados.


Caso não seja possível usar esse recurso, outra abordagem é o build multi-stage, numa etapa inicial do Dockerfile você copia o .env e usa os valores conforme necessário, e em uma etapa posterior (que gera a imagem final) o arquivo não é incluído.



HashiCorp Vault e Gerenciadores Externos


Um Vault é um exemplo de solução robusta de gerenciamento de segredos externa à aplicação, fornecendo um cofre centralizado onde credenciais são armazenadas cifradas e são liberadas às aplicações sob rigoroso controle de acesso. Em vez de distribuir arquivos .env com todos os segredos, o Vault permite que a aplicação recupere apenas os segredos de que necessita, mediante políticas de acesso, e frequentemente fornecendo segredos de forma dinâmica (por exemplo, senhas temporárias que expiram).



Querendo usar .env ainda?


Quando por qualquer motivos opta-se por usar um .env, alguns cuidados adicionais de segurança devem ser seguidos. As permissões devem ser restritivas no arquivo, o .env deve possuir permissões de leitura apenas para o usuário do processo da aplicação e não deve ser acessível por outros usuários do host/container.


Por exemplo, pode-se definir permissões estilo Unix 600 no arquivo e assegurar que o dono seja o usuário sob o qual a aplicação roda. No caso de containers Docker, muitas imagens por padrão rodam como root, se possível, é aconselhável rodar como um usuário não privilegiado e ajustar a propriedade do .env a esse usuário, limitando o acesso.


No entanto, vale ressaltar que dentro de um único container não há isolamento forte entre processos do mesmo usuário, portanto, as permissões ajudam mais para evitar acesso a nível de sistema de arquivos por outros serviços ou adminstração indevida, mas não impedem que um invasor com execução de código dentro do container leia o .env se este estiver presente.



Docker Compose Secret


Para relembrar, veja aqui e aqui.


O segredo é montado como um arquivo dentro do container (normalmente em /run/secrets/<nome>).


É uma boa prática manter o segredo um local fora do código fonte, por exemplo /etc/secrets/ ou outro diretório seguro, não versionado. Em um diretório com acesso restrito apenas a administradores ou ao usuário que faz o deploy.


As permissões que você pode aplicar são:

  • Proprietário: root ou usuário administrativo seguro.

  • Grupo: um grupo restrito (por exemplo secrets ou deployers) ou deixa root.

  • Permissões: 0400 (somente leitura para proprietário) ou 0440 (leitura para proprietário e grupo autorizado).


Não permitir escrita ou execução por usuários comuns. Se a aplicação roda como usuário não-root, pode não conseguir ler o segredo montado se as permissões não permitirem, tomar cuidado com isso.



Docker Secret (Docker Swarm)


O comando docker secret é uma ferramenta de linha de comando para gerenciar secrets no contexto do Docker Swarm. Ele permite criar, inspecionar, listar e remover segredos usados por serviços Swarm, dados confidenciais como senhas, chaves, certificados ou tokens que não devem ficar embutidos em Dockerfiles ou no código-fonte.


A sintaxe do comando docker secret inclui vários subcomandos para operações específicas:

  • create: Cria um secret a partir de um arquivo ou da entrada padrão (STDIN).
  • inspect: Exibe metadados de um secret (ID, criação, etc.), sem revelar seu valor.
  • ls: Lista todos os secrets existentes no cluster Swarm.
  • rm: Remove um secret (desde que não esteja sendo usado por um serviço).

Por exemplo:

# Cria um secret a partir de entrada padrão:
printf "minha-senha" | docker secret create meu_secret -

# Cria um secret a partir de arquivo:
docker secret create meu_certificado ./server.cert

# Lista os secrets:
docker secret ls

# Inspeciona (metadados) de um secret:
docker secret inspect meu_secret

# Remove um secret:
docker secret rm meu_secret

O segredo é montado como um arquivo dentro do container (normalmente em /run/secrets/<nome>).



O problema com o Secret


Para que o uso de secret funcione bem, a aplicação precisa estar preparada para trabalhar com ele. Imagine uma aplicação que tem um arquivo de configuração com os dados de conexão do banco de dados, você poderia embutir esses dados no código, usar variáveis de ambiente (.env) ou adotar secrets.


A primeira abordagem é insegura, a segunda melhora, mas ainda deixa dados sensíveis em ambientes expostos. Já a terceira é mais segura, mas exige que a aplicação saiba ler os valores de secrets.


No exemplo abaixo, ela busca os dados via variáveis de ambiente:

<?php

$dbtype="pgsql";
$dbhost = getenv('POSTGRES_HOST') ?: 'server';
$dbname = getenv('POSTGRES_DB') ?: 'banco';
$dbuser = getenv('POSTGRES_USER') ?: 'user';
$dbpass = getenv('POSTGRES_PASSWORD') ?: 'senha';
$dbport = getenv('POSTGRES_PORT') ?: '5432';

?>

Imagine que você tenha definido um secret chamado db_password no Docker/Swarm ou Docker Compose, e ele será montado dentro do container em /run/secrets/db_password.


Seu código teria que passar por uma transformação, para que ele possa ler os dados a partir do arquivo:

<?php

function get_secret($name) {
$path = "/run/secrets/{$name}";
if (is_readable($path)) {
return trim(file_get_contents($path));
}
return null;
}

function get_env_fallback($var, $default = null) {
return getenv($var) ?: $default;
}

// Configurações de banco de dados
$dbtype = "pgsql";
$dbhost = get_env_fallback('POSTGRES_HOST', 'server');
$dbname = get_env_fallback('POSTGRES_DB', 'banco');
$dbuser = get_env_fallback('POSTGRES_USER', 'user');

// Para a senha, tentamos ler do secret; se não existir, caímos para variável de ambiente
$dbpass = get_secret('db_password');
if ($dbpass === null) {
$dbpass = get_env_fallback('POSTGRES_PASSWORD', 'senha_padrao');
}

$dbport = get_env_fallback('POSTGRES_PORT', '5432');

?>

Dessa forma, você adapta a aplicação para consumir secrets corretamente, sem depender apenas de variáveis de ambiente para dados sensíveis.



Aplicação não é minha!


Quando a aplicação não for sua e você notar que possa ser muito complexo alterar ela para trabalhar com secret, existem alguns pontos que podemos notar para decidir o melhor caminho. Se ela for uma aplicação Docker e já vem configurada para trabalhar com segredos via variáveis de ambiente, podemos montar o secret como arquivo e usar um entrypoint script ou wrapper que, ao iniciar o container, leia o arquivo secreto e defina variáveis de ambiente ou altere o arquivo de configuração da aplicação. Ou seja, você "traduz" secret para configuração esperada pela aplicação existente.


Usando o exemplo acima, poderiamos fazer isso com shell script. Suponha que você tenha um arquivo de template (config.template.yml), com conteúdo parecido com:

<?php

$dbtype = "pgsql";
$dbhost = "${DB_HOST}";
$dbname = "${DB_NAME}";
$dbuser = "${DB_USER}";
$dbpass = "${DB_PASSWORD}";
$dbport = "${DB_PORT}";

?>

Esse template usa placeholders (variáveis com ${...}) para indicar onde os valores devem ser inseridos. O entrypoint script poderia fazer algo como:

#!/bin/sh
set -e

TEMPLATE_PATH="/app/config.template.yml"
CONFIG_PATH="/app/config.yml"
SECRET_PATH="/run/secrets/db_password"

if [ -r "$SECRET_PATH" ]; then
DB_PASSWORD="$(cat "$SECRET_PATH")"
# Usar sed para substituir o placeholder no template
sed "s|\${DB_PASSWORD}|$DB_PASSWORD|g" "$TEMPLATE_PATH" > "$CONFIG_PATH"
else
# Se não houver secret, podemos copiar o template em branco ou erro
cp "$TEMPLATE_PATH" "$CONFIG_PATH"
fi

# Opcional: unset DB_PASSWORD no shell
unset DB_PASSWORD

exec "$@"

O Dockerfile precisa usar o entreypoint. Lembrando que tudo aqui é bem simples, apenas um teste:

FROM alpine:3.22

RUN addgroup -S fulano \
&& adduser -S -D -H -s /sbin/nologin -G fulano fulano

RUN mkdir /app

COPY ./config.template.yml /app/config.template.yml

COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh

RUN chmod +x /usr/local/bin/entrypoint.sh

RUN chown -R fulano:fulano /app

WORKDIR /app

USER fulano:fulano

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

Vou usar o Compose para exportar o secret:

services:
alpine:
build:
context: .
dockerfile: Dockerfile
image: alpine:latest
tty: true
stdin_open: true
environment:
POSTGRES_DB: meu_banco
POSTGRES_USER: meu_usuario
# Para container de bancod e dados, podemos usar a convenção '*_FILE' para que o container leia a senha do secret:
#POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
command: sh

secrets:
db_password:
file: ./db_password.txt

Agora execute:

docker compose up --build

E podemos ver como ficou:

# Verifique o arquivo de configuração:
/app $ cat config.yml
<?php

$dbtype = "pgsql";
$dbhost = "${DB_HOST}";
$dbname = "${DB_NAME}";
$dbuser = "${DB_USER}";
$dbpass = "testando_secrets";
$dbport = "${DB_PORT}";

?>

# Verifique se a variável é exibida:
/app $ printenv
HOSTNAME=c0ddbcf46194
SHLVL=1
HOME=/home/fulano
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
POSTGRES_USER=meu_usuario
PWD=/app
POSTGRES_DB=meu_banco
/app $

nunca use export nas variáveis do entrypoint

Se você usar export nas variáveis no entrypoint, isso fará com que a variável deixe de ser local e passe a ficar disponível para processos filhos do processo principal do container.


Um teste simples que podemos fazer é o seguinte. No entrypoint.sh, defina assim a parte onde obtemos os dados do secret:

export DB_PASSWORD="$(cat "$SECRET_PATH")"

Ao iniciar o container, execute:

# Use attach para acessar o shell em execução do container,
# em vez de abrir um novo processo.

$ docker attach teste_docker-alpine-1
/app $
/app $ echo $DB_PASSWORD
teste_senha_compose

Agora, se removermos o export, a variável deixará de ser acessível fora do script:

DB_PASSWORD="$(cat "$SECRET_PATH")"

Faça um novo teste:

$ docker attach teste_docker-alpine-1
/app $ echo $DB_PASSWORD
/app $