Skip to content

Prompto-Studio/keyrotate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

keyrotate

Rotate any API key in one command. Update 1Password, GitHub, Supabase, Netlify, Fly.io, and your .env in one shot — verified end-to-end.

🌐 keyrotate.dev

License: MIT Made with Bun macOS Apple Silicon Linux x64 Windows x64 Status: alpha

Stops you from getting halfway through an API-key rotation, breaking production, and forgetting which destinations you already updated.


The pain this solves

Rotating one API key isn't one step — it's six. Take Resend, used both by your backend and a GitHub Action:

  1. Create the new key in the Resend dashboard
  2. Update the secret in GitHub Actions (gh secret set …)
  3. Update the secret in Supabase Edge Functions (supabase secrets set …)
  4. Save the new key in 1Password so you and your team don't lose it
  5. Verify the new key actually works against the upstream API (a typo here = silent production outage)
  6. Revoke the old key in the dashboard

Miss any of those and you've just shipped a half-rotated key. Either the alert path is broken, or the old key is still alive, or your team's vault is out of sync. Every solo founder eventually builds a sloppy bash script for this; we made a clean one.

$ keyrotate rotate resend-alerts

  Provider:     Resend
  Destinations: onepassword, github, supabase
  Create new at: https://resend.com/api-keys

  ✓ 1Password: 1Password ready (vault: Private)
  ✓ GitHub Actions secret: gh ready, repo Prompto-Studio/Prompto-Bot-Img
  ✓ Supabase Edge Function secret: supabase ready, project lubvpbxxwyy…

  ? Paste the new Resend key (hidden): ****

  ▸ Verifying against Resend…
  ✓ Resend authenticated
  ▸ Writing to 1Password (RESEND_API_KEY)…
  ✓ Updated 1Password item "Prompto · Resend Alerts" (vault: Private)
  ▸ Writing to GitHub Actions secret (RESEND_API_KEY)…
  ✓ Set GitHub secret RESEND_API_KEY on Prompto-Studio/Prompto-Bot-Img
  ▸ Writing to Supabase Edge Function secret (RESEND_API_KEY)…
  ✓ Set Supabase secret RESEND_API_KEY on lubvpbxxwyylzsnriorr
  ▸ Triggering post-rotate workflow .github/workflows/test-alerts.yml…
  ✓ Workflow triggered (check Actions tab)

  ✓ Rotation complete (3/3 destinations).
  ? Open https://resend.com/api-keys to revoke the old key now? [Y/n]

One command. ~30 seconds. Audit-logged. Verified.


Why keyrotate over a bash script

Bash script keyrotate
Verifies the new key before writing ✅ Hits the provider's API first; aborts if the key is rejected
Updates 1Password alongside the destinations Maybe ✅ Auto-creates the vault item if it doesn't exist, updates rotated_at if it does
Hidden-input prompt for the secret If you remember read -s ✅ Built-in, with paste-detection
Audit log of every rotation ✅ JSONL at ~/.config/keyrotate/audit.log — grep, ship to SIEM, anything
Multi-rotation config in your repo One script per key ✅ One keyrotate.toml per project — every key your service uses, in one file
Per-destination overrides (e.g. VITE_FOO in .env, FOO in GitHub) Manual [rotations.x.overrides.envfile] env_name = "VITE_FOO"
Triggers a post-rotate verification workflow If you remember ✅ Configurable in keyrotate.toml, runs after success
Works across macOS Apple Silicon, Intel, Linux Maybe ✅ Single-binary, no Node/Bun needed at runtime

Install

Easiest — npx (no install)

If you have Node.js 18+ on your Mac, you can run keyrotate without installing anything:

npx keyrotate setup       # interactive wizard
npx keyrotate rotate <name>

Each npx invocation downloads the right binary for your Mac (Apple Silicon or Intel) automatically. Good for trying it out.

Recommended on macOS — npm install -g

If you'll use keyrotate more than once, install it globally so keyrotate and kr are always on your PATH:

# 1. Make sure Node.js is installed.
#    Check with: node --version    (need 18 or newer)
#    Install via Homebrew if missing:
brew install node

# 2. Install keyrotate
npm install -g keyrotate

# 3. Verify it works
keyrotate version
kr version                # `kr` is the short alias

That's it — keyrotate and kr now work in any terminal.

On Windows

Same as macOS — pick npx for a one-off or npm install -g to make it permanent. Works in PowerShell, Command Prompt, or Windows Terminal:

# Make sure Node.js 18+ is installed (https://nodejs.org or `winget install OpenJS.NodeJS.LTS`)
node --version

# One-off
npx keyrotate setup

# Or install globally
npm install -g keyrotate
keyrotate version
kr version

WSL2 users can use the macOS/Linux instructions instead — npx keyrotate setup works identically under Ubuntu/Debian WSL.

Before your first rotation on Windows, install the destination CLIs (use winget or each tool's official MSI):

winget install 1Password.CLI       # `op`
winget install GitHub.CLI          # `gh`
winget install Supabase.CLI        # `supabase`
winget install Netlify.Netlify     # `netlify`
winget install Fly-io.flyctl       # `flyctl`

Sign each one in once (op signin, gh auth login, etc.) and you're set. 1Password unlock on Windows uses Windows Hello instead of Touch ID — otherwise the flow is identical to macOS.

Homebrew (once the tap is published — currently in setup)

brew tap Prompto-Studio/homebrew-tap
brew install keyrotate

Curl one-liner (no Node, no Homebrew)

curl -fsSL https://raw.githubusercontent.com/Prompto-Studio/keyrotate/main/scripts/install.sh | bash

Drops the binary at ~/bin/keyrotate (and the kr short alias). Honors KEYROTATE_INSTALL_DIR and KEYROTATE_REPO if you want to override.

From source

git clone https://github.com/Prompto-Studio/keyrotate.git
cd keyrotate && bun install && bun run build
ln -sf "$(pwd)/dist/keyrotate" ~/bin/keyrotate
ln -sf "$(pwd)/dist/keyrotate" ~/bin/kr

Requires Bun 1.2+ to build (not at runtime — the binary is standalone).

Before your first rotation

You'll also need these CLIs signed in (only the ones for destinations you actually use):

brew install --cask 1password-cli   # `op` — 1Password vault
brew install gh                     # GitHub Actions secrets
brew install supabase/tap/supabase  # Supabase Edge Function secrets
brew install netlify-cli            # Netlify env vars
brew install flyctl                 # Fly.io app secrets

# Sign each one in once:
op signin
gh auth login
supabase login
netlify login
flyctl auth login

Then run keyrotate setup (interactive wizard) or keyrotate init (write a starter keyrotate.toml).


Quick start

If you're new and don't even know what you have:

cd /path/to/your/project
keyrotate init                  # scaffolds keyrotate.toml
keyrotate discover              # scan .env / 1Password / Bitwarden for existing keys (read-only)
$EDITOR keyrotate.toml          # define a rotation for each key you found
keyrotate import <name>         # bring an existing key under management (first-time)
keyrotate self-check            # doctor + discover + check-rotations in one pass

Day-to-day:

keyrotate list                  # show all rotations
keyrotate rotate <name>         # interactive rotation (paste new key)
keyrotate auto-rotate <name>    # zero-prompt rotation (Resend / PostHog)
keyrotate audit                 # show rotation history
keyrotate check-rotations       # see what's due (great in a daily cron)

For a long-tail provider keyrotate doesn't know natively:

keyrotate add-custom mailgun    # 30-second wizard, writes a [providers.custom.mailgun] block
keyrotate verify mailgun        # try the verifier without rotating
keyrotate rotate mailgun        # rotate when you're ready

Core concepts

Rotation = a (provider × destinations × secret-name) tuple you'll rotate as a unit. You define them once in keyrotate.toml; you trigger them by name.

Provider = how keyrotate knows that a value works — a cheap, idempotent HTTP call to the upstream service. Verifies the new key before anything is written anywhere.

Destination = where the new value gets written. Always 1Password (the canonical truth), plus whichever CI / runtime / hosting platforms actually need the value.


Supported providers

Each provider knows how to verify that a candidate key actually works against the upstream service. New providers are ~30 lines each — PRs welcome.

Provider ID Verifier endpoint
Resend resend GET /domains
OpenAI openai GET /v1/models
Google Cloud (Gemini, Imagen, Veo) google-cloud GET /v1beta/models
fal.ai fal GET /health
ElevenLabs elevenlabs GET /v1/user
Stripe stripe GET /v1/balance
Netlify netlify GET /api/v1/user
Supabase Management API supabase GET /v1/projects
HuggingFace huggingface GET /api/whoami-v2
PostHog posthog GET /api/users/@me
AbuseIPDB abuseipdb GET /api/v2/check
Generic (no verifier) generic (skips verification)

Coming soon: Anthropic, AWS, Cloudflare, Dropbox, Google OAuth, Backblaze B2, Fly.io tokens, GitHub PATs, Vercel.


Supported destinations

Destination ID CLI needed What it does
1Password onepassword op Default canonical store. Auto-creates the item if missing; updates credential + rotated_at metadata otherwise. Best for desktop / interactive use.
Bitwarden bitwarden bw Alternative canonical vault. Recommended for headless / server use — supports fully non-interactive BW_CLIENTID / BW_CLIENTSECRET API-key auth.
GitHub Actions secrets github gh Repo-scoped Actions secret.
Supabase Edge Function secrets supabase supabase Project-scoped function secret.
Netlify env vars netlify netlify Per-context site env var.
Fly.io app secrets flyio flyctl Staged with --stage (deploy to apply).
Local .env file envfile NAME=VALUE line, idempotent rewrite. Mode 0600. Cross-platform (works on Windows).

Configuration reference

A minimal keyrotate.toml:

[defaults]
op_vault = "Private"                   # 1Password vault for all rotations

[rotations.resend-alerts]
provider     = "resend"                # which provider plugin to use
title        = "Prompto · Resend Alerts"  # 1Password item title
secret_name  = "RESEND_API_KEY"        # secret name used by destinations
destinations = ["onepassword", "github", "supabase"]
postRotateWorkflow = ".github/workflows/test-alerts.yml"  # optional

[destinations.github]
repo = "owner/repo"

[destinations.supabase]
project_ref = "abcdefghij"

[destinations.onepassword]
vault = "Private"

A more complex rotation with per-destination overrides (rare but useful — e.g. the local .env uses VITE_OPENAI_API_KEY but Supabase uses OPENAI_API_KEY):

[rotations.openai]
provider     = "openai"
title        = "Prompto · OpenAI"
secret_name  = "OPENAI_API_KEY"
destinations = ["onepassword", "supabase", "envfile"]

[rotations.openai.overrides.envfile]
path     = ".env.local"
env_name = "VITE_OPENAI_API_KEY"

[destinations.envfile]
path = ".env.local"

Multiple Fly.io apps? Override app per rotation:

[rotations.fly-ffmpeg]
provider     = "flyio"
title        = "Prompto · Fly.io ffmpeg-svc"
destinations = ["onepassword", "flyio"]

[rotations.fly-ffmpeg.overrides.flyio]
app = "prompto-ffmpeg-svc"

What rotate actually does, step by step

  1. Find config. Walks up from cwd until it finds keyrotate.toml.
  2. Pre-flight every destination. Does gh auth status succeed? Is op signed in? Is the configured vault reachable? Aborts before asking for the key if anything's missing.
  3. Hidden-input prompt (stty -echo) for the new value, with paste-tolerance.
  4. Format check. If the provider exposes a looksLikeKey predicate, sanity-check the format before going further.
  5. Verify against the provider. Hits a cheap idempotent endpoint with the new key. If this fails, nothing is written anywhere. (This is the single most important thing keyrotate does.)
  6. Write to each destination in order. Surfaces per-destination success/failure. Partial failures don't unwind earlier successes — you can re-run.
  7. Optional post-rotate workflow. Triggers a GitHub Actions workflow via gh workflow run to verify the end-to-end path (e.g. our test-alerts.yml confirms the new Resend key actually delivers email).
  8. Audit log. Appends a JSONL entry to ~/.config/keyrotate/audit.log — timestamp, provider, destinations attempted/succeeded/failed, verifier result, operator.
  9. Offer to revoke. Opens the provider's revocation page in your browser.

A real-world example

Prompto Studio AI — a solo-founder AI-video SaaS — runs 11 rotations through keyrotate: Resend, OpenAI, Google Cloud (Gemini/Imagen/Veo), fal.ai, ElevenLabs, Stripe, Netlify PAT, Supabase Management PAT, PostHog, HuggingFace, and AbuseIPDB. Each one writes to 1Password + the right combination of GitHub / Supabase / Netlify / Fly.io. A full rotation including post-rotate verification: ~30 seconds.

See examples/keyrotate.toml for the full Prompto config.


Security model

  • Secrets never appear in process argv for 1Password and GitHub — they're piped via stdin. (Supabase CLI and Fly.io CLI accept secrets via argv only; that's their interface — ps can briefly see them. PRs to use Management API directly are welcome.)
  • No telemetry. Zero phone-home. The binary makes HTTP calls only to the provider you're verifying against and the destination CLIs you've configured.
  • No keys logged. The audit log records rotated_at, outcome, destinations succeeded/failed — never the key value.
  • .env files written with mode 0600.
  • 1Password unlock uses your system biometrics (Touch ID on macOS) via the op CLI. keyrotate never sees your master password.

Roadmap

Shipped — v00.00.19 (current)

Discovery & onboarding

  • keyrotate discover — read-only scan of .env / 1Password / Bitwarden for existing keys, with masked previews and provider guesses
  • keyrotate import <name> — first-time import of an existing key (verifies against provider, writes to all destinations, skips the "revoke old?" prompt)
  • keyrotate self-check — runs doctor + discover + check-rotations in one pass (ideal cron entry)
  • keyrotate add-custom <id> — interactive wizard that writes a [providers.custom.<id>] block in 30 seconds

Auto-rotation

  • Provider.create(oldKey) capability added to the plugin interface
  • Implemented for Resend and PostHog (both have public PAT-mints-PAT APIs)
  • keyrotate auto-rotate <name> — zero-prompt rotation: read old key from 1Password → call provider.create() → verify → write everywhere

Reminders

  • rotate_every = "90d" policy per rotation
  • keyrotate check-rotations — read-only check that prints OK / SOON / DUE / OVERDUE / NEVER per rotation, exits non-zero when attention is needed
  • --email flag sends a summary via the user's own Resend key (zero infrastructure on our side)

OAuth

  • keyrotate github-oauth <client-id> — full OAuth device flow against github.com using a user-supplied OAuth App. Returns an OAuth user token (works as Bearer auth for most REST API calls).

Destinations

  • Bitwarden destination plugin (headless-friendly, recommended for server use)

Distribution

  • Homebrew tap (brew install Prompto-Studio/homebrew-tap/keyrotate)
  • npm package (npx keyrotate setup, npm install -g keyrotate)
  • Windows x64 binary (cross-compiled by Bun)
  • Curl install script

v00.00.20+ (next)

  • Auto-rotation for Stripe restricted keys, Netlify PATs, Fly.io tokens, Supabase Mgmt — each via the provider's own OAuth / management flow
  • Google Cloud OAuth device flow for service-account-key creation
  • Optional Slack / Discord notifications on rotation outcome
  • Stable plugin API + Homebrew-core submission

FAQ

Why Bun instead of Node? Single-binary compilation. bun build --compile ships a 60-80 MB statically-linked executable. No runtime, no node_modules, no version-mismatch dramas. The binary works on machines that have never seen Bun installed.

Why not [the popular DevOps secret tool]? Most "real" secret tools (HashiCorp Vault, Doppler, Infisical, Bitwarden Secrets Manager) are great if your team has $200+/mo in budget and infrastructure to run a secret backend. keyrotate is for the case before that — where 1Password is already the source of truth and you just need to stop hand-syncing it with GitHub/Supabase/Netlify.

Does it work in CI? Yes, if you wire op service-account tokens into your runner. We use it locally and in CI for one-off "rotate everything quarterly" scripts.

What about Windows? Bun supports Windows but I haven't tested keyrotate there. PRs welcome.

Can I add a new provider? Yes — ~30 lines in src/providers/<id>.ts and one line in the registry. See src/providers/resend.ts as the canonical example.

Can I add a new destination? Slightly more involved — the destination plugin implements set() + check(). See src/destinations/onepassword.ts for the most feature-complete example.


Contributing

Issues and PRs welcome. The codebase is small and intentionally dependency-light (one runtime dep: @iarna/toml).

git clone https://github.com/Prompto-Studio/keyrotate.git
cd keyrotate
bun install
bun run dev rotate <name>     # run from source against your project
bun run test
bun run lint                  # tsc --noEmit

License

MIT © BotFlow Lab


Support

If keyrotate saved you from a production outage, the most useful thing you can do is ⭐ star the repo — that's what helps other devs find it.

If you want to do more, ♥ Sponsor on GitHub is the preferred way (it covers recurring + one-time; GitHub takes no fee). Ko-fi support is coming soon.


Built by @SteveKinzey for solo founders who got tired of rotation footguns.

About

Rotate any API key in one command — 1Password, Bitwarden, GitHub, Supabase, Netlify, Fly.io, .env. Verified end-to-end.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors