SecurityAugust 4, 20265 min read

How to Manage Secrets and Environment Variables on a VPS

Hardcoded API keys and passwords in code are a breach waiting to happen. Learn to store secrets in env files, systemd, and vaults โ€” the right way.

NBy Nxeon

Every app needs secrets โ€” database passwords, API keys, tokens. Where you put them determines whether a small mistake becomes a breach. Hardcoding them in source, committing .env files to git, or leaving them world-readable are the classic ways to leak credentials. This guide covers safer patterns for a VPS.

The golden rule: separate secrets from code

Config that changes between environments โ€” and especially secrets โ€” should live *outside* your codebase, injected at runtime. This is the core of the twelve-factor approach. It means you can rotate a key without a code change, and your git history never contains credentials.

Use environment variables

The simplest mechanism is environment variables. Set one for a session:

export DATABASE_URL="postgres://appuser:secret@localhost/myapp"

Your app reads it (process.env.DATABASE_URL, os.environ, etc.). But exported shell variables don't persist and can leak into process listings, so for real services use the patterns below.

Store secrets in a locked-down .env file

Keep secrets in a file readable only by the app's user:

sudo touch /etc/myapp/env
sudo chown appuser:appuser /etc/myapp/env
sudo chmod 600 /etc/myapp/env

Permissions matter more than location โ€” chmod 600 means only the owner can read it. Populate it as KEY=value lines.

{{SCREENSHOT}}

Inject secrets with systemd

If your app runs as a systemd service, load the env file directly โ€” no secrets in the unit file or in ps output:

[Service]
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myapp
User=appuser

This is clean, persistent, and integrates with the service's lifecycle. See health checks and auto-restart for the rest of a solid systemd unit.

Never commit secrets to git

Add secret files to .gitignore *before* your first commit:

.env
*.pem
/config/secrets.yml

If a secret ever lands in git history, rotate it immediately โ€” deleting the file doesn't remove it from history, and assume it's compromised. Scanning tools like git-secrets or trufflehog catch leaks before they're pushed.

Keep secrets out of container images

Baking secrets into a Docker image embeds them in layers anyone can extract. Pass them at runtime instead:

docker run --env-file /etc/myapp/env myimage

More on this in secure Docker containers.

When to use a secrets manager

For a single VPS with a few services, locked-down env files and systemd are perfectly adequate. As you grow to multiple servers, rotation policies, and audit requirements, a dedicated secrets manager (HashiCorp Vault, cloud KMS, SOPS-encrypted files) pays off. Don't over-engineer early โ€” a chmod 600 env file beats a misconfigured vault.

Rotate and audit

Secrets should be rotatable. Because they're injected at runtime, rotating a key is: update the env file, restart the service. Keep an eye on who can read them โ€” anyone with root or sudo can, so guard admin access with SSH keys and 2FA.

Audit for secrets already leaking

Before improving how you store secrets, find where they're currently exposed. A quick sweep of a project catches the obvious cases:

grep -rEn '(password|secret|api[_-]?key|token)\s*[=:]' . --exclude-dir=node_modules

For git history specifically โ€” where deleting a file doesn't remove the secret โ€” dedicated scanners are worth running:

# scan the working tree and history for high-entropy strings and known patterns
trufflehog git file://.

Anything these turn up should be treated as compromised and rotated, not just deleted. It's common to discover a database password committed years ago that's still valid; rotating it is the only safe response.

Separate config per environment

A clean pattern that scales well is one env file per environment, none of them in git. Your repository holds a .env.example listing the *names* of required variables with placeholder values, so a new developer knows what to provide, while the real values live only in each machine's locked-down env file:

# .env.example  (committed, no real values)
DATABASE_URL=
STRIPE_SECRET_KEY=
JWT_SIGNING_KEY=

This gives you self-documenting configuration without ever exposing a real secret. Pair it with a startup check that fails loudly if a required variable is missing, so a misconfigured deploy stops immediately instead of running in a half-broken state. As you grow beyond one server, the same principle โ€” names in code, values injected at runtime โ€” carries straight over to a secrets manager, so adopting the discipline early makes that later migration painless.

FAQ

Are environment variables secure?

They're a reasonable mechanism if the source (an env file) has tight permissions and isn't committed to git. Avoid export-ing secrets in shared shells, where they can appear in process listings.

Should I encrypt my .env file?

On a single trusted VPS, chmod 600 is usually enough since root can read anything anyway. Encryption (e.g. SOPS) matters when secrets are stored in git or synced across machines.

What do I do if I committed a secret?

Rotate it immediately โ€” assume it's compromised. Removing it from the latest commit isn't enough; it stays in history. Then add it to .gitignore.

Do I need Vault for one server?

No. Locked-down env files plus systemd EnvironmentFile are fine for a single VPS. Reach for a secrets manager when scale, rotation, and auditing demand it.

Secrets management checklist

Good secrets hygiene on a single VPS comes down to a few consistent habits:

  • Keep secrets out of source code โ€” inject them at runtime, never hardcode.
  • Store them in a locked-down env file (chmod 600, owned by the app user).
  • Load them via systemd EnvironmentFile so they're not in unit files or ps output.
  • Add secret files to .gitignore before the first commit, and commit a .env.example with names only.
  • Rotate on leak โ€” if a secret hits git history, assume it's compromised and change it.
  • Keep secrets out of container images; pass them with --env-file.
  • Scan with tools like trufflehog to catch anything already exposed.

For one server, that's genuinely enough โ€” a chmod 600 env file plus systemd beats a misconfigured vault every time. Reach for a dedicated secrets manager only when scale, rotation policies, and audit requirements demand it. The core discipline โ€” names in code, values injected at runtime โ€” stays the same as you grow, so building the habit now makes any future migration painless.

Handled well, secrets management is mostly discipline: keep them out of code, lock down permissions, and rotate on leak. Build on a VPS made for developers โ€” see Nxeon for developers and the VPS overview.

#secrets#environment-variables#devops#security#vps#seobatch

Deploy your first server in under a minute

Creating an account is free and takes no card details. You pay when you deploy โ€” choose a billing term and pay from your wallet or by card at checkout.