What is it? #
Your application needs configuration that differs per environment and credentials that must not be in source control.
On a VPS the standard mechanism is an environment file on the server, loaded by systemd and passed to the process.
The file needs the right ownership and 600 permissions, and it must never be committed. A committed credential is a credential that has leaked.
Rotation is the part most setups skip. Credentials should be changeable without redeploying code, and must be changed when someone leaves or a key is exposed.
Think of it like this #
Keys kept in a safe in the building rather than printed in the operating manual.
The manual is copied and shared. The safe is opened by exactly the people who need what is inside.
Simple example #
An application needs a database URL, a secret key and an API token. They live in an env file owned by the application user, loaded by systemd, with an example file committed to the repository.
Code #
# /srv/app/.env — owned by the service user, never committed
DATABASE_URL=postgresql://appuser:LONG-RANDOM@localhost/shop
SECRET_KEY=a-long-random-value-generated-once
STRIPE_API_KEY=sk_live_...
LOG_LEVEL=info
PAGE_SIZE=20
sudo chown appuser:appuser /srv/app/.env
sudo chmod 600 /srv/app/.env # only the service user can read it
# systemd loads it and passes it to the process
[Service]
User=appuser
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/srv/app/.venv/bin/gunicorn app.main:app --bind 127.0.0.1:8000
# Applying a change to the env file:
# sudo systemctl restart myapp (a reload does not re-read it)
# .env.example — this one IS committed
DATABASE_URL=postgresql://user:password@localhost/dbname
SECRET_KEY=generate-with-openssl-rand-hex-32
STRIPE_API_KEY=sk_test_xxx
LOG_LEVEL=info
PAGE_SIZE=20
# .gitignore
.env
.env.local
*.pem
*.key
# Generating strong secrets
openssl rand -hex 32 # a 64-character hex string
openssl rand -base64 32
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
# Verify the running process has what you expect (as root)
sudo tr '\0' '\n' < /proc/$(pgrep -f gunicorn | head -1)/environ | grep -v KEY
# Note: this also shows that root can read any process's environment.
Rotation, when it matters
on staff departure rotate anything that person could read
on exposure rotate immediately; deleting the commit is not enough
periodically at least annually for long-lived API keys
The process: add the new credential alongside the old, deploy, verify,
then revoke the old one. Rotating in one step causes an outage.
How it works #
EnvironmentFile reads simple KEY=value lines and passes them to the process. It does not run a shell, so there is no variable expansion and no quoting subtleties — values are taken literally.
Permissions of 600 with the service user as owner mean no other account on the machine can read the credentials. This matters on any server with more than one user.
A restart is required after changing the file. systemctl reload signals the application to re-read its own configuration, but the environment is set when the process starts, so it does not pick up changes.
Committing .env.example documents every variable the application needs without exposing any value. It is what lets a new developer or a rebuilt server know what to set.
Generating secrets with openssl rand produces genuinely random values. Hand-typed secrets are shorter and more predictable than people assume.
The /proc command demonstrates an important limitation: environment variables are readable by root and visible in the process environment. They are a reasonable mechanism, not a strong secret store.
Rotating in two steps — add, deploy, verify, revoke — avoids the window where the old credential is gone and the new one is not yet in use.
Real-world use #
Env files are the default on a VPS because they are simple and work with every runtime. They are adequate for most systems.
Secret managers — Vault, AWS Secrets Manager, and similar — add rotation, audit logging and short-lived credentials. They are worth adopting when the number of secrets or the compliance requirements grow.
Committed secrets are one of the most common real incidents. Scanners watch public repositories continuously, and a leaked cloud key is typically abused within minutes. Removing it from the latest commit does nothing; it must be rotated.
Secrets also leak through logs. Logging a full request payload or an exception with configuration attached puts credentials into a system that many people can read and that is retained for months.
Backups contain secrets too, which is why the backups lesson insists on encrypting them before they leave the machine.
Common mistakes #
- Committing a .env file with real values.
- Leaving the env file world-readable instead of 600.
- Reloading instead of restarting after changing the environment.
- Logging configuration or full payloads, leaking credentials into logs.
- Rotating in one step and causing an outage.
Practice #
Create an env file with a generated secret, set the correct ownership and permissions, and load it through a systemd unit. Commit a matching .env.example and confirm .env is ignored by git. Then change a value and verify a restart is needed for it to take effect.