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.

DevOps Engineervaultsecretssecurity

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:

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

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

Fix the Nginx 502 Bad GatewayDevOps projectRecover a Crashed Linux ServiceDevOps projectCorrect a Postgres Port MismatchDevOps projectDevOps roadmapStep by step to hiredDevOps interview questionsSTAR answersAll DevOps projectsProjects hub

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 →