vercel-rotate
v0.5.2-phase5
Published
Rotate credentials across Vercel projects after the April 2026 breach
Readme
vercel-rotate
Rotate credentials across your Vercel projects after the April 2026 breach.
Detects secrets in Vercel env vars, rotates them with the provider that issued them, propagates the new value to every other place it lives (Render, Fly.io, Railway, Netlify, GitHub Actions), and records an audit trail. Value-hash deduplication means one credential used in forty projects rotates once at the provider and propagates forty times.
Install
npm i -g vercel-rotate
vercel-rotate initNode 20+. The first-run wizard prompts for a Vercel API token and, optionally, for each supported sink (Render, Fly.io, Railway, Netlify, GitHub Actions). Every token lands in your OS keychain; nothing is written to disk in plaintext.
Use
# Preview what will rotate (writes a plan file)
vercel-rotate plan
# Review the plan
cat ~/.config/vercel-rotate/runs/<timestamp>/plan.md
# Execute
vercel-rotate apply
# If interrupted
vercel-rotate resume
# Past runs
vercel-rotate audit --since 2026-04-20Commands
| Command | What it does |
|---|---|
| init | Prompt for provider + sink tokens, store in OS keychain |
| plan | Inventory Vercel env vars, dedup by value-hash, detect providers, write plan |
| apply | Execute the plan: rotate → propagate → delete old |
| resume | Continue the most recent incomplete run |
| audit | Query run history (--since, --run-id, --provider, --json) |
| scaffold provider \| sink <id> | Generate a new adapter from the built-in template |
Useful flags
plan
--verify— call each adapter's liveness check; flag dead/stale credentials--label— interactively label unknown secrets; answers persist to~/.config/vercel-rotate/learned-labels.json--forget ENV_VAR— remove one learned label (repeatable)--forget-all --yes— wipe all learned labels (non-TTY needs--yes)--labels-file PATH— override learned-labels location
apply
--dry-run— show what would happen; make no API writes--include-risky— include risky secrets (DB, JWT, OAuth)--i-understand-this-causes-downtime— required alongside--include-risky--halt-on-first-risky-failure— stop the risky queue after first failure (default: continue)--new-values-file PATH— for oauth-paste flow: JSON file of operator-supplied new values--oauth-console-url URL— override the console URL printed during oauth-paste flow
Providers
| Provider | keyTypes | Rotation |
|---|---|---|
| OpenAI | admin-api-key | Full (create + delete-old) |
| Anthropic | admin-api-key | Full |
| Stripe | restricted-key (sk/rk) | Full for rk_*; sk_* routed to manual |
| Resend | api-key | Full |
| Twilio | api-key (SK-prefix) | Full; auth-tokens route to manual |
| AWS IAM | access-key | Full (inline sigv4). Refuses root. Refuses if user already has 2 keys. |
| Supabase | anon-jwt, service-jwt, mgmt-token, db-password | Detection + routing |
| Supabase (DB password) | db-password | Risky. Snapshot + rollback; causes ~60s downtime. |
| Supabase (JWT signing key) | service-jwt | Risky. Signing-keys API; HS256 generated client-side; rollback restores prior key. |
| GitHub | 6 variants (PAT, OAuth app secret, installation token, ...) | Detection + routing to manual / oauth-paste |
| OAuth paste flow | oauth-client-secret | Risky. Operator rotates in provider console, pastes new value into the CLI. |
Sinks
| Sink | Surface | |---|---| | Vercel | Env vars + redeploy coalescing (default sink; always enabled) | | Render | Services + env vars + deploys | | Fly.io | Apps + secrets (implicit deploy) | | Railway | GraphQL variables + redeploy | | Netlify | Site + account env + builds | | GitHub Actions | Repo secrets, org secrets, and environment secrets (libsodium-encrypted) |
How it works
- Reads env vars across all your Vercel projects (production, preview, development) — scoped to the admin-token's team
- Deduplicates by value-hash — one OpenAI key in forty projects rotates once, propagates forty times
- Detects the provider from value shape + keyName; unknowns can be labeled interactively
- Calls each provider's rotation API to create a new credential alongside the old one
- Updates every Vercel env occurrence with the new value; writes to every other sink that holds the same value
- Triggers redeploys per sink (coalesced per run), waits for each to succeed
- Only after all dependents are green: deletes the old credential at the provider (where supported)
For risky rotations (DB passwords, JWT secrets, OAuth client secrets), the flow adds a snapshot → rotate → rollback-on-fail lifecycle: the provider captures pre-rotation state, the orchestrator encrypts it with AES-256-GCM (scrypt-derived key, 24h TTL), and rolls back automatically if rotation or propagation fails. Risky rotations run serially, optionally spaced apart via plan.flags.riskySpacingSeconds.
Safety
- All admin tokens stored in OS keychain (macOS Keychain, libsecret, etc.) — never on disk
- Raw secret values never written to the state database — only value-hashes
- Snapshot payloads encrypted at rest with authenticated encryption (GCM auth tag + scrypt-derived key)
- Old credential is never deleted until every sink has been redeployed on the new value
applyasks for a typed "YES" confirmation before executing a non-dry-run plan- Risky paths require two flags to opt in (
--include-risky+--i-understand-this-causes-downtime) - AWS IAM provider refuses to rotate root credentials and refuses if the target user already has the IAM-maximum two access keys
- Per-run audit log at
~/.config/vercel-rotate/runs/<runId>/state.db
Known limitations
- No live-API verification in this release. Adapters are covered by 461 mocked-HTTP tests (nock). No rotation has yet fired against a real OpenAI / Resend / Twilio / AWS / Anthropic / Stripe / Supabase endpoint. If you run
applyagainst production credentials, you are the first integration test. Start with a throwaway account. - GitHub Actions secrets are write-only through the API — verify updates via the secret's
updated_attimestamp in the GitHub UI. - The OAuth paste flow requires an interactive TTY by design; CI environments surface a clean error.
- Stripe live secret keys (
sk_live_*/sk_test_*) cannot be rotated via API; they're routed to the manual bucket with a console link.
Docs
ADAPTERS.md— authoring contract for providers + sinksCONTRIBUTING.md— PR flow, coding standardsSECURITY.md— security policy and vulnerability disclosureCHANGELOG.md— release historydocs/lifecycle.html— visual product lifecycle
License
MIT — see LICENSE.
