← Voltar para o Blog

Onde guardar segredo no servidor: o .env e as alternativas

A senha do banco está num .env com permissão 644, versionada por engano em 2023 e replicada em quatro máquinas. Este é o caminho do arranjo bagunçado até algo defensável, sem virar projeto.

Equipe EasyOps Cloud · · 7 min de leitura

Em quase todo servidor existe um arquivo .env com a senha do banco, a chave da API de pagamento e o token de algum serviço. Ele costuma ter permissão 644, o que significa que qualquer usuário da máquina consegue lê-lo.

Com frequência ele também já foi versionado por engano em algum momento, está replicado em quatro servidores com valores ligeiramente diferentes, e ninguém sabe qual está correto.

Não é preciso montar um cofre corporativo para melhorar isso. Alguns ajustes elevam bastante o patamar, e cabem em uma tarde.

O básico que quase sempre está errado

Comece pelo que existe. Segredo só deve ser legível por quem precisa:

# Quem consegue ler os seus arquivos de ambiente
sudo find /var/www /etc /opt -name '.env' -o -name 'env' 2>/dev/null | \
  xargs -r ls -l

A correção:

sudo chown root:meuapp /etc/meuapp/env
sudo chmod 640 /etc/meuapp/env

Dono root, grupo do serviço, e ninguém mais. A aplicação lê porque pertence ao grupo; o usuário que fez deploy não lê, e nenhum outro serviço da máquina lê.

Tire o arquivo de dentro do diretório servido pela web. Um .env em /var/www/meuapp/.env está a uma configuração errada de nginx de ser baixável pela internet — acidente que acontece com regularidade. /etc/meuapp/env não tem esse risco.

E confirme que ele não está no versionamento:

git ls-files | grep -E '\.env|secret|credential'

Se aparecer alguma coisa, remover do próximo commit não resolve — o valor continua no histórico e precisa ser considerado comprometido. A única ação correta é rotacionar a credencial.

Variável de ambiente vaza mais do que se imagina

A recomendação usual é passar segredo por variável de ambiente. Ela é melhor que arquivo no repositório, e tem fragilidades que vale conhecer.

O ambiente de um processo é legível pelo dono do processo e por root:

sudo cat /proc/$(pgrep -f meuapp | head -1)/environ | tr '\0' '\n'

Além disso, variáveis costumam aparecer em lugares inesperados: página de erro de framework em modo de depuração, relatório de exceção enviado a serviço externo, saída de comando de diagnóstico colada num chamado, e log de container.

Duas consequências práticas. A primeira é preferir arquivo com permissão restrita a variável exportada globalmente, quando a aplicação suportar ler de arquivo. A segunda é garantir que o modo de depuração esteja desligado em produção — é por ele que a maior parte dos vazamentos desse tipo acontece.

Entregando ao serviço pelo systemd

O jeito limpo de conectar o arquivo à aplicação:

# /etc/systemd/system/meuapp.service.d/env.conf
[Service]
EnvironmentFile=/etc/meuapp/env

O systemd lê o arquivo como root e repassa ao processo. O arquivo pode ficar inacessível ao usuário da aplicação, o que é um ganho: nem o próprio serviço consegue reler o arquivo depois de iniciado.

Para segredo que não deve nem transitar por variável, versões recentes do systemd oferecem entrega por arquivo em memória:

[Service]
LoadCredential=db-senha:/etc/meuapp/db-senha

A aplicação lê o caminho indicado em $CREDENTIALS_DIRECTORY, que existe apenas para aquele processo e some quando ele termina. Não aparece em /proc/*/environ nem é herdado por processo filho.

Nunca coloque o valor direto na unit com Environment=SENHA=... — o arquivo da unit é legível por todos, e systemctl show exibe o conteúdo para qualquer usuário.

Um lugar só, várias máquinas

O problema seguinte é a duplicação. Quatro servidores com quatro cópias do mesmo arquivo levam à situação em que ninguém sabe qual é a verdade.

Para poucas máquinas, um repositório privado de configuração com os valores criptografados resolve bem e sem infraestrutura nova:

# Criptografar com uma chave que só existe fora do repositório
age -r age1exemplo... -o env.age /etc/meuapp/env

# No servidor, na hora do deploy
age -d -i /root/.age-key env.age | \
  sudo install -m 640 -o root -g meuapp /dev/stdin /etc/meuapp/env

O repositório guarda o arquivo cifrado, com histórico de quem mudou o quê. A chave de decifragem fica no servidor, protegida por permissão. Ferramentas como sops ou ansible-vault implementam a mesma ideia com mais recursos.

O gerenciador de segredos dedicado — com política de acesso, auditoria e emissão de credencial temporária — é o passo seguinte, e faz sentido a partir de certa escala. Para três servidores e uma equipe pequena, ele costuma custar mais operação do que resolve.

Rotação sem downtime

Segredo que nunca muda é segredo que já vazou e ninguém sabe. Rotacionar exige um detalhe de ordem que evita indisponibilidade.

O erro é trocar a senha no banco e depois atualizar a aplicação — entre os dois passos, o serviço fica fora.

A sequência correta cria a credencial nova antes de remover a antiga:

-- 1. Nova credencial, com as mesmas permissões
CREATE USER app_v2 WITH PASSWORD 'nova-senha-longa';
GRANT ALL PRIVILEGES ON DATABASE app TO app_v2;
# 2. Atualizar o arquivo e recarregar
sudo install -m 640 -o root -g meuapp novo-env /etc/meuapp/env
sudo systemctl restart meuapp

# 3. Confirmar que a aplicação está usando a nova
sudo -u postgres psql -c \
  "SELECT usename, count(*) FROM pg_stat_activity GROUP BY 1;"
-- 4. Só depois, remover a antiga
DROP USER app_v1;

Vale rotacionar por evento, não só por calendário: saída de pessoa da equipe, suspeita de exposição, ou credencial que apareceu em log ou em chamado.

Descobrir o que já vazou

Antes de organizar o futuro, vale olhar para trás:

# Segredos no histórico do git
git log --all -p | grep -inE '(password|secret|api[_-]?key|token)\s*[=:]' | head -30

# Em log da aplicação
sudo grep -rinE '(password|token|secret)=[^ ]+' /var/log/ 2>/dev/null | head

# Em histórico de shell — muito comum
grep -inE '(mysql|psql|curl).*(-p|password|token)' ~/.bash_history 2>/dev/null | head

O histórico de shell é o esconderijo mais subestimado. Uma senha passada na linha de comando fica no arquivo, e também aparece em ps para qualquer usuário durante a execução do comando.

Para evitar isso, use variável de ambiente lida de arquivo, ou o mecanismo próprio da ferramenta — ~/.pgpass no Postgres, --password-stdin no Docker.

Segredo em container

Container adiciona um caminho de vazamento que não existe em aplicação comum: a imagem.

Um ARG ou ENV com valor sensível no Dockerfile fica gravado nas camadas da imagem. Quem tiver acesso a ela recupera o valor, mesmo que uma camada posterior tenha "apagado" o arquivo:

docker history --no-trunc minha-imagem | grep -i -E 'token|senha|key'

Isso vale inclusive para imagem em registro privado — acesso de leitura é suficiente.

O caminho correto é injetar em tempo de execução, nunca em tempo de construção:

services:
  api:
    image: minha-api:1.4
    env_file: /etc/meuapp/env

O env_file do compose lê o arquivo do host na hora de subir; o conteúdo não entra na imagem. Mantenha esse arquivo com a permissão restrita descrita acima, e fora do diretório do projeto para que um docker build não o capture no contexto.

Quando for preciso um segredo durante a construção — token para baixar dependência privada, por exemplo —, existe o mecanismo próprio, que monta o valor apenas para aquele comando e não o registra em camada nenhuma:

RUN --mount=type=secret,id=npmtoken \
    NPM_TOKEN=$(cat /run/secrets/npmtoken) npm ci
docker build --secret id=npmtoken,src=./token.txt .

E acrescente um .dockerignore com .env, .git e chaves — pelo mesmo motivo do --exclude no rsync: o que não deve viajar não deve nem entrar no contexto.

O arranjo defensável

  • Arquivo fora do diretório web, com dono root e permissão 640.
  • Entregue pelo systemd com EnvironmentFile, ou LoadCredential quando disponível.
  • Nunca no versionamento em texto claro; cifrado, se precisar versionar.
  • Modo de depuração desligado em produção.
  • Rotação com credencial nova antes de remover a antiga.
  • Uma revisão semestral procurando o que vazou para log e histórico.

Isso não é um cofre, e não pretende ser. É o suficiente para que um usuário comum da máquina não leia a senha do banco, para que um git clone não entregue as chaves, e para que a troca de credencial seja uma operação de rotina — o que já resolve a maior parte do risco real. O controle de quem tem acesso à máquina em primeiro lugar, que é a camada anterior a esta, está no artigo sobre usuários, grupos e sudo.

Vale uma última consideração sobre prioridade. Entre todas as medidas acima, duas respondem pela maior parte do risco real: o arquivo com permissão restrita e fora do diretório web, e a certeza de que nada sensível está no histórico do versionamento. Se o tempo disponível for de uma hora, gaste nessas duas.

O resto — cifragem no repositório, rotação com credencial dupla, entrega por LoadCredential — é refinamento que compensa conforme a equipe e o número de máquinas crescem. Começar pelo refinamento antes do básico é comum e não protege de nada: um cofre bem configurado não ajuda se o .env de produção continua legível por qualquer usuário da máquina.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta