Raxx · internal docs

internal · gated ↑ index

Rotation SOP — Stripe Restricted API Key

Mode: operator-assisted Last validated: 2026-04-24 UTC Validation method: read-only-docs (NEVER live-rotate without grace period + sandbox first) Average duration: 10m Required role: superadmin

Applies to: STRIPE_RESTRICTED_KEY (and any per-environment restricted keys, e.g., STRIPE_RESTRICTED_KEY_LIVE, STRIPE_RESTRICTED_KEY_TEST). The Stripe master / secret key is never stored in our infrastructure and is not in scope here.

Stripe rotation has a built-in grace period. Use it. Selecting "Now" on rotation deletes the old key instantly and risks a billing outage if propagation fails. Always specify a grace period (recommended: 24h) so old + new keys coexist during the swap window.

When to run

Prerequisites

Steps

1. Pre-rotation checks

# Confirm current restricted key works
curl -sS -u "$CURRENT_RESTRICTED_KEY:" https://api.stripe.com/v1/customers?limit=1 | jq '.data | length'
# Expect: 0 or 1 (or whatever the test query returns; HTTP 200 is what matters)

Confirm the key's scopes match what we expect (visible in the dashboard's key detail panel). Document the scope set in the audit context.

2. Generate the new credential (rotate with grace period)

  1. Navigate to https://dashboard.stripe.com/apikeys (live) or https://dashboard.stripe.com/test/apikeys (test).
  2. Locate the restricted key by name.
  3. Click the overflow menu () → "Rotate key".
  4. Select expiration date for the OLD key: choose 24 hours (or 1 hour for a compromise scenario). Avoid "Now" unless you are mitigating active exploitation.
  5. Click "Rotate API key".
  6. Copy the new key value immediately — shown once.
  7. Add a note in the dashboard documenting where the key is stored (e.g., "Infisical: prod/STRIPE_RESTRICTED_KEY").

The old key continues to work for the grace period; the new key works immediately.

3. Validate the new credential

NEW_RESTRICTED_KEY="..."
curl -sS -u "$NEW_RESTRICTED_KEY:" https://api.stripe.com/v1/customers?limit=1 | jq '.data | length'
# Expect: HTTP 200 with same shape as step 1.

Confirm scope (try a permitted call and a denied call):

# Permitted:
curl -sS -u "$NEW_RESTRICTED_KEY:" https://api.stripe.com/v1/subscriptions?limit=1 | jq '.object'
# Expect: "list"

# Denied (if scope excludes balance):
curl -sS -u "$NEW_RESTRICTED_KEY:" https://api.stripe.com/v1/balance | jq '.error.code'
# Expect: "permission_error" or whatever the denied scope returns.

4. Store in Infisical

infisical secrets set STRIPE_RESTRICTED_KEY="$NEW_RESTRICTED_KEY" \
  --projectId="$INFISICAL_PROJECT_ID" --env=prod

5. Propagate to downstream consumers

Consumer How
Raptor (raxx-api-prod) heroku config:set STRIPE_RESTRICTED_KEY="$NEW_RESTRICTED_KEY" -a raxx-api-prod
Staging (raxx-api-staging) If using a separate test-mode key, rotate independently
Webhook signing secret NOT this SOP — webhook secrets are rotated via a separate path (stripe-webhook-secret.md — to be created if/when we add webhooks)

6. Verify downstream

# Hit a billing endpoint that uses Stripe
curl -sS https://api.raxx.app/api/billing/subscription | jq '.status'
# Expect: a successful response (not 401 / 500).

# Tail logs for any 401 / authentication_required from Stripe SDK
heroku logs --tail -a raxx-api-prod | grep -i stripe
# Expect: no auth errors after the dyno restart.

Run for at least 5 minutes to catch background jobs that may use the key.

7. Revoke the old credential

If the grace period was set to >0, the old key auto-expires at the deadline — no action needed. To explicitly expire earlier:

  1. Dashboard → API keys → old key → overflow → "Expire key".
  2. Confirm.

Verify:

curl -sS -o /dev/null -w "%{http_code}\n" -u "$OLD_RESTRICTED_KEY:" https://api.stripe.com/v1/customers?limit=1
# Expect: 401

8. Audit log entry

action: secret.rotate.completed
actor: <admin_id>
context: {
  "secret_name": "STRIPE_RESTRICTED_KEY",
  "method": "operator-assisted-dashboard",
  "grace_period_hours": 24,
  "old_key_expires_at": "..."
}

Rollback

If validation fails before the grace period ends, revert to the old key:

  1. Pull the previous value from Infisical version history.
  2. heroku config:set STRIPE_RESTRICTED_KEY="$OLD_KEY" -a raxx-api-prod.
  3. Wait for dyno restart.
  4. The old key continues to work until the grace period expires.
  5. Investigate the new key's failure; redo from step 2 with a fresh rotation.

After grace period expiry, the old key is dead. If the new key is broken at that point, generate a fresh restricted key (not a rotation — a brand new key with the same scopes) via Dashboard → "Create restricted key", and redo step 4+5.

Vendor doc references

Known gotchas