Detecting and Rotating Leaked Cloud Credentials

A contractor pushed a .env file to a public fork at 2am. By the time our GuardDuty alert fired, someone in another timezone had spun up eight p4d.24xlarge instances to mine crypto. We caught it inside forty minutes and the bill was still four figures. That's the good outcome, and it's the one you plan for. Static credentials leak. Not because your team is careless, but because a long-lived string of characters that grants API access will eventually end up somewhere it shouldn't: a committed config, a debug log, a Slack paste, a stolen laptop, a CI variable printed by a well-meaning set -x.

If your security model assumes keys stay secret, your model is wrong. Assume the leak. Build for detection and fast response.

Detection in layers #

No single control catches everything, so stack them.

Pre-commit and pre-push. The cheapest place to stop a leak is before it lands. Run gitleaks in CI and, better, as a local hook.

bash.bash

# scan the whole history of a repo
gitleaks detect --source . --report-format json --report-path leaks.json

# scan only what's staged, as a pre-commit hook
gitleaks protect --staged --verbose

trufflehog goes a step further and actually verifies whether a found key is live, which cuts the noise from expired or fake test keys:

bash.bash

trufflehog git file://. --only-verified

Turn on GitHub push protection at the org level. It blocks pushes containing recognized secret patterns and has stopped more real leaks for us than any scanner we run ourselves, because it catches the human before the mistake is public.

Provider-side detection. AWS scans public code hosts for exposed access keys. When it finds one, it attaches the AWSCompromisedKeyQuarantine policy to the user, which denies the high-blast-radius actions (IAM changes, spinning up expensive instances) and emails the account root. Do not treat that email as spam. If you use a third party, GitGuardian watches public GitHub in near real time and will often ping you before your own scanners run.

Behavioral detection. Scanning finds keys that leaked through code. It does nothing for a key stolen off a laptop. For that you need to watch behavior. GuardDuty flags anomalies like calls from a new geography, GetCallerIdentity reconnaissance, or a burst of RunInstances. If you're rolling your own, CloudTrail plus a few Athena queries for "actions this principal has never called before" gets you most of the way.

The response runbook #

When a key is confirmed leaked, work in this order. Speed matters more than elegance.

1. Disable first, ask questions later. Deactivating a key is reversible and instant. Deleting it destroys evidence. Do this before anything else:

bash.bash

aws iam update-access-key \
--user-name ci-deploy \
--access-key-id AKIAEXAMPLE12345 \
--status Inactive

Now the attacker is locked out and you still have the key on record for the investigation.

2. Assess blast radius. What did the key touch? Pull its recent activity from CloudTrail:

bash.bash

aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLE12345 \
--start-time 2026-07-13T00:00:00Z \
--query 'Events[].{Time:EventTime,Name:EventName,Source:EventSource}' \
--output table

Read it looking for two things: reconnaissance (ListBuckets, GetCallerIdentity, ListUsers) and impact (RunInstances, CreateUser, PutObject, GetObject on data buckets). If you see CreateAccessKey or CreateUser, the attacker made themselves persistence and disabling one key isn't enough. Widen the hunt.

3. Rotate. Issue a new key, deploy it to the legitimate consumer, verify the workload is healthy, then delete the old one. Order matters so you don't cause your own outage.

4. Root cause. How did it leak? A committed file means you also need to purge git history and assume it's public forever. A CI log means fix the logging, not just the key. Write it down in a postmortem while it's fresh.

5. Eliminate the credential type. This is the step most teams skip, and it's the only one that stops the incident from recurring. If a workload leaked a key, that workload probably shouldn't have had a static key at all.

The credential you don't have can't leak #

Every step above is response work you're forced into because a long-lived secret existed. The durable fix is to stop having the secret.

Most things that hold static keys don't need them. CI runners, EC2 instances, EKS pods, Lambda, GitHub Actions, and workloads in other clouds can all get short-lived credentials through role assumption and OIDC federation instead. There's no key to scan for, nothing to rotate, and a stolen token expires in an hour whether you notice or not. We wrote up the mechanics in keyless authentication; pair it with tight least privilege so that even a live token can't do much.

Static keys don't disappear entirely. A legacy vendor integration, an on-prem box, a partner API: some corners keep them. For those, scope them to the minimum, put them in a secrets manager with rotation, and keep the detection layers running. But treat every remaining static key as a liability you're carrying deliberately, not a default.

The call we'd make #

Detection and a rehearsed runbook are table stakes: run gitleaks in CI, enable push protection, wire up CloudTrail anomaly alerts, and drill the disable-assess-rotate sequence until it's muscle memory. But don't let a smooth runbook become an excuse to keep the keys around. Every static credential you federate away is one you never have to detect, rotate, or explain in a postmortem. Spend your effort there. The best leaked-credential incident is the one that can't happen because there was no credential to leak.