Skip to main content

Storing Secrets on a Server: HashiCorp Vault and Env Vars

Security · 29.09.2026

What is wrong with passwords in .env and in code

Storing secrets — database passwords, API keys, SSH private keys — often comes down to a single .env file in the project root. The file gets accidentally committed to Git, copied whole onto a backup server, and read with cat by any process with server access. A leak of one file exposes every service at once.

The problem runs deeper than "don't commit .env". A password in a plain text file has no access history: there is no way to know who read it or when, and you cannot revoke access for one employee without changing the password for everyone else. That is exactly what dedicated secrets management systems solve.

Environment variables: the minimum level of protection

Until Vault is deployed, follow basic rules for environment variables:

  • The .env file must have permissions 600 and belong to the service user, not root.
  • Add .env to .gitignore before the first commit, not after a leak.
  • Never pass secrets as command-line arguments — they are visible in ps aux output to every local user.
  • In systemd use EnvironmentFile= with permissions 600 instead of Environment= in the unit — unit files are readable by everyone.
chmod 600 /etc/myapp/.env
chown myapp:myapp /etc/myapp/.env

This reduces the leak risk but does not solve rotation and auditing — for that you need a dedicated secrets service such as HashiCorp Vault.

HashiCorp Vault: how secrets storage works

Vault is a server that stores secrets encrypted and hands them out on request through an API, checking the client's rights. Three key concepts:

  • Secrets engine — a storage module: KV for key-value pairs, Database for temporary MySQL and PostgreSQL accounts, PKI for issuing certificates.
  • Seal and Unseal — Vault starts sealed, with data on disk encrypted by a master key. The server can only be unsealed by collecting 3 of 5 key shares.
  • Lease and TTL — every issued secret lives for a limited time; once the TTL expires it is revoked automatically.

This model replaces static passwords with short-lived tokens and gives a full log: who requested which secret and when.

Installing Vault on a VPS: step by step

An example for Ubuntu 24.04 on a VDS with 2 GB RAM — the minimal configuration for a single node:

curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault

Set the storage backend in /etc/vault.d/vault.hcl — for a single server the file storage backend is enough — and start the service:

sudo systemctl enable --now vault
export VAULT_ADDR='https://127.0.0.1:8200'
vault operator init
vault operator unseal

The vault operator init command outputs 5 unseal keys and one root token — store them in different places, never on this same server. The root token is only needed for initial policy setup, after which it should be revoked.

Dynamic secrets for the database

Instead of one permanent MySQL password, set up the Database secrets engine — Vault will create a temporary account for every request:

vault secrets enable database
vault write database/config/mysql \
    plugin_name=mysql-database-plugin \
    connection_url="{{username}}:{{password}}@tcp(127.0.0.1:3306)/" \
    allowed_roles="app-role" \
    username="vaultadmin" password="StrongRootPass"
vault write database/roles/app-role \
    db_name=mysql \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, UPDATE ON appdb.* TO '{{name}}'@'%';" \
    default_ttl="1h" max_ttl="24h"

The application requests credentials through the API and gets a login with a TTL of 1 hour. After the hour the account is deleted automatically, even if the application forgot about it. Compromising one token does not give permanent access to the database.

Rotation, auditing, and the checklist

Enable an audit device so every secret request is written to a log:

vault audit enable file file_path=/var/log/vault_audit.log

For static secrets, schedule rotation with a cron job that updates the password in the target system at the same time. Grant access policies on a least-privilege basis — a separate policy for every application, not one shared root token.

  • Secrets are not stored in Git and are never passed as command-line arguments.
  • Files with environment variables have permissions 600 and are owned by the service user.
  • Databases use dynamic secrets with a TTL instead of a permanent password.
  • The Vault root token is revoked after setup; day-to-day work uses named tokens with policies.
  • Audit logging is enabled and access to it is restricted to a separate role.

For more on encrypting the files themselves on disk, see the article on GPG encryption of files and backups, and on access rights see the piece on Linux file permissions. The general order for hardening a new server is described in the Linux server security checklist.

← Back to Knowledge Base Ask Support