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

Source: https://startwithidentity.com/recipes/rotate-leaked-secret/
Last updated: 2026-11-05
License: content by Start with Identity. Cite the source URL.

---

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](https://startwithidentity.com/breaches/incidents/2026-litellm-leaked-admin-key-poisoned-release/), an admin key was deleted from a public repo about three hours after it was committed but [never revoked](https://startwithidentity.com/blog/2026-10-23-litellm-traces-poisoned-release-to-an-admin-key-leaked-in-may-and-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](https://startwithidentity.com/techniques/secrets-in-repositories-and-ci/) and [static API key abuse](https://startwithidentity.com/techniques/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](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)). Org owners can revoke members' fine-grained tokens under `Organization settings > Personal access tokens > Active tokens` ([reviewing tokens](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/reviewing-and-revoking-personal-access-tokens-in-your-organization)); classic tokens are not listed there.

**GitHub token you found but do not own.** Use the [credential revocation API](https://docs.github.com/en/rest/credentials/revoke). 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.

```bash
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](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps)). 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](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app).

**AWS access key.** Deactivate it, then attach `AWSDenyAll` to the user. Deactivation alone is not enough: [temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_disable-perms.html) 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.

```bash
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`](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSCompromisedKeyQuarantineV3.html) 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](https://docs.stripe.com/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](https://startwithidentity.com/recipes/github-actions-oidc-aws-azure/)); if you must keep a key:

```bash
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](https://startwithidentity.com/vendors/secrets/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`](https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html) covers 90 days, one Region per call, at two requests per second.

```bash
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](https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/identifying-audit-log-events-performed-by-an-access-token)):

```bash
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](https://docs.github.com/en/code-security/secret-scanning/managing-alerts-from-secret-scanning/evaluating-alerts) with validity checks and [push protection](https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection), which blocks pushes containing known secret formats. Dedicated scanners such as [GitGuardian](https://startwithidentity.com/vendors/secrets/gitguardian/) can run alongside it. Replace static keys with federation ([workload identity federation](https://startwithidentity.com/glossary/workload-identity-federation/)) and automate [secrets rotation](https://startwithidentity.com/glossary/secrets-rotation/) for what remains ([API key rotation guide](https://startwithidentity.com/guides/machine-identity/api-key-rotation-automation-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.

## Related

- [API key](https://startwithidentity.com/glossary/api-key/) and [secrets rotation](https://startwithidentity.com/glossary/secrets-rotation/)
- [Secrets in repositories and CI](https://startwithidentity.com/techniques/secrets-in-repositories-and-ci/)
- [New York Times source code taken with an exposed GitHub token](https://startwithidentity.com/breaches/incidents/2024-new-york-times-exposed-github-token/)
- [LiteLLM poisoned release from a never-revoked key](https://startwithidentity.com/breaches/incidents/2026-litellm-leaked-admin-key-poisoned-release/)
- [GitHub Actions OIDC to AWS and Azure](https://startwithidentity.com/recipes/github-actions-oidc-aws-azure/)
