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
--applynothing 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,rotateandrevokeask you to type the key ID first (--yesskips this in automation; without a terminal it is required).- Each applied step is appended to
rotation-log.tsvwith 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
- Anything that can create resources or read customer data.
- Anything with billing attached.
- Anything that grants further access (SSH, CI, deploy keys).
- 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.