How to Move Secrets to HashiCorp Vault (KV v2)
Secrets in .env files end up in git history, CI logs, and container images - all readable by anyone with repo access. Moving them into HashiCorp Vault's KV v2 store takes three steps: write the secrets to Vault, fetch them from the HTTP API on startup, and delete the .env file.
Why .env files are a problem
A .env file checked into a repo - or baked into a container image - is a standing
credential leak. gitleaks, GitHub secret scanning, and any attacker with a copy of
the image can read every value instantly. Vault keeps secrets encrypted at rest,
auditable, and rotatable with no impact on running code.
Step 1: write the secrets to Vault KV v2
Vault's KV v2 secrets engine is pre-mounted at secret/. Use the CLI:
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=dev
vault kv put secret/app \
API_KEY=supersecret \
DB_PASSWORD=hunter2 \
JWT_SECRET=changeme
Or write them via the HTTP API directly (useful in scripts or CI):
curl -sS -X PUT \
-H "X-Vault-Token: dev" \
-H "Content-Type: application/json" \
-d '{"data": {"API_KEY": "supersecret", "DB_PASSWORD": "hunter2", "JWT_SECRET": "changeme"}}' \
http://127.0.0.1:8200/v1/secret/data/app
Verify the write:
vault kv get secret/app
Step 2: fetch secrets from Vault on startup
Replace the .env read with a Vault HTTP call at application startup. KV v2 nests
the actual values under data.data:
import requests
def load_secrets():
r = requests.get(
"http://127.0.0.1:8200/v1/secret/data/app",
headers={"X-Vault-Token": "dev"},
)
r.raise_for_status()
return r.json()["data"]["data"]
secrets = load_secrets()
API_KEY = secrets["API_KEY"]
DB_PASSWORD = secrets["DB_PASSWORD"]
JWT_SECRET = secrets["JWT_SECRET"]
The double ["data"]["data"] is intentional - KV v2 wraps the payload in a
metadata envelope, so the secrets themselves live one level deeper than KV v1.
Step 3: delete the .env file
Once the app loads secrets from Vault, the .env is redundant and dangerous:
rm /workspace/.env
Confirm it is gone before committing - the next git status should show it
as deleted, not modified. If it was previously tracked, git rm --cached .env
removes it from the index without overwriting .gitignore.
Production differences
The dev-server pattern above (root token, no TLS) is fine for a sandbox. In production the key differences are:
- Auth method - use AppRole or Kubernetes auth (a projected ServiceAccount token) instead of a static root token; each service gets a policy-scoped token.
- Dynamic secrets - Vault can generate short-lived database credentials per request via the database secrets engine, so credentials rotate automatically.
- Sidecar agent -
vault-agentor the CSI secrets store driver writes secrets as environment variables or files, so application code never speaks the Vault API. - Audit logging - every read and write logged to a file or Splunk; you know exactly who fetched which secret and when.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Writing secrets to Vault KV v2 via the CLI and the HTTP API
- Fetching secrets from Vault in Python with the
requestslibrary - Removing a .env file from disk and git history after migrating to Vault
FAQ
How do I store secrets in HashiCorp Vault KV v2?
Use vault kv put secret/<path> KEY=value ... (CLI) or PUT to /v1/secret/data/<path> with a JSON body of {"data": {"KEY": "value"}} via the HTTP API. KV v2 versions every write automatically.
How do I read Vault secrets in Python?
Call GET /v1/secret/data/<path> with the X-Vault-Token header, then extract response.json()["data"]["data"] - KV v2 wraps the payload in a metadata envelope, so the secrets are one level deeper than you might expect.
What should I do with the .env file after migrating to Vault?
Delete it from disk (rm .env) and remove it from git tracking (git rm --cached .env), then add .env to .gitignore. Even if the file is empty after migration, leaving it on disk risks it being repopulated or committed by accident.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →