Respond

02 · Rotate and revoke, per secret type

Pattern for every secret: create the new one → deploy it everywhere it is used → revoke the old one → record the time.

If the leaked secret is actively being abused, reverse the order: revoke first and accept the downtime.

Everything in <angle brackets> is a placeholder.

With Spillkit: plan first, then rotate step by step

leak plan turns a scan report into a checklist in the order below (order of operations), with where to revoke each secret and a link to its section in this guide. leak rotate then takes cloud keys and GitLab tokens through the pattern above, one step per run:

./leak plan secrets-report.json                    # rotation-plan.md for the incident issue, .html to print, .json for automation
./leak rotate gcp-sa-key --account <sa>@<project>.iam.gserviceaccount.com --key-id <key-id>    # dry run
./leak rotate gcp-sa-key ... --apply check                                   # who used it, and when
./leak rotate gcp-sa-key ... --apply replace --store gcp-sm:<secret>         # new key, straight into Secret Manager
./leak rotate gcp-sa-key ... --apply disable                                 # after consumers have the new key
./leak rotate gcp-sa-key ... --apply delete
Recipe Identifies the credential with Steps --store targets
aws-key --user, --key-id check, replace, disable, delete aws-sm:<secret-id> (Secrets Manager)
azure-app-secret --app, --key-id check, replace, delete azure-kv:<vault>/<name> (Key Vault)
gcp-sa-key --account, --key-id check, replace, disable, delete gcp-sm:<secret> (Secret Manager)
azure-storage-key --group, --account, --key check, renew optional: the new connection string to Key Vault
gitlab-token --token-id, and --project or --group for their access tokens check, rotate (or revoke) any; it must already exist
azure-devops-pat --org, --authorization-id check, replace, revoke any

Any recipe can use any store. What it guarantees:

  • Without --apply nothing runs: you get the steps with their exact commands, to review or run by hand.
  • The new secret goes from the provider’s CLI to the store through a pipe. It is never shown, never written to disk and never in a command line. If the store refuses it, the new credential is deleted again.
  • disable, delete, renew, rotate and revoke ask you to type the key ID first (--yes skips this in automation; without a terminal it is required).
  • Each applied step is appended to rotation-log.tsv with its UTC time: credential IDs, never values.

GitLab replaces a token in one call that revokes the leaked one at once, so gitlab-token checks the store before it rotates (a revoked token cannot be brought back), defaults the new token to 90 days instead of GitLab’s one week (--expires to change it), and, if the store still refuses it, revokes the new token too and says so. For no downtime, make a replacement in GitLab yourself, deploy it, and use --apply revoke instead. check without --token-id lists the active tokens with their IDs and last use.

It uses the aws, az and gcloud CLIs you are already signed in to, and for GitLab the API with GITLAB_TOKEN (the api scope; not the leaked token) and GITLAB_URL when self-managed. Everything else in this guide is rotated by hand.


Azure

Service principal / app registration secret

# See the app's current secrets (key IDs and expiry, never values)
az ad app credential list --id <app-id> -o table

# Add a new secret alongside the old one
az ad app credential reset --id <app-id> --append --display-name "rotated-$(date +%F)" --years 1

# After updating every consumer (Key Vault, pipelines, app settings): remove the leaked one
az ad app credential delete --id <app-id> --key-id <leaked-key-id>

Better still: replace the secret with a managed identity or workload identity federation (OIDC), so there is no secret to leak next time.

Storage account keys

# Every SAS token signed with this key stops working too
az storage account keys renew -g <rg> -n <account> --key key1
az storage account keys renew -g <rg> -n <account> --key key2

# If user-delegation SAS tokens leaked
az storage account revoke-delegation-keys -g <rg> -n <account>

Cosmos DB, Service Bus, Event Hubs, Redis

az cosmosdb keys regenerate -g <rg> -n <account> --key-kind primary
az servicebus namespace authorization-rule keys renew -g <rg> --namespace-name <ns> -n <rule> --key PrimaryKey
az eventhubs namespace authorization-rule keys renew -g <rg> --namespace-name <ns> -n <rule> --key PrimaryKey
az redis regenerate-keys -g <rg> -n <cache> --key-type Primary

Azure SQL admin password

az sql server update -g <rg> -n <server> --admin-password '<new-password>'

Azure DevOps personal access token

User settings → Personal access tokens → Revoke. Organisation admins can revoke any user’s tokens from Organization settings.

Azure DevOps also scans public GitHub repositories for its tokens and, unless the tenant policy turns it off, revokes a leaked one itself: check the organisation’s audit log for Token.PatPublicDiscoveryEvent and Token.PatSystemRevokeEvent first. leak rotate azure-devops-pat lists your tokens, creates the replacement with the same scope straight into your secret store, and revokes the leaked one once the new one is deployed.


AWS

# When was the key last used, and for what service?
aws iam get-access-key-last-used --access-key-id <AKIA-key-id>

# New key, then disable and delete the leaked one
aws iam create-access-key --user-name <user>
aws iam update-access-key --user-name <user> --access-key-id <AKIA-key-id> --status Inactive
aws iam delete-access-key  --user-name <user> --access-key-id <AKIA-key-id>

If AWS has already attached the AWSCompromisedKeyQuarantine policy to the user, that is a confirmation the key was found publicly.


Google Cloud

# Service account key (JSON file with a private key): disable at once, delete after consumers move
gcloud iam service-accounts keys list    --iam-account=<sa>@<project>.iam.gserviceaccount.com
gcloud iam service-accounts keys disable <key-id> --iam-account=<sa>@<project>.iam.gserviceaccount.com
gcloud iam service-accounts keys delete  <key-id> --iam-account=<sa>@<project>.iam.gserviceaccount.com

# API key: create a replacement, ship it, delete the old one
gcloud services api-keys create --project=<project> --display-name="rotated-$(date +%F)"
gcloud services api-keys delete <old-key-id> --project=<project>

Google can disable an exposed service account key automatically; the audit log then shows gcp-compromised-key-response@system.gserviceaccount.com. Prefer workload identity federation to a new key file.


Keys that are meant to be public

Some keys are designed to ship inside web and mobile apps, so anyone using the app can read them. Scanners still flag them, and providers may still send exposure emails.

Examples Real protection After a leak
Firebase web config apiKey, Google Maps browser key, Stripe publishable key, Sentry DSN, analytics write keys Restrictions on the key (allowed domains, apps, APIs) and server-side rules (database and storage security rules, backend authorisation) Check the restrictions and rules are in place; rotate only if they were missing or the key was abused

The danger is never the key itself but what it can reach without further checks, for example a database whose rules allow anyone to read or write. Their secret counterparts (Stripe secret key, server-side API keys, service account keys) are real credentials: rotate them.


GitHub

Secret Revoke at
Personal access token Settings → Developer settings → Personal access tokens
Deploy key Repo → Settings → Deploy keys
GitHub App private key App settings → Private keys → Delete (generate a new one first)
Actions secrets that were also in the repo Repo or org → Settings → Secrets and variables → update

Databases

-- PostgreSQL
ALTER ROLE <app_user> WITH PASSWORD '<new-password>';

-- SQL Server / Azure SQL (contained or server login)
ALTER LOGIN <app_user> WITH PASSWORD = '<new-password>';

-- MySQL
ALTER USER '<app_user>'@'%' IDENTIFIED BY '<new-password>';

Then check for logins or roles you do not recognise, and narrow firewall rules to known addresses.


SSH keys and certificates

# New key pair
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_<service>_<account> -C "<user>@<host> <service> $(date +%F)"

# On every server that trusted the leaked key: remove its line
grep -n '<leaked-key-comment-or-fingerprint>' ~/.ssh/authorized_keys

TLS private keys (.pem, .pfx, .key): revoke the certificate with the issuing CA and issue a new one with a new key. Reusing the key with a new certificate does not help.


Application secrets

Secret Rotation Side effect
JWT / session signing key New key; accept old and new briefly if you can, then drop the old Users are logged out
Webhook signing secret Rotate on both sender and receiver Brief failed deliveries
Encryption key (data at rest) New key plus re-encrypting data Plan it; it is a migration
age or SOPS key New key, re-encrypt every file it protects, then rotate the secrets those files held: anyone with the old key has read them Every encrypted file changes
SMTP / email API key Regenerate in the provider None
Payment provider key Roll in the provider dashboard; most support an overlap period None if overlap used

More secret types: where to revoke

The pattern is the same everywhere: create the replacement → deploy it → revoke the old one → check the provider’s usage or audit page for the exposure window.

Secret Revoke or rotate at Watch out for
AI provider API keys (OpenAI, Anthropic, Google Gemini, Azure OpenAI, others) The provider console’s API keys page; Azure OpenAI: regenerate the resource key Bills grow fast when stolen keys run inference; check usage and set spend limits
Password manager credentials (1Password service account token or account Secret Key) 1Password → Developer → Service accounts; for a Secret Key, Profile → Security: regenerate it and change the account password Treat every item in the vaults it could read as leaked, and rotate those too
Container registry tokens (Docker Hub, GHCR, ACR, ECR) Registry account → access tokens Pushed images: an attacker could replace tags. Verify image digests
Package registry tokens (npm, PyPI, NuGet, RubyGems) Registry account → tokens Check for new package versions published in the window: a supply-chain risk
Kubernetes (kubeconfig, service account tokens) Rotate cluster credentials or delete the token; prefer short-lived tokens A kubeconfig with admin rights means full cluster access: review workloads and RBAC changes
Terraform Cloud / HCP tokens User or team settings → tokens Could change infrastructure: review runs in the window
Source platform tokens (GitLab PAT, Bitbucket app password or access token, Azure DevOps PAT) User or organisation settings → tokens Check for new deploy keys, webhooks and pushes made with the token
Messaging and email (Slack, Twilio, SendGrid, Mailgun) The provider’s API keys or app settings Spam and phishing sent in your name; check sending logs
Payment providers (Stripe and others) Dashboard → API keys → roll key (most allow an overlap period) Refunds, payouts or customer data access: review the event log
Webhooks with secrets in the URL (chat incoming webhooks, CI triggers) Delete and recreate the webhook The URL is the secret

Order of operations, when there are many

  1. Anything that can create resources or read customer data.
  2. Anything with billing attached.
  3. Anything that grants further access (SSH, CI, deploy keys).
  4. The rest.

Keep a table as you go: secret · where it was used · rotated at (UTC) · old one revoked at (UTC) · by whom. It becomes part of the timeline.