Secret rotation is one of those jobs everyone agrees matters and almost nobody enjoys. A key leaks, or a quarterly policy says it's time, and suddenly you're editing five .env files by hand, pasting the new value into staging, then prod, then wondering if local dev ever got updated. Miss one and you get a 2am "staging can't connect to the database" page.
Envault is a local CLI that rotates a secret and rolls it out across environments in one command, with an audit log so you can prove it happened. It runs offline on the free tier, has no telemetry, and the installed command is rh-envault. Here's the workflow.
# from source (not on public PyPI)
pip install git+https://github.com/Coding-Dev-Tools/envault.git
Python 3.10+. MIT licensed.
Give it a key name and Envault generates a fresh cryptographically random value:
# rotate the production database password
rh-envault rotate DB_PASSWORD --env prod
# rotate an API key for a specific environment
rh-envault rotate STRIPE_SECRET_KEY --env staging
# preview the new value without writing it yet
rh-envault rotate JWT_SECRET --length 64 --dry-run --show
The --dry-run --show combo is what I reach for first: it prints the generated value so I can confirm it looks right before anything on disk changes.
You don't tell it what kind of value to make. It reads the key name and picks a format that won't break the thing consuming it:
| Key pattern | Generated value |
|---|---|
| DB_PASSWORD, DATABASE_URL | database-safe password (no ambiguous chars) |
| API_KEY, STRIPE_SECRET | prefixed API key |
| JWT_SECRET | 256-bit base64 secret |
| WEBHOOK_SECRET | long hex key |
| anything else | 32-character random string |
That last point matters more than it sounds. A raw random string can land an @ or / in a URL-shaped secret and quietly break a connection string. Envault avoids that for known patterns.
When a root credential leaks or a policy forces a bulk reset, rotate-all walks every key in an environment and regenerates each one, with a per-key dry-run preview first:
rh-envault rotate-all --env prod --dry-run
rh-envault rotate-all --env prod
Rotation updates the source environment. To push it to the others without clobbering their own secrets:
# copy the rotated value from prod into staging, keep staging's other secrets
rh-envault sync prod staging --strategy source_wins
# confirm the two environments actually match now
rh-envault diff prod staging --fail-on-missing
source_wins means the source environment's values take precedence on conflict, so the rotated value lands in the target. Keys that only exist in the target are left alone — you don't accidentally flatten an environment's unique credentials.
Every rotate, diff, and sync is written to .envault-audit.log. When someone asks "who changed the database password and when," you don't grep git history:
rh-envault audit --action rotate --limit 20
rh-envault audit --key DB_PASSWORD
The cheapest way to stop drift is to fail the build before a bad deploy lands. Add a parity check that blocks promotion when production is missing a key staging has:
# .github/workflows/env-check.yml (excerpt)
- name: Check parity with staging
run: rh-envault diff staging prod --fail-on-missing
- name: Audit before deploy
run: rh-envault audit --action rotate --limit 20
The rotate/audit/diff/sync loop is free and unlimited on the local CLI. Secret-store integrations (AWS SSM, HashiCorp Vault, Doppler, 1Password), unlimited audit history, team shared configs, and compliance exports unlock at paid tiers: Individual $12/mo, the 11-tool Suite $49/mo, Team $79/mo. Enterprise is custom.
pip install git+https://github.com/Coding-Dev-Tools/envault.git
rh-envault init my-project
rh-envault rotate DB_PASSWORD --env prod
MIT. No telemetry. No account required for the free tier.