DEV Community

Cover image for How to Stop Accidentally Committing AWS Keys to GitHub
Alan West
Alan West

Posted on

How to Stop Accidentally Committing AWS Keys to GitHub

Last week the CISA — yes, the Cybersecurity and Infrastructure Security Agency — reportedly had one of their admins commit AWS GovCloud access keys to a public GitHub repo. If the people whose entire job is telling us not to do this can still do it, we should probably stop pretending it can't happen to us.

I've cleaned up this exact mess on three different teams now. Each time, the panic feels new. Each time, the fix follows the same playbook. So let's walk through what actually happens when secrets leak, how to scrub them, and — more importantly — how to make it nearly impossible for it to happen again.

Why Secrets Keep Ending Up in Repos

The usual suspects:

  • A .env file that wasn't in .gitignore (or was, but the file got force-added)
  • Hardcoded test credentials that someone swore they'd remove "before the PR"
  • Terraform state files committed by mistake
  • A throwaway script left in a scripts/ folder with creds in a variable at the top
  • IDE config files (.idea/, .vscode/) that picked up secrets from environment variables

The deeper root cause is almost always the same: secrets exist in plaintext somewhere on the developer's machine, and git add . is a very blunt instrument. Combine that with a tired engineer at 11pm, and you get a leak.

Here's the thing that bites people: deleting the file and pushing a new commit does not remove the secret. Git stores history. Anyone who cloned (or any bot scraping GitHub for keys — and there are many) can recover the value. GitHub's secret scanning catches some of these, but you can't rely on it.

Step 1: Rotate First, Investigate Second

When you find a leaked credential, do not start by trying to rewrite history. The clock is already ticking. Public GitHub leaks have been observed to be scraped and exploited within minutes.

Rotate first. For AWS, that looks like:

# Disable the leaked key immediately
aws iam update-access-key \
  --access-key-id AKIA_LEAKED_KEY_HERE \
  --status Inactive \
  --user-name the-user

# Then create a new one
aws iam create-access-key --user-name the-user

# Once you've verified the new key works in your apps, delete the old one
aws iam delete-access-key \
  --access-key-id AKIA_LEAKED_KEY_HERE \
  --user-name the-user
Enter fullscreen mode Exit fullscreen mode

Don't skip the Inactive step. Setting it inactive first means you can re-enable it for forensics if something else breaks unexpectedly, without keeping it live to attackers.

Then check CloudTrail for any usage of that key from IPs you don't recognize. Assume compromise if it was public for more than a few minutes.

Step 2: Scrub the History

Only after rotation should you clean the repo. The best open-source tool for this is git-filter-repo — the official replacement for the old git filter-branch. The Git project itself recommends it.

# Install it (macOS)
brew install git-filter-repo

# Make a fresh clone — filter-repo refuses to run on a repo with a remote by default,
# which is a good safety net
git clone --mirror git@github.com:org/repo.git
cd repo.git

# Create a file listing the literal secret strings you want gone
cat > secrets.txt <<EOF
AKIAIOSFODNN7EXAMPLE
wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
EOF

# Replace each match with ***REMOVED***
git filter-repo --replace-text secrets.txt

# Force-push the cleaned history
git push --force
Enter fullscreen mode Exit fullscreen mode

A few things people get wrong here:

  • Tell every collaborator to re-clone. Their local history still contains the secret and they will helpfully push it back.
  • Open PRs need to be recreated. Force-pushing the default branch invalidates them.
  • Cached forks and the GitHub web UI commit view may still show the secret for a while. Open a GitHub support request to expire those.

Honestly, on a small repo, sometimes the saner move is to nuke it and start a new one. I've done that twice and never regretted it.

Step 3: Build a Wall So It Never Happens Again

This is where most teams stop too early. Cleaning up is reactive. The real win is making leaks structurally impossible.

Pre-commit scanning

I run gitleaks as a pre-commit hook on every project. It's fast, has good default rules for AWS keys, GCP creds, private keys, and dozens of other patterns.

Using the pre-commit framework:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks
        # Fail the commit if any matches are found in staged files
        stages: [pre-commit]
Enter fullscreen mode Exit fullscreen mode

Then each developer runs pre-commit install once. Now git commit actually scans before anything leaves their machine.

Is it bulletproof? No — a developer can --no-verify past it. But it catches the accidental case, which is 99% of leaks.

CI-side scanning

Belt and suspenders. Run the same scan in CI so the --no-verify path also gets caught before merge:

# .github/workflows/secret-scan.yml
name: secret-scan
on: [pull_request]
jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          # Fetch full history so we scan the whole PR diff, not just HEAD
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

trufflehog is also worth a look — it actually verifies whether detected secrets are still live by calling the relevant APIs. Useful for triage when you find old hits.

Stop using long-lived static keys

This is the lesson the CISA incident really teaches: a static AKIA... key sitting in a config file is a footgun. Where possible, replace them with short-lived credentials.

  • For CI/CD pushing to AWS, use OIDC federation so GitHub Actions assumes a role with no static secret stored anywhere
  • For developer access, use SSO + aws sso login which issues credentials that expire in a few hours
  • For app workloads, use IAM roles attached to the compute resource

If there's no static key, there's nothing to leak.

Use a real secret manager for the rest

For the secrets that genuinely have to exist somewhere, store them in something built for it. OpenBao (the open-source fork of Vault) or AWS Secrets Manager will do. Your app fetches secrets at startup; nothing touches the repo.

A Reasonable Default Setup

If I'm starting a new repo today, this is the baseline before any application code is written:

  • .gitignore includes .env, .env.*, *.pem, *.key, terraform.tfstate*
  • pre-commit installed with gitleaks enabled
  • A CI job running gitleaks on every PR
  • All cloud access via OIDC or SSO — no static keys checked in anywhere
  • A SECURITY.md documenting the rotation process so the next person who panics has a runbook

Ten minutes of setup. Saves you the worst Slack message of your career.

Leaks happen to everyone — apparently including the agency that warns the rest of us about them. The point isn't to be perfect. The point is to make the accidental case loud enough that it never reaches the public internet in the first place.

Top comments (0)