>_ josetilabs.com
← blog

25 Oct 2026 · linux, seguranca · 4 min

How do you store a secret in an environment variable without leaking it?

A secret in code or in a log is already a lost secret. A .env file solves storage, but only if the file mode and the .gitignore rule are right. The script below creates the file with mode 600, generates the .gitignore and tests whether the value shows up in a log. See also cron done right for how cron inherits (or not) environment variables, and containers without root for running the service without privilege.

What leaks and why

Three paths usually leak a secret: the .env file enters git because nobody added the ignore rule; the application log prints the whole variable during debugging; and the secret sits in a shell variable exported by a script running as root and read by another user. The first is the most common and the cheapest to prevent.

The Twelve-Factor rule helps frame it: configuration varies between deploys, code does not. A secret is configuration, so it does not belong in the repository. A .env outside git with mode 600 covers the single-host case. For several hosts or frequent rotation, a secret manager (Vault, SOPS, the provider’s vault) starts to earn its cost.

The artifact

```bash

!/usr/bin/env bash

set -euo pipefail

DEMO=$(mktemp -d) cd “$DEMO”

1. arquivo de segredos com permissão restrita

umask 077 cat > .env <<’ENV’ APP_SECRET=sk_live_9f8e7d6c5b4a3210 DB_PASSWORD=hunter2-nao-use-isso ENV chmod 600 .env

echo “=== permissão do .env ===” stat -c ‘%a %U:%G %n’ .env

2. .gitignore precisa cobrir .env

if [ ! -f .gitignore ] || ! grep -qxF ‘.env’ .gitignore; then printf ‘%s\n’ ‘.env’ ‘.env.*’ ‘!.env.example’ >> .gitignore fi echo “=== .gitignore ===” cat .gitignore

3. leitura mascarada: nunca imprime o segredo inteiro

mask() { local v=”$1” if [ “${#v}” -le 4 ]; then printf ‘****’ else printf ‘%s…%s’ “${v:0:2}” “${v: -2}” fi }

set -a

shellcheck disable=SC1091

. ./.env set +a

echo “=== variáveis carregadas (mascaradas) ===” printf ‘APP_SECRET=%s\n’ “$(mask “$APP_SECRET”)” printf ‘DB_PASSWORD=%s\n’ “$(mask “$DB_PASSWORD”)”

4. teste de vazamento em log: o log nunca pode conter o valor bruto

LOG=”$DEMO/run.log” { printf ‘inicio %s\n’ “$(date -Is)” printf ‘APP_SECRET=%s\n’ “$(mask “$APP_SECRET”)” printf ‘fim\n’ } > “$LOG”

echo “=== teste de vazamento ===” if grep -Fq “$APP_SECRET” “$LOG” || grep -Fq “$DB_PASSWORD” “$LOG”; then echo “FALHA: segredo encontrado no log” exit 1 else echo “ok: nenhum segredo bruto no log” fi

5. exemplo de unit systemd (não executa aqui; é o que o leitor cola)

echo “=== unit de exemplo (EnvironmentFile) ===” cat <<’UNIT’ [Service] EnvironmentFile=/etc/meu-app/.env ExecStart=/usr/local/bin/meu-app UNIT

echo “=== pronto ===”```

The script sets umask 077 before creating .env, so the file is born with mode 600 even if the shell umask is relaxed. Reading is masked: the mask function shows only the first two and last two characters. The final test searches for the raw value in the log and fails if it finds it.

How to verify

Run bash env.sh. Expected output includes the line 600 in the stat output and ok: nenhum segredo bruto no log. In a real repository, also confirm with git status --porcelain --ignored, which should list !! .env.

Terminal showing chmod 600 on .env, stat with mode 600 and leak test passing
Mode 600 on .env and the leak test passing.

If it goes wrong

.env, shell or vault

Approach When to use Limit
.env file with mode 600 One host, one service, stable secret Manual rotation, no audit
Variable in shell or crontab Single ephemeral command Stays in history and in ps
Secret manager Several hosts or automatic rotation One more dependency to operate

FAQ

Can I commit a .env.example? Yes, as long as it contains only variable names and example values without a secret. The script adds !.env.example for that.

Does systemd read .env directly? Yes, with EnvironmentFile=/etc/meu-app/.env. The example line in the script shows the unit. systemd ignores lines starting with # and requires KEY=VALUE without export.

Is chmod 600 enough? For a host with a single service user, yes. If several users need to read, use a dedicated group with mode 640 and a service group, not 644.

How to rotate without stopping the service? Update .env and reload the service (systemctl reload or the app’s mechanism). If the app reads the file only at boot, a restart is needed. Keep the .env outside the deployment artifact so a rollback does not restore an old secret.

Is it worth encrypting .env in git? For a small team, no. If you need to version a secret, use SOPS or git-crypt, not the plain file. Rotate through your deploy pipeline, not by hand.

Sources

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.