Every application has secrets: database passwords, API keys, SMTP credentials, encryption keys, OAuth client secrets. Leaked secrets are among the most common causes of breaches. They get committed to Git, left in world-readable files, printed in logs or baked into Docker images. Here’s how to handle them properly on your own servers, from simple setups to dedicated tools.
Rule 1: never commit secrets to Git
Even in private repositories, secrets in Git history spread to every clone, CI system and backup, and they stay in the history even after you delete them.
# .gitignore
.env
.env.*
!.env.example
*.pem
*.key
Commit a .env.example with placeholder values instead. Add a secret scanner (gitleaks, trufflehog, or GitHub’s built-in push protection) to catch mistakes before they’re pushed.
If a secret was committed: rotate it immediately. Rewriting history (git filter-repo) is a cleanup step, not a fix, because the secret must be assumed exposed.
Rule 2: lock down .env files on the server
The .env file approach is fine for small deployments, if the file is protected:
sudo chown root:myapp /srv/myapp/.env
sudo chmod 640 /srv/myapp/.env # readable by root and the app's group only
- Keep it outside the web root. A
.envinsidepublic/can be downloaded. Bots scan for/.envconstantly. See Fail2Ban for Nginx. - Block dotfiles in Nginx as a safety net:
location ~ /\.(?!well-known) { deny all; }
- Use different secrets per environment (dev, staging, production).
Rule 3: don’t leak through the side doors
- Process arguments are public: anything passed on the command line (
mysql -pSecret,--api-key=…) is visible inpsto all users, and lands in shell history. Use config files, environment files or prompts. See history. - Logs: don’t log full request bodies, headers (
Authorization) or config dumps. Scrub error reports (Sentry and similar have scrubbing options). - Docker images: never
COPY .envorARGsecrets into an image. Anyone with the image can read every layer. Pass secrets at runtime instead. - Debug pages: framework debug modes (
APP_DEBUG=true, DjangoDEBUG=True) can print environment variables to visitors. Always disable them in production. - Backups: your backups contain your secrets. Encrypt them. See GPG encryption.
Better options for services
systemd: EnvironmentFile and credentials
[Service]
EnvironmentFile=/etc/myapp/secrets.env # root:root 600 is fine; systemd reads it as root
Newer systemd versions offer credentials, which are passed to the service as files in a private directory, not as environment variables, and can be encrypted on disk:
[Service]
LoadCredential=db_password:/etc/myapp/db_password
# the app reads $CREDENTIALS_DIRECTORY/db_password
systemd-creds encrypt can encrypt them, bound to the machine (and optionally its TPM). See systemd unit files.
Docker Compose secrets
services:
app:
image: myapp
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Secrets are mounted as files under /run/secrets/. Many official images (PostgreSQL, MySQL) support *_FILE variables. See Docker environment variables.
Encrypted secrets in Git: SOPS / age
SOPS encrypts the values in YAML, JSON or .env files with age or GPG keys, so encrypted config can live in Git and is decrypted at deploy time:
sops --encrypt --age age1… secrets.yaml > secrets.enc.yaml
sops --decrypt secrets.enc.yaml > /etc/myapp/secrets.yaml
Ansible Vault does the same for Ansible projects. See Ansible playbooks.
Secret managers
For teams and many services: HashiCorp Vault / OpenBao, Infisical, Doppler, or cloud secret managers. They provide central storage, access policies, audit logs and dynamic secrets (short-lived database credentials). They add operational complexity, which is worth it at scale, but often overkill for one VPS.
Rotation
Plan for rotating secrets before you need to:
- Use separate credentials per app and per environment, so one rotation doesn’t break everything.
- Database users per application, with only the privileges they need. See MySQL users and privileges.
- Keep a list of where each secret is used.
- Rotate on staff changes, on suspected leaks, and periodically for high-value keys.
When a secret leaks
- Revoke or rotate it at the source (API provider, database) immediately.
- Deploy the new secret.
- Check the provider’s logs for misuse during the exposure window.
- Remove the leak source (Git history, logs, public files), and add a scanner or other control to prevent a repeat.
Quick reference
| Do | Don’t |
|---|---|
.env outside the web root, chmod 640 |
Commit .env to Git |
| Secrets via files or credentials at runtime | Bake secrets into Docker images |
| Separate secrets per environment | Pass secrets as command-line arguments |
| Encrypt backups | Log request headers or full configs |
| Rotate after leaks and staff changes | Leave debug mode on in production |
Related: Linux server hardening and GitHub Actions deploy.