#The situation
A colleague writes: "The site's app.js has an AWS access key in plain text." You find the key hard-coded in the repository's src/upload.js. It was built and copied to the public folder, and the source map (app.js.map) went public with it.
When a key or token reaches a public place, there are only a few decisions to make. But get the order wrong and the key stays usable the whole time. Revoke before you delete. Revoke before you rewrite history. This playbook turns that order, and the investigation, into steps.
Key formats and how the scans work are covered in Credential theft and leaked secrets. Here you run the decisions and steps after a find, on a disposable VM.
| Step | What you do | What you use |
|---|---|---|
| 1. See | Find the key in the repository and the public folder | API key and private key detection, Secrets in published front-end files |
| 2. Investigate | Since when, where, and who could fetch it | git log -S, the access log, Access log diagnosis (AccessScope) |
| 3. Respond | Revoke at the issuer, handle history, replace | The issuer's console, git filter-repo |
| 4. Prevent | Stop a publish that still carries a key | The exit code of roamswitch scan-frontend |
| 5. Clean up | Remove the practice setup |
The key in this playbook is the invalid sample from the AWS documentation (AKIAIOSFODNN7EXAMPLE and its paired secret access key). No real key is used. Every scan here only reads local files and sends nothing to any outside service. All requests to the site are made from inside the VM.
#Set up the practice
Install nginx and git-filter-repo on the VM, and create a dummy repository. The second commit adds the key, and the built app.js and its source map go into the public folder. The commit dates are set to fit the story.
This installs packages and writes files (it changes the system). Cleanup is after the last lab.
sudo apt-get install -y nginx git-filter-repo
mkdir -p ~/shop/src && cd ~/shop
git init -q -b main
git config user.name "Dev Sample"; git config user.email dev@example.com
echo '# shop (dummy for practice)' > README.md
printf 'import { upload } from "./upload.js";\nupload();\n' > src/main.js
git add -A
GIT_AUTHOR_DATE="2026-10-20T10:00:00+09:00" GIT_COMMITTER_DATE="2026-10-20T10:00:00+09:00" \
git commit -qm "Initial commit"
cat > src/upload.js <<'J'
// Practice only: the invalid sample key from the AWS documentation
const cfg = {
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
};
export function upload() { return cfg.accessKeyId.length; }
J
git add -A
GIT_AUTHOR_DATE="2026-10-22T15:30:00+09:00" GIT_COMMITTER_DATE="2026-10-22T15:30:00+09:00" \
git commit -qm "Add image upload"
# Build and publish (the way the key reached the public folder)
mkdir -p dist
{ cat src/upload.js src/main.js; echo '//# sourceMappingURL=app.js.map'; } > dist/app.js
echo '{"version":3,"sources":["src/upload.js","src/main.js"],"mappings":""}' > dist/app.js.map
sudo cp dist/app.js dist/app.js.map /var/www/html/
git log --oneline96653d2 Add image upload
c5e1f37 Initial commit#Read the findings
A key ends up in two kinds of place: the source in the repository, and the build output in the public folder. There is one command for each.
Both only read; nothing is changed. Use audit-secrets for the repository and scan-frontend for the public folder.
cd ~
roamswitch audit-secrets ./shop
sudo -E roamswitch scan-frontend /var/www/html
echo "exit code: $?"=====================================================
🔑 Secret & API Key Leak Audit (directory)
=====================================================
Scanned 3 files.
./shop/src/upload.js
⚠️ AWS Access Key ID (AWS) — line 3
Masked: AKIAIO...MPLE
↳ Recommendation: Deactivate the access key in the AWS IAM console.
Found 1 exposure across 1 file. Address these before committing or sharing.
=====================================================
Check of the files a web front end publishes
=====================================================
Looked at: . (3 files)
- Secret (API key, private key, ...): app.js:3 AWS AKIAIO...MPLE
- Source map (hands out the original source code): app.js.map
2 findings (secrets 1, tokens 0, source maps 1)
A key or token found here has already left your hands if the files are published: revoke it at its issuer and create a new one. Keep secrets out of the build and on the server. Do not publish source maps, or restrict who can fetch them.
exit code: 1audit-secretslooked at 3 files. It does not enter.gitordist. The published build is whatscan-frontendis for- The secret access key on line 4 of
upload.jsis not reported, because it has no fixed shape. Once its paired ID (line 3) is found, treat the whole key pair as leaked scan-frontendalso lists the source map. A source map lets anyone read the original sourcescan-frontendexits with code 1 when it finds something, and so doesaudit-secrets. The last section uses that
#Since when, where, and who could fetch it
You do not need a long investigation before revoking. But to know what to look for in the issuer's logs, pin down since when the key was public. Here you first play outside visitors from inside the VM, then investigate.
The first block makes requests from an ordinary visitor and from something hunting for .env and the like. The VM's own address stands in for an outside client.
IP=$(hostname -I | awk '{print $1}')
curl -s -o /dev/null -A "Mozilla/5.0" http://127.0.0.1/app.js
for p in /.env /.git/config /app.js /app.js.map /.aws/credentials; do
curl -s -o /dev/null -A "python-requests/2.31" "http://$IP$p"
donecd ~/shop
git log -S AKIAIOSFODNN7EXAMPLE --date=iso --format='%h %ad %an <%ae> %s'
stat -c '%n %U:%G %a %y' /var/www/html/app.js src/upload.js
sudo grep -E '"GET /app\.js' /var/log/nginx/access.log
sudo -E accessscope scan /var/log/nginx/access.log96653d2 2026-10-22 15:30:00 +0900 Dev Sample <dev@example.com> Add image upload
/var/www/html/app.js root:root 644 2026-11-12 01:37:56.696528531 +0000
src/upload.js admin:admin 644 2026-11-12 01:37:56.595875759 +0000
127.0.0.1 - - [12/Nov/2026:01:37:56 +0000] "GET /app.js HTTP/1.1" 200 327 "-" "Mozilla/5.0"
192.168.64.12 - - [12/Nov/2026:01:37:56 +0000] "GET /app.js HTTP/1.1" 200 327 "-" "python-requests/2.31"
192.168.64.12 - - [12/Nov/2026:01:37:56 +0000] "GET /app.js.map HTTP/1.1" 200 70 "-" "python-requests/2.31"
| # | Severity | Confidence | Finding | Count | Main sources |
|---|---|---|---|---|---|
| 1 | Low | Likely | Probing for secrets, config files and admin pages | 3 (1 IP) | 192.168.64.12(3) |- Since when:
git log -Sshows the commit that added the string. The key went in on 22 October at 15:30 JST (06:30 UTC). Search the issuer's logs from that moment on - Where: the
stattime is when the file was placed in the public folder, in UTC (+0000). In real operations, the public period also includes any earlier deploys - Who could fetch it: two clients received
app.jswith a 200.app.jsis part of an ordinary page, so every visitor received the key - The 3 requests AccessScope reports are the hunts for
.env,.git/configand.aws/credentials.--report FILEprints the evidence log lines. Fetching an ordinary file does not count as probing
The access log tells you who received the file. It does not tell you whether the key was used. Only the issuer's activity log (CloudTrail for AWS, for example) shows that.
#The order of revocation
Once a key is in a public place, revoke it at the issuer before you delete any file. This one step happens outside RoamSwitch. The scan's own recommendation says the same: deactivate the access key in the AWS IAM console.
- Revoke at the issuer. For AWS, deactivate the access key in IAM
- Look for signs of use. Read the issuer's activity log from the moment the key went in. The log stays after revocation
- Create a new key and change where it lives. Do not put it in any file that reaches the browser
- Remove it from the source and the history. The next section covers this
- Add a check that stops a publish. The last section covers this
Revoking a key stops whatever ran on it. A key that reached a public place (a public site, a public repository) is revoked at once, and you accept the outage, because you cannot know who holds it. If the key was only in an internal repository and the readers are limited, you may create the new key first, swap it in, and revoke the old one right after. Either way, the old key is always revoked.
- Delete the key from the file and stop there, without revoking it
- Rewrite history before revoking (the key stays usable meanwhile)
- Try the leaked key yourself to "see if it still works"
- Paste the full key into a chat or issue to share the situation (use the masked form)
#Handling the history
Deleting the key from the source and committing does not remove it: the key stays in the earlier commits. A folder scan with audit-secrets does not enter .git, so the history needs its own check.
Pretend the code that uses the key moved to the server: rewrite upload.js and commit. Then scan the current files and every past version.
cd ~/shop
cat > src/upload.js <<'J'
// The key now stays on the server. The page asks the server for a one-time upload URL.
export async function upload() { return (await fetch("/api/upload-url")).ok; }
J
GIT_AUTHOR_DATE="2026-11-04T10:50:00+09:00" GIT_COMMITTER_DATE="2026-11-04T10:50:00+09:00" \
git commit -qam "Move upload signing to the server"
roamswitch audit-secrets ./src # the current files
roamswitch audit-secrets "$(git log -p --all)" # every past version=====================================================
🔑 Secret & API Key Leak Audit (directory)
=====================================================
Scanned 2 files.
No secret or API key exposure detected.
=====================================================
🔑 Secret & API Key Leak Audit
=====================================================
Detected 2 exposed secrets or API keys. Remove them before committing or sharing.
⚠️ AWS Access Key ID (AWS): line 14
Masked: AKIAIO...MPLE (entropy: 3.68)
↳ Recommendation: Deactivate the access key in the AWS IAM console.
⚠️ AWS Access Key ID (AWS): line 35
Masked: AKIAIO...MPLE (entropy: 3.68)
↳ Recommendation: Deactivate the access key in the AWS IAM console.Nothing is found in the current files. The history diff (git log -p) has two hits: the removed line (-) and the added line (+). The line numbers are lines of the git log -p output. Anyone with the repository can read the old version with git show.
Rewriting history does not remove the key from clones people already have, or from copies kept wherever it was published. The fix for a leaked key is revocation. A rewrite changes commit IDs and everyone has to clone again. Rewrite anyway when you want a revoked key to stop showing up in every scan, or you do not want it in the repository you hand out from now on. Do it after revoking.
git filter-repo replaces the key with REMOVED in every commit. This rewrites history (it changes the system). Make a full copy first so you can go back.
cd ~
cp -a shop shop.bak # a backup (it still holds the key)
cd shop
printf '%s\n' 'AKIAIOSFODNN7EXAMPLE==>REMOVED' \
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY==>REMOVED' > /tmp/replace.txt
git log --oneline
git filter-repo --force --replace-text /tmp/replace.txt
git log --oneline
roamswitch audit-secrets "$(git log -p --all)"9aa9bba Move upload signing to the server
96653d2 Add image upload
c5e1f37 Initial commit
Parsed 3 commitsHEAD is now at 7e184d5 Move upload signing to the server
New history written in 0.02 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
Completely finished after 0.08 seconds.
7e184d5 Move upload signing to the server
ea1a703 Add image upload
c5e1f37 Initial commit
=====================================================
🔑 Secret & API Key Leak Audit
=====================================================
No exposed secrets or API keys were detected.- The first commit (
c5e1f37), which never held the key, keeps its ID. Every commit from the one that added the key onward gets a new ID - Scanning the rewritten history finds nothing
- To go back, put
shop.bakback in place
git filter-repo rewrites the local repository's history. Applying it to a shared repository needs a force push, and everyone else must clone again. The backup shop.bak still holds the key. Delete it once you have checked the result.
#Replace, and check before publishing
The new key never goes into a file that reaches the browser. In this story, the server now issues the upload URL (the commit in the previous section). Next, make sure a publish that still carries a key stops. scan-frontend exits with 1 when it finds something; put it in the middle of the publish steps.
A short script that builds, checks, and copies to the public folder only when the check passes. Try it on the version that held the key (the backup moved back one commit) and on the current one. Finally, delete the source map still in the public folder (this changes the system).
cd ~
cat > deploy.sh <<'D'
#!/bin/sh
# Build into dist, check it, then copy it to the public folder
set -e
cd "$1"
rm -rf dist && mkdir dist
cat src/upload.js src/main.js > dist/app.js
roamswitch scan-frontend dist || { echo "Publishing stopped"; exit 1; }
sudo cp dist/app.js /var/www/html/
D
chmod +x deploy.shcd ~
git -C shop.bak checkout -q HEAD~1 # the version that held the key
./deploy.sh ~/shop.bak; echo "exit code: $?"
./deploy.sh ~/shop; echo "exit code: $?"
sudo rm /var/www/html/app.js.map
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/app.js.map
sudo -E roamswitch scan-frontend /var/www/html=====================================================
Check of the files a web front end publishes
=====================================================
Looked at: . (1 file)
- Secret (API key, private key, ...): app.js:3 AWS AKIAIO...MPLE
1 finding (secrets 1, tokens 0, source maps 0)
A key or token found here has already left your hands if the files are published: revoke it at its issuer and create a new one. Keep secrets out of the build and on the server. Do not publish source maps, or restrict who can fetch them.
Publishing stopped
exit code: 1
=====================================================
Check of the files a web front end publishes
=====================================================
Looked at: . (1 file)
Nothing was found in the files that would be published
exit code: 0
404
=====================================================
Check of the files a web front end publishes
=====================================================
Looked at: . (2 files)
Nothing was found in the files that would be published- The version with the key stops at the check and is not copied to the public folder
- The current version passes with exit code 0 and replaces
app.js - With the source map deleted,
app.js.mapreturns 404. A fresh scan of the public folder finds nothing
Clean up the practice. The backup still holds the key, so it goes too. nginx and git-filter-repo were installed only for this practice; remove them only on a disposable VM.
cd ~
rm -rf shop shop.bak deploy.sh /tmp/replace.txt
sudo rm -f /var/www/html/app.js
sudo apt-get purge -y -qq nginx nginx-common git-filter-repo > /dev/null
sudo rm -rf /var/www/html /var/log/nginx
systemctl is-active nginxinactive#Blind spots and review
- Only the published invalid sample key was used. Revoking at the issuer, reading its activity log, and issuing a new key happen outside RoamSwitch and were not run here
- The scans find keys in formats they know. Here too, the secret access key was not reported. "Not found" does not prove "not there"
- In a folder scan,
audit-secretsdoes not enter.git,node_modules,dist,buildand the like. Check the history by passing itgit log -p scan-frontendreads files ending injs,mjs,cjs,css,map,html,jsonandtxt, plus.env,.env.*and*.env(checked in the code). Also runaudit-secretsto catch a key in a file with any other name- The access log shows only who fetched a file. Copies kept by a CDN, a search engine cache or someone's clone are invisible from this server
You removed the key from the live app.js and redeployed, and scan-frontend now finds nothing. What do you do with the key?
Show the explanation
Every visitor received the
app.jsthat held the key. Fixing the file does not erase the copies they received. The core of the response is revoking at the issuer.
After deleting the key from the source and committing, roamswitch audit-secrets ./shop finds nothing. Which reading is right?
Show the explanation
A folder scan does not enter
.git. To check past versions, pass the output ofgit log -ptoaudit-secrets.
Next, Credential theft and leaked secrets covers the key formats the scans find and how to handle source maps. For a web shell placed in the public folder, see A web shell was found.
- API key and private key detection
- Secrets in published front-end files
- Access log diagnosis (AccessScope)
Related techniques (ATT&CK): T1204.004
Verified on 2026-11-12. Target: Linux 1.17.39. On a disposable Ubuntu 24.04 VM with the published 1.17.39, ran every lab in Japanese and English with a dummy repository, nginx and the invalid AWS sample key.
Command output varies with the product version and the environment. If yours differs, check the version first.