A leaked credential is not in one place — it is in every clone, fork, mirror, and CI cache that has ever held the repository. Revocation is urgent because the credential’s lifetime is no longer its rotation schedule; it is the lifetime of every object store that has ever touched this repo.
← All break/fix scenarios in Git, CI/CD & GitOps
Secret committed to Git and pushed to remote
Reported symptoms
- ●GitHub Advanced Security or a third-party scanner fires an alert: `aws_access_key_id` and `aws_secret_access_key` pair detected in commit `<sha>`
- ●The alert email arrived three days after the secret was first pushed; CloudTrail shows API calls from the key within that window
- ●The file containing the key is `terraform.tfvars`, committed to `main` and visible to every fork, mirror, and CI cache
- ●Engineer rotation logs show the engineer who committed it has been on PTO since the day after the merge
- ●The repository is private, but two contractors with read access were added last week
- ●A `git log --all -S "AKIA..." --pickaxe-regex` returns multiple matches across history, including a force-pushed commit
- ●The AWS key has the `AdministratorAccess` IAM policy attached
Evidence
- · `git show <merge-sha> -- terraform.tfvars` shows the literal `AKIA...` and the matching 40-character secret
- · `git log --all --oneline -- terraform.tfvars` shows the file appearing first in commit `<c1>` and persisting through `<merge-sha>`
- · `git fsck --unreachable` lists no dangling objects containing the secret — meaning it is reachable through `refs/heads/main`
- · The GitHub audit log shows the secret-scanning alert at T+3 days and the alert recipient is `security@example.com`
- · CloudTrail `LookupEvents` with `AccessKeyId=<key>` returns 14 events between T+1h and T+72h, including `iam:ListUsers` and `s3:ListAllMyBuckets`
- · `curl https://api.github.com/repos/<org>/<repo>/forks | jq .[].full_name` returns three forks, all of which contain the secret in their object database
- · The pre-commit hook that runs `gitleaks` is configured but only locally; it does not run in CI and was not active when the engineer committed
- · GitHub push protection is disabled at the org level
Diagnosis and resolutionclick to reveal
Root cause
The secret reached the repository because no automated control prevented it. The pre-commit hook that detects AWS keys existed on one developer's laptop but not on the engineer who committed, because hooks are not distributed with the repository — they live in `.git/hooks/`, which is per-clone, not in version control. Push protection, the server-side equivalent that GitHub can enforce on every push regardless of which client is used, was disabled at the organisation level. The result is that the only line of defence was the developer's memory, and a single missed check produced a credential that is now present in the object database of every clone, fork, mirror, CI cache, and developer laptop that has pulled since. The secret's lifetime is no longer its rotation schedule — it is the lifetime of every object store that has ever held this repository.
Remediation
Revoke first, investigate second. In AWS IAM, disable the access key immediately (do not delete it; the audit trail needs it), then issue a new key for whatever workload actually needs it. Audit CloudTrail for every API call between T+0 and revocation — `iam:*`, `s3:*`, and `sts:GetCallerIdentity` are the high-signal calls to look for. Treat any successful call from the leaked key as an active compromise; assume the worst, not the best, for the first 24 hours of activity. Then clean up the repository: `git filter-repo --invert-paths --path terraform.tfvars` (or `bfg-repo-cleaner --delete-files terraform.tfvars`), re-sign all rewritten commits (the original signatures are now over different content), force-push with `--force-with-lease` after coordinating with every maintainer, and rotate every CI secret that could read the old history. Notify every fork owner and every mirror operator to re-clone from the rewritten history; the secret is no longer in `main`, but it is still in every clone that existed before the rewrite.
Verification
`git log --all -S "AKIA..."` returns nothing in the rewritten history. `git fsck --unreachable` shows no dangling object whose blob contents match the secret. Every fork and mirror has been notified and confirmed re-cloned (or destroyed). CloudTrail shows no successful API call from the revoked key after the revocation timestamp. The replacement workload uses an OIDC federation role, not a long-lived key — meaning the next leak, if there is one, has no standing credential to leak.
Prevention
Make the secret impossible to commit, not just discouraged. Distribute pre-commit hooks through the repository (use a framework like `pre-commit.com` whose hook definitions live in `.pre-commit-config.yaml` and are versioned alongside the code) so every developer and CI runner gets the same checks. Enable GitHub push protection at the organisation level — it blocks the push before the secret ever leaves the developer, not after the secret-scanner notices it days later. Replace long-lived cloud credentials with short-lived OIDC-issued tokens wherever the workload supports it: GitHub Actions OIDC for AWS/GCP/Azure, IAM Roles Anywhere for on-prem. Add `*.tfvars` (when it contains secrets), `.env`, `*.pem`, and `id_*` to `.gitignore`, and treat any future commit that tries to add them as a security incident, not a routine mistake.