Skip to content
Start with Identity
Recipe · Intermediate · 60 min

Rotate a leaked secret properly: revoke, rotate, check usage logs

What to do in the first hour after a GitHub token, AWS access key, or Stripe API key leaks: contain it, move consumers to a new credential, read the logs back to the leak date, and stop the next one.

SecretsGitHubAWSStripe
Before you start
  • Admin rights on the system that issued the secret (GitHub org owner, IAM admin, Stripe admin)
  • AWS CLI v2 configured with a separate admin identity, not the leaked key
  • A list of where the secret is deployed (CI variables, secret stores, config)
  • The date the secret was first exposed, from the commit or log that leaked it

Rotating a leaked secret is four steps in a fixed order: contain it (revoke or deactivate the credential now, accepting some breakage), rotate consumers onto a new credential, investigate every use of the old one back to the date it leaked, and prevent the next leak with push protection and short-lived credentials. Deleting the commit, rewriting history, or making the repo private is not remediation. In the LiteLLM incident, an admin key was deleted from a public repo about three hours after it was committed but never revoked, and an attacker used it months later to reach a GitHub org owner's token and publish a poisoned release.

Before you start: triage

Secret Contain Logs to read
GitHub PAT or OAuth token Delete it, or POST /credentials/revoke Enterprise audit log by hashed_token
GitHub App private key New key, then delete the old one Audit log events by the app
AWS access key update-access-key --status Inactive plus deny-all CloudTrail by AccessKeyId
Stripe secret or restricted key Rotate key, Expiration Now Workbench request logs

Assume the secret was used. The secrets in repositories and CI and static API key abuse technique pages explain why attackers find these within minutes.

1. Contain: kill the credential

GitHub personal access token you own. Go to Settings > Developer settings > Personal access tokens, pick Fine-grained tokens or Tokens (classic), and click Delete (managing tokens). Org owners can revoke members' fine-grained tokens under Organization settings > Personal access tokens > Active tokens (reviewing tokens); classic tokens are not listed there.

GitHub token you found but do not own. Use the credential revocation API. It must be called unauthenticated (authenticated calls return 403), takes up to 1,000 tokens per request, is limited to 60 requests per hour, and notifies the owner. It covers ghp_, github_pat_, gho_, ghu_, and ghr_ tokens.

curl -s -X POST https://api.github.com/credentials/revoke \
  -H "Accept: application/vnd.github+json" \
  -d '{"credentials":["ghp_EXAMPLE0000000000000000000000000000"]}'
# expect HTTP 202

GitHub App private key. Under Settings > Developer settings > GitHub Apps > Edit, generate a new key, deploy it, then delete the leaked key (managing private keys). Keys never expire, an app can hold up to 25, and you must add a new key before deleting the only one. Installation tokens minted with the old key expire after 1 hour.

AWS access key. Deactivate it, then attach AWSDenyAll to the user. Deactivation alone is not enough: temporary credentials are valid until they expire (up to 36 hours), but their permissions are evaluated on every request, so removing the user's permissions stops them.

aws iam update-access-key --user-name ci-deployer \
  --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive
aws iam attach-user-policy --user-name ci-deployer \
  --policy-arn arn:aws:iam::aws:policy/AWSDenyAll

If AWS detected the leak first, it may have attached AWSCompromisedKeyQuarantineV3 to the user and opened a support case. AWS says not to remove that policy; follow the case instructions.

Stripe key. On the API keys page, open the key's menu, choose Rotate key, and set Expiration to Now. The up-to-7-day grace period is for planned rotation, not a leak.

Check: a call with the old credential is rejected by GitHub, AWS, or Stripe (Stripe returns an authentication error for an expired key). Common failure: production breaks because the leaked credential was also the live one. That is the expected cost; move straight to step 2. Rollback: only for AWS: update-access-key --status Active restores the key. Do it only if you have proof the key never left your control.

2. Rotate consumers

Issue the replacement on a fresh identity where possible, because a deny-all user cannot be reused. For AWS, the long-term fix is a role assumed through OIDC (GitHub Actions OIDC to AWS and Azure); if you must keep a key:

aws iam create-user --user-name ci-deployer-2
aws iam create-access-key --user-name ci-deployer-2
# attach the same least-privilege policy, update the secret store, redeploy
aws iam delete-access-key --user-name ci-deployer --access-key-id AKIAIOSFODNN7EXAMPLE

Update every location from your inventory: CI variables, AWS Secrets Manager or another vault, and runtime environment variables. For GitHub, prefer a GitHub App or a fine-grained token scoped to the repos it needs, with an expiration.

Check: each consumer runs green on the new credential, and aws iam list-access-keys --user-name ci-deployer returns no keys. Common failure: a forgotten consumer (a cron job, a laptop profile) keeps failing; its errors are your inventory of places you missed. Rollback: redeploy the previous config only with a different, non-leaked credential.

3. Investigate back to the leak date

AWS. Find the last use, then pull every management event signed by the key. lookup-events covers 90 days, one Region per call, at two requests per second.

aws iam get-access-key-last-used --access-key-id AKIAIOSFODNN7EXAMPLE
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  aws cloudtrail lookup-events --region "$r" --start-time 2026-05-27T00:00:00Z \
    --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAIOSFODNN7EXAMPLE \
    --query 'Events[].[EventTime,EventName,EventSource]' --output text
done

Look for CreateAccessKey, CreateUser, AttachUserPolicy, CreateRole, and GetSessionToken. Temporary credentials have their own access key ID, so search again for each one GetSessionToken returned. lookup-events returns management events only; S3 object reads and other data events are visible only if a trail or event data store was capturing them.

GitHub. Hash the token and search the enterprise audit log (token events):

echo -n "$TOKEN" | openssl dgst -sha256 -binary | base64
# search: hashed_token:"<value>"

Git events are not returned by search; export the audit log for clone and push activity.

Stripe. Open the key's menu and select View request logs to see its calls in Workbench.

Check: you can explain every event between the leak date and containment. Common failure: you start from the detection date and miss months of use. Rollback: not applicable. Anything unexplained becomes an incident.

4. Prevent the next one

Turn on secret scanning with validity checks and push protection, which blocks pushes containing known secret formats. Dedicated scanners such as GitGuardian can run alongside it. Replace static keys with federation (workload identity federation) and automate secrets rotation for what remains (API key rotation guide).

Check: the repository's secret scanning settings show push protection enabled, and new alerts reach an owner who can revoke. Common failure: developers bypass push protection with a reason code and nobody reviews bypasses. Rollback: not applicable.

Verify it works

  • aws sts get-caller-identity run with the old key fails.
  • curl -H "Authorization: Bearer $OLD" https://api.github.com/user is rejected instead of returning a user.
  • A Stripe call with the old key returns an authentication error; the new key succeeds.
  • The CloudTrail and GitHub audit log queries from step 3 show no events after containment.
  • The secret scanning alert's validity check shows the secret as inactive, and the alert is closed with the investigation linked.

Frequently asked questions

Is deleting a leaked secret from a Git commit enough?
No. Anyone who cloned, forked, or scraped the repository already has it, and the credential keeps working until you revoke it. LiteLLM's October 2026 incident started with an admin key that was deleted from a public repo within hours but never revoked, then used months later.
How do I revoke a leaked GitHub token I do not own?
Send it to GitHub's credential revocation endpoint, POST /credentials/revoke, without authentication. It accepts up to 1,000 tokens per request for classic and fine-grained personal access tokens, OAuth app tokens, and GitHub App user tokens, and GitHub notifies the owner.
What should I do first when an AWS access key leaks?
Set the key to Inactive with aws iam update-access-key, then attach a deny-all policy to the IAM user, because temporary credentials minted from the key with GetSessionToken stay valid until they expire. Then check CloudTrail for every call the key made.
How do I rotate a Stripe API key immediately?
On the Dashboard API keys page, open the key's menu, choose Rotate key, and set Expiration to Now. The old key stops working at once, so deploy the new key right away; a later expiration gives up to 7 days of overlap, which is only appropriate when the key was not exposed.
How far back should I check logs after a secret leak?
To the first moment the secret was exposed, not the moment you noticed. CloudTrail event history lookups cover the last 90 days per Region, so older exposures need a trail or CloudTrail Lake.
Last reviewed By SWI Community TeamSuggest a correctionHow we research
Free to read and use. Code is illustrative, vendor-neutral, and built on the open standards. Review it against your provider's docs and your threat model before shipping. See more in the recipes index and the standards library.