All posts
6 min read

A Practical Guide to Secret Rotation

guidessecuritysecret-rotation

You get a PagerDuty alert at 2 AM. Your Stripe webhook secret was exposed in a public repo. You need to rotate it immediately. Do you know, right now, every platform and environment where that secret is deployed?

Most teams don't. Secret rotation is one of those things everyone agrees they should do regularly but almost nobody does until forced. And when you're forced, it's always urgent, always stressful, and always reveals how little visibility you have into where your secrets actually live.

Why rotate secrets at all

The case for rotation is simple: every secret has a nonzero probability of being compromised. The longer a secret lives, the more opportunities there are for it to leak through git history, log files, developer machines, third-party breaches, or plain human error.

Regular rotation limits the blast radius. A database password that's rotated monthly means a leak from two months ago is already invalidated. A key that hasn't been rotated in eighteen months means an eighteen-month window of potential exposure.

Beyond risk reduction, some compliance frameworks (SOC 2, PCI DSS, HIPAA) require documented rotation policies. Even if you're not pursuing certification today, building the habit early saves a painful retrofit later.

The rotation checklist

Every secret rotation follows the same basic steps. The complexity comes from doing them consistently across multiple platforms.

1. Generate the new credential

Go to the provider (Stripe, your database, AWS IAM, etc.) and create a new key. Don't revoke the old one yet.

2. Deploy the new value everywhere

This is where most rotations fail. You update production but forget staging. You update Vercel but forget Convex. You update the platform but forget to update the CI/CD pipeline.

You need a complete inventory of every location where the secret is used:

  • Platform environments -- Vercel (development, preview, production), Railway, Convex, etc.
  • CI/CD pipelines -- GitHub Actions secrets, GitLab CI variables
  • Local developer machines -- everyone's .env files
  • Infrastructure -- Kubernetes secrets, AWS Parameter Store, Terraform state

3. Verify the new credential works

After deploying, verify that each service is functioning correctly with the new value. Check API responses, webhook deliveries, database connections. Don't skip this -- silent auth failures are worse than loud ones.

4. Revoke the old credential

Only after you've confirmed the new value works everywhere should you revoke the old one. This two-phase approach (deploy new, then revoke old) avoids downtime from premature revocation.

5. Document what you did

Record which secret was rotated, when, by whom, and which platforms were updated. This is both a compliance requirement and a debugging aid.

Where teams get stuck

No single inventory of secrets

The first question in any rotation is "where is this secret used?" If the answer requires checking three dashboards, a CI/CD config, and asking your teammates, you've already lost time.

A dotenvy.yaml config gives you that inventory:

secrets:
  - STRIPE_SECRET_KEY
  - STRIPE_WEBHOOK_SECRET
  - DATABASE_URL
  - RESEND_API_KEY
  - CLERK_SECRET_KEY

targets:
  vercel:
    type: vercel
    project: my-app
    mapping:
      development: test
      preview: test
      production: live
  convex:
    type: convex
    deployment: my-app
    mapping:
      default: test
  railway:
    type: railway
    project: my-workers
    mapping:
      production: live

When you need to rotate STRIPE_SECRET_KEY, the config tells you immediately: it's deployed to Vercel (three environments), Convex (one deployment), and Railway (one service). No guessing.

The multi-platform update problem

Even with a clear inventory, updating a secret across multiple platforms means logging into each dashboard separately. For a single secret across three platforms and two environments, that's up to six manual updates.

dotenvy collapses this to two commands:

# Rotate the test-mode key
dotenvy set STRIPE_SECRET_KEY=sk_test_new_xxx

# Rotate the live-mode key
dotenvy set STRIPE_SECRET_KEY=sk_live_new_xxx --env live

Each set command updates your local file and pushes to every target mapped to that environment. All platforms are updated in one operation.

Forgetting to rotate test credentials

Production rotations get attention because outages are visible. But test credentials matter too. If your test-mode Stripe key leaks, an attacker can read your test data, understand your integration patterns, and potentially use that knowledge to target your production setup.

Rotate both environments:

dotenvy set STRIPE_SECRET_KEY=sk_test_rotated --env test
dotenvy set STRIPE_SECRET_KEY=sk_live_rotated --env live

No verification step

After rotating, you need to know the new value actually took effect on every platform. A dry-run comparison shows you what's currently deployed:

dotenvy sync test --dry-run
dotenvy sync live --dry-run

If everything shows "synced," the rotation is complete. If anything shows "drifted," you have a platform that didn't get the update.

Building a rotation schedule

Not every secret needs the same rotation frequency. A reasonable starting point:

Rotate immediately on exposure:

  • Any secret found in git history, logs, or public channels. No exceptions.

Rotate quarterly:

  • API keys for third-party services (Stripe, Resend, Twilio)
  • Webhook signing secrets
  • OAuth client secrets

Rotate when team membership changes:

  • Any secret that a departing team member had access to
  • Shared service account credentials

Rotate annually (at minimum):

  • Database passwords
  • Encryption keys (with re-encryption of stored data if needed)
  • Infrastructure credentials (AWS, GCP, Azure)

The specific intervals matter less than having intervals at all. A quarterly rotation that actually happens is better than a monthly policy that nobody follows.

Automating the workflow

A manual rotation process that takes thirty minutes and touches six platforms will get skipped. The goal is to reduce the friction until rotation is a two-minute task.

Here's a practical rotation workflow:

# 1. Generate new credential at the provider (manual step)

# 2. Update test environments
dotenvy set STRIPE_SECRET_KEY=sk_test_new_value

# 3. Verify test environments
dotenvy sync test --dry-run
# Confirm all targets show "synced"

# 4. Update production environments
dotenvy set STRIPE_SECRET_KEY=sk_live_new_value --env live

# 5. Verify production environments
dotenvy sync live --dry-run
# Confirm all targets show "synced"

# 6. Revoke old credential at the provider (manual step)

Steps 1 and 6 require visiting the provider's dashboard -- there's no way around that. But steps 2 through 5 are fully handled by dotenvy. The entire rotation touches your terminal and the provider dashboard. No other dashboards involved.

The cost of not rotating

Every unrotated secret is a bet that it hasn't been compromised. The longer the interval, the worse the odds. And when a breach happens, the investigation always reveals the same thing: the compromised credential was set fourteen months ago and never rotated.

Regular rotation is cheap insurance. Especially when the sync step is automated, the only real cost is generating a new credential and revoking the old one. That's a five-minute task per secret, per quarter. Compare that to the cost of a breach.

Start with an inventory. Know where your secrets live. Rotate the most critical ones first. Automate the deployment step so there's no excuse to skip it.

curl -fsSL https://dotenvy.dev/install.sh | sh