GitHub Recon: Finding Secrets in Public Repositories
GitHub recon turns public repositories into a source of live secrets: API keys, tokens, and cloud credentials. This guide covers code search operators, scanning git history with trufflehog and gitleaks, safe triage, and how to lock it all down.
GitHub recon is the practice of reading an organisation's public repositories for secrets its developers never meant to ship: API keys, access tokens, cloud credentials, internal hostnames, and config files that were supposed to stay private. The code is public on purpose, but the secrets inside it are almost never public on purpose. That gap is one of the most reliable ways into an environment, and it costs an attacker nothing but patience. This guide walks the whole workflow: what leaks and why, how to search for it with GitHub's own operators, why the git history is where the real treasure sits, how to automate the hunt with trufflehog and gitleaks, how to verify a finding without abusing it, and how a defender shuts the whole thing down.
Read it top to bottom once to build the method, then keep it open as a checklist. Every example here points at a repository you own or are authorized to assess.
Run this only against repositories and organisations you own or have written permission to assess, or your own lab accounts. The examples use example-org/example-repo and the lab host lab.0xpriv.dev. Searching is passive, but acting on someone else's live credentials without authorization is a crime. Find it, prove it is real with the lightest possible check, and report it.
01Why public repositories leak secrets and what to look for
GitHub recon works because public repositories leak secrets constantly, and they do it for ordinary, human reasons. A developer hardcodes an API key to test something quickly and forgets to pull it back out. A .env file gets committed before anyone writes a .gitignore. A config template ships with a real token instead of a placeholder. A private repo is made public during a cleanup, dragging its entire history along. None of this is exotic. It is the default failure mode of fast-moving teams, which is exactly why recon pays off.
Knowing what a secret looks like is half the battle, because most of them follow recognisable shapes. The table below is the short list worth memorising before you start searching.
| What leaks | What it looks like |
|---|---|
| AWS keys | AKIA... access key id next to a 40-char secret |
| Cloud / SaaS tokens | GitHub ghp_..., Slack xoxb-..., Stripe sk_live_... |
| Private keys | a BEGIN RSA PRIVATE KEY or BEGIN OPENSSH PRIVATE KEY header |
| Config files | .env, config.php, application.yml, .npmrc, id_rsa |
| Internal hostnames | names like db-internal.lab.0xpriv.dev or RFC1918 hosts such as 10.10.10.5 |
Internal hostnames and connection strings matter as much as keys. A database DSN or an internal URL is not a password, but it maps the environment and tells you what a leaked key is for. Treat the whole picture as the prize, not just the token.
02GitHub code search operators and useful queries
GitHub code search is the first tool for GitHub recon because it reads the contents of files across indexed repositories, and a few operators narrow a giant haystack to a handful of candidates. You run it logged in, either at github.com/search with the type set to Code or from the command line with gh search code. Searches combine free-text content terms with qualifiers that scope where to look.
# 0xpriv.dev/recon -- scope every query to a target you are authorized to assess # content term + org qualifier: find a variable name across one org gh search code "AWS_SECRET_ACCESS_KEY" --owner example-org # path qualifier: config files that should never be committed gh search code "path:**/.env DB_PASSWORD" --owner example-org gh search code "path:**/application.yml password" --owner example-org # match the header of a private key in file contents gh search code "BEGIN RSA PRIVATE KEY" --owner example-org # narrow by language or repo gh search code "api_key" --owner example-org --language php gh search code "ghp_" --repo example-org/example-repo
Two habits make the difference. Search for the variable name, not the value, because AWS_SECRET_ACCESS_KEY or DB_PASSWORD appears right next to whatever was assigned to it. And scope every query with --owner or --repo so you stay on the target you are allowed to test. The web UI uses the same qualifiers: org:example-org path:.npmrc _authToken reads the same way in the search bar.
GitHub code search only indexes the default branch of a repository, and only the current version of each file. It does not read other branches, and it does not read deleted content. A secret that was removed in a later commit will not show up here at all, which is the single most important limitation to understand before you trust an empty result.
03Why git history matters: deleted is not gone
Git history is where most real secrets hide, because a secret deleted from the current files still lives in every old commit that ever contained it. When a developer notices a committed key and removes it, the removal is just another commit. The key is still sitting in the parent commit, fully intact, and anyone who clones the repository gets the entire history by default. Code search cannot see it. A history scan can.
This is why the hunt has two stages: search the visible files first, then clone the repository and walk its commits. The diagram shows the trap. The current tree looks clean, but the object sitting one commit back is not.
You can confirm this by hand in seconds. Clone the repository, find the commit that touched the sensitive file, and read it back out of history.
$ git clone https://github.com/example-org/example-repo $ cd example-repo $ git log --oneline --all -- .env a1b2c3d remove committed .env (oops) f4e5d6c add initial service config $ git show f4e5d6c:.env DB_PASSWORD=S3rv1ce-Db-Pass! AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
04Automated scanning with trufflehog and gitleaks
Automated scanners do the history walk for you and match hundreds of secret patterns in one pass, which is why trufflehog and gitleaks are the backbone of serious GitHub recon. Doing a few extractions by hand builds the intuition, but no human greps every commit of a busy repository. The two tools overlap and are worth running together, because each catches things the other misses.
trufflehog (by Truffle Security) scans a repository and its full git history, and its best feature is verification: it takes a candidate secret and actively checks it against the provider to tell you whether the credential is still live. Point it at a repo by URL or at a local clone.
# scan a single repo straight from GitHub, including its history trufflehog github --repo=https://github.com/example-org/example-repo --only-verified # scan a local clone's full git history git clone https://github.com/example-org/example-repo trufflehog git file://example-repo --only-verified
The --only-verified flag is the one that saves your time: it drops the thousands of dead and example keys and leaves the credentials that actually authenticate right now.
$ trufflehog git file://example-repo --only-verified Found verified result Detector Type: AWS Decoder Type: PLAIN Verified: true Repository: https://github.com/example-org/example-repo Commit: f4e5d6c9a01b2c3d4e5f6a7b8c9d0e1f2a3b4c5d File: .env
gitleaks (an open-source project under the gitleaks org) scans git history by default and runs on a large ruleset of regex patterns. It is fast, easy to drop into a pipeline, and writes a clean JSON report you can triage later.
# scan the full history of a local clone and write a JSON report gitleaks detect --source example-repo --report-format json --report-path leaks.json --verbose # scan only the current files, no history (quick sanity pass) gitleaks detect --source example-repo --no-git
$ gitleaks detect --source example-repo --verbose Finding: AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/... Secret: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY RuleID: aws-access-token File: .env Commit: f4e5d6c9a01b2c3d4e5f6a7b8c9d0e1f2a3b4c5d 2 leaks found
05Scanning a whole organisation and its forks
Scanning a whole organisation widens GitHub recon from one repository to every repository a target owns, and that is usually where the overlooked one hides. trufflehog takes an --org flag and will enumerate and scan all of an organisation's public repositories in a single run. The oldest, least-watched side project is often the one with the committed key, so breadth matters more than depth here.
# scan every public repo in an organisation trufflehog github --org=example-org --only-verified # include forks: a fork can carry a secret the upstream never had trufflehog github --org=example-org --include-forks --only-verified
Forks deserve their own pass. A developer forks a repository into their personal account, commits a working config with real credentials to test locally, and never pushes it upstream. The fork is still public, still searchable, and still tied by name to the target. The --include-forks flag tells trufflehog to follow them. The same logic applies to the personal accounts of known employees, which you find from the organisation's member list and from commit author data: people leak their work secrets in their own repos all the time.
A quick verified-only scan across every repo and fork beats an exhaustive scan of one repo. Sweep wide first, rank by what came back verified, then dig into the handful of repositories that actually produced a live hit.
06Verifying and triaging a found secret without abusing it
Verifying a found secret means proving it is real and live with the lightest possible touch, and then stopping. This is the line between a professional assessment and a breach. The goal of recon is to show that a valid credential is exposed and to report it, not to see how far it goes. One read-only identity check is enough to prove the point.
Pick the single call that confirms the key works and reveals who it belongs to, without reading data, listing resources, or changing anything. For an AWS key, that is the identity lookup. For a GitHub token, it is the current-user endpoint, which also returns the token's scopes in a response header.
# AWS: confirm the key is live and whose account it is. reads nothing else. AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... aws sts get-caller-identity # GitHub token: who it is and what scopes it has (-I shows the X-OAuth-Scopes header) curl -sI -H "Authorization: token ghp_REDACTED" https://api.github.com/user
Record the proof, redact the secret in your notes and report, and move on. The steps below are the whole triage routine.
Classify the secret
Identify the provider and type from its prefix or format, so you know which single read-only call will confirm it.
Confirm it is live
Run one identity or whoami call. A verified scanner result plus one manual check is all the proof you need.
Scope the impact
Note what the credential is and what it could reach, from its type and scopes, without actually exercising that access.
Report and stop
Write it up with the repo, commit hash, and file path, redact the value, and hand it to the owner to rotate.
Do not use the credential beyond a single identity check. No listing buckets, no reading mail, no creating resources, no lateral movement. Even in an authorized engagement, confirm exposure and report it unless your rules of engagement explicitly say otherwise in writing.
07A worked example: scanning one repo end to end
A worked example ties the whole GitHub recon method together on a single repository, from search to a verified, triaged finding. Say your scope lists example-org. You start with code search, find nothing in the current files, and then do the history scan that code search cannot. That gap is the lesson of the whole exercise.
$ gh search code "AWS_SECRET_ACCESS_KEY" --owner example-org no code results matched your search $ trufflehog github --org=example-org --include-forks --only-verified Found verified result Detector Type: AWS Verified: true Repository: https://github.com/example-org/example-repo Commit: f4e5d6c File: .env
Code search found nothing because the .env was deleted months ago, so it is absent from the current default branch. trufflehog found it because it read the history, and it marked the result verified, which means the key still authenticates. Now you take exactly one confirming step and then write the finding up.
$ aws sts get-caller-identity { "UserId": "AIDAEXAMPLEEXAMPLE", "Account": "210987654321", "Arn": "arn:aws:iam::210987654321:user/ci-deploy" }
That single response is the entire proof: a live AWS key, tied to a ci-deploy user, committed in example-repo at commit f4e5d6c. The report writes itself, the secret gets redacted, and the owner rotates it. No bucket was listed, nothing was touched. That restraint is what makes it an assessment and not an incident.
08Detecting and monitoring leaked secrets
Detecting leaked secrets is the defender's side of the same coin, and it is mostly about watching the same surfaces an attacker searches. The strongest signal is GitHub's own secret scanning, which GitHub runs on public repositories and which alerts you when a recognised credential format is pushed. Beyond that, treat your public code as something to monitor continuously, not to check once.
Three signals are worth wiring up. Run trufflehog or gitleaks on a schedule against your own organisation, so you find a leak the same way an attacker would, only first. Subscribe to GitHub secret scanning alerts for every public repo. And watch provider-side logs: an AWS key appearing from a new region or a GitHub token used from an unfamiliar address, soon after a commit, is the shape of a leaked credential being tried. Alert on the credential use, not just on the commit, because the commit is often months old by the time anyone looks.
09Shutting it down: how to stop secrets reaching GitHub
Shutting down secret leaks is a layered job, because no single control catches everything, and the most important move is the one people skip: rotate, do not just delete. Deleting a secret from the current files leaves it alive in history and still valid at the provider, so a quiet delete fixes nothing. The box below is the full set, from keeping secrets out of commits to limiting the damage when one slips through.
The pre-commit hook is the cheapest win, so wire it in first. gitleaks plugs straight into the pre-commit framework:
# .pre-commit-config.yaml -- block secrets before they are committed
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks10Frequently asked questions
Is GitHub recon legal if the repositories are public?FAQ
Reading and searching public code is generally fine, but using a credential you find is not. The moment you authenticate with someone else's exposed key beyond proving it is live, you are accessing a system without authorization, which is a crime in most places. Keep it to scoped, authorized targets and report what you find.
Why does code search miss secrets that trufflehog finds?FAQ
GitHub code search only indexes the current version of files on the default branch. A secret that was committed and later removed is gone from the current files, so search returns nothing, but it still exists in the git history. trufflehog and gitleaks read the full history, which is why they catch what search cannot.
trufflehog or gitleaks: which should I use?FAQ
Run both. gitleaks is fast and great in a pipeline, and it scans history out of the box. trufflehog adds live verification, so it tells you which of the hundreds of candidate keys still authenticate right now. Using them together gives you broad coverage plus a short, verified shortlist to act on.
I committed a secret by accident. Is deleting the file enough?FAQ
No. Deleting the file in a new commit leaves the secret in every old commit, and anyone who cloned the repo already has it. Assume it is compromised: rotate the credential at the provider first so the old value stops working, then rewrite history to remove it. Rotation is the step that actually matters.
Does GitHub secret scanning make my own hunting pointless?FAQ
No. Secret scanning and push protection cover the formats GitHub recognises, but they miss custom tokens, internal hostnames, and secrets in formats without a known signature. Running your own trufflehog and gitleaks sweeps on a schedule catches the rest, and it finds leaks before an outsider does.
11Wrapping up
GitHub recon comes down to one habit: assume the secret is in the history, not the current files. Search the visible code with GitHub's operators to get oriented, then clone and scan the full history with trufflehog and gitleaks, because that is where the removed-but-never-rotated keys live. When something turns up, prove it is real with a single read-only check and report it, nothing more. On the defender's side the lesson flips neatly: keep secrets out of commits with pre-commit scanning and a secret manager, turn on push protection, and when one does leak, rotate it rather than quietly deleting it. The hunt and the fix are the same story told from two sides.
references
Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.