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 -lA correção:
sudo chown root:meuapp /etc/meuapp/env
sudo chmod 640 /etc/meuapp/envDono 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/envO 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-senhaA 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/envO 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 | headO 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/envO 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 cidocker 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
roote permissão640. - Entregue pelo systemd com
EnvironmentFile, ouLoadCredentialquando 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.