You copy an n8n API key into a .env file to test something quick, commit the whole folder because you're in a hurry, notice a minute later, and delete the file in the next commit. Problem solved, right? Except git doesn't forget. The key is still sitting in that first commit, reachable by anyone who can run git log -p against your history, whether or not it's still in the file today.
That's not a hypothetical. GitGuardian scanned public GitHub commits in early August 2026 and found 4,576 distinct n8n API tokens sitting out in the open, tied to 1,255 different hostnames. Of the 896 instances they could actually reach, 321 accepted at least one of the leaked tokens, no exploit required, no password guessing, just an unrotated token from an old commit. A valid token hands over workflows, execution history, stored data tables, and enough about your credentials to know exactly what to go after next.
This walkthrough covers two things: a script that actually finds n8n keys in your git history (not just anything that looks vaguely like a secret), and what to do once you find one.
Blip:"I deleted the file" and "I removed the secret" are not the same sentence. Git remembers the first draft even after you've moved on from it.
Before you start
You'll need shell access to a local clone of any repo you want to check, and access to your n8n instance's Settings > n8n API page for the rotation step later. No n8n instance changes happen until Step 3, so it's safe to run the scan against a repo without touching anything live yet.
Step 1: Know what you're looking for
n8n's public API keys are JWTs, they start with eyJ, the standard base64 prefix for JSON, followed by two more base64 segments separated by dots. That shape isn't unique to n8n, plenty of other services hand out JWTs too, so a naive "does this look like a token" grep gets noisy fast. What is specific to n8n is what's inside the payload: n8n signs its API keys with "iss": "n8n" and "aud": "public-api" claims. Decode the middle segment of any JWT and you can tell immediately whether it's one of these keys or something else entirely.
Step 2: Scan your git history
This is the part worth actually running, not just reading. It walks every commit (git log -p --all), pulls out anything shaped like a JWT, decodes each candidate's payload, and only flags the ones whose iss/aud claims match n8n's:
#!/usr/bin/env bash
# Scans the full git history (not just the working tree) of the current repo
# for n8n public API keys. n8n API keys are JWTs, so a plain "looks like a
# JWT" grep also catches unrelated tokens; this decodes each candidate's
# payload and only flags the ones whose issuer/audience claims match n8n's.
set -euo pipefail
candidates=$(git log -p --all -- . 2>/dev/null \
| grep -oE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' \
| sort -u || true)
if [-z "$candidates"]; then
echo "No JWT-shaped strings found in git history."
exit 0
fi
found=0
while IFS= read -r token; do
payload_b64=$(echo "$token" | cut -d. -f2)
padded=$(echo "$payload_b64" | tr '_-' '/+')
case $(( ${#padded} % 4 )) in
2) padded="${padded}==" ;;
3) padded="${padded}=" ;;
esac
payload=$(echo "$padded" | base64 -d 2>/dev/null || true)
if echo "$payload" | grep -q '"iss": *"n8n"' && echo "$payload" | grep -q '"aud": *"public-api"'; then
found=$((found + 1))
echo "FOUND n8n API key in git history: ${token:0:24}...${token: -8}"
echo " payload: $payload"
fi
done <<< "$candidates"
if ["$found" -eq 0]; then
echo "No n8n API keys found (JWTs present but none matched n8n's issuer/audience claims)."
exit 0
fi
echo ""
echo "$found n8n API key(s) found in git history. Deleting the file today does NOT remove"
echo "them, they're still reachable via 'git log -p' or any clone. Rotate every key found"
echo "here, then scrub history (git filter-repo / BFG) if this repo is or was ever public."
exit 1
Save it as scan-n8n-tokens.sh, chmod +x it, and run it from inside the repo you want to check:
./scan-n8n-tokens.sh
I tested this against two throwaway repos before writing it into this post. One had an n8n key committed and then "removed" in a later commit, exactly the scenario above, and the script caught it and printed the payload showing iss: n8n, aud: public-api. The other had a JWT-shaped decoy with a completely different issuer, and the script correctly ignored it, so you won't get paged for every unrelated token your history happens to contain. A clean repo with no JWTs at all reports that plainly and exits 0, so you can drop this into a pre-merge CI check without it crying wolf.
Run it against every repo that's ever touched an n8n key, including ones you've since made private. GitGuardian's numbers came from public GitHub commits specifically, but a repo you flipped to private after the fact doesn't retroactively un-leak anything that was already indexed or cloned while it was public.
Step 3: Rotate anything you find
If the scan turns up a hit, treat that key as burned, full stop, whether or not you can prove anyone actually used it. Rotation itself is a UI action in n8n, not something scriptable through the public API (you'd otherwise need a valid key just to revoke itself, which doesn't help you here):
- Open Settings > n8n API on the affected instance.
- Click Create an API key , give it a label you'll recognize later, and set an Expiration if your plan supports it, an expiring key limits exactly this kind of damage the next time a token slips into a commit.
- Update every place that used the old key, your automation scripts, CI secrets, anywhere it was configured, to the new one.
- Go back to Settings > n8n API , find the old key in the list, and select Delete next to it. It's revoked immediately.
Do this even if the leaked key is months old and "probably fine." GitGuardian's whole point was that these tokens don't expire on their own, an n8n API key created without an expiration date stays valid indefinitely until someone deletes it, so "probably fine" and "still fully valid" are the same thing from the token's perspective.
Step 4: Get it out of history too, if the repo was ever public
Rotating the key stops it from working, but the string itself is still sitting in your git history, which is untidy and, if this repo is open source or shared, worth cleaning up so the next contributor doesn't find it and wonder if it's still live. git filter-repo (or the older BFG Repo-Cleaner) can strip a specific string from every commit that ever contained it. This rewrites history, so it needs a force-push and coordination with anyone else who has the repo cloned, which is a bigger operation than this post covers, but it's the right follow-up once the immediate danger (a working, unrevoked key) is handled.
What this doesn't solve
Scanning your own repo's history only tells you what's true for that repo. It can't tell you whether someone already cloned it while the key was live, whether it's sitting in a CI log, a Slack message, or a support ticket screenshot, or whether it leaked through a channel that has nothing to do with git at all. Treat "I found it and rotated it" as closing the door you know about, not a guarantee nothing walked through it first.
It also won't catch a key that's still currently committed in your working tree, only what's reachable through history. Run a plain git grep for eyJ against your current checkout too, and consider adding GitHub's push protection (Settings > Code security > Push protection) on any repo that might touch one of these keys again, it blocks a commit containing a recognizable secret pattern before it ever reaches the remote, which is a much better place to stop this than a post-hoc scan.
Where to start
Run the scan today against every repo that has ever had an n8n key anywhere near it, not just the ones you remember. If it comes back clean, good, now you know, and it's a five-minute script you can rerun anytime you're not sure. If it finds something, rotate the key before you do anything else, the five minutes that takes is a lot cheaper than being one of the 321 instances that answered when GitGuardian knocked. It's one item on a short list of n8n instance-hardening checks worth running together, alongside the sandbox-escape vulnerability patch and the $fromAI prototype-leak escape hatch, both quiet-until-someone-finds-it bugs in the same vein as an unrotated key.
Top comments (0)