No Plaintext Anywhere: A Secrets Pass Over the Whole Lab

I was about to set up automatic mirroring from my self-hosted GitLab to GitHub, which means every commit I make at home gets relayed to someone else’s servers within seconds of me making it. That seemed like the right moment to stop and ask what’s actually in these repos. Not just the current files, the whole history. A secret you deleted in the latest commit is still sitting in every clone of the repo, forever.

So before wiring anything up I went looking for credentials in all the places they hide.

History first. I ran git log with the patch output across every branch and grepped it for token prefixes: GitHub tokens, GitLab tokens, AWS keys, private key headers. Every provider stamps a recognizable prefix on their tokens these days, which makes this kind of scan cheap insurance. My repos came back clean, which I’ll admit surprised me.

The current state of things was less clean. One repo had an access token pasted directly into the git remote URL, the “just make it work” shortcut from months ago that I’d forgotten about. Another directory turned out to be a stale copy of a repo with an API token hardcoded into a config file, and a container was still mounting it and reading from it. Neither one would show up in any repo scan, because neither one was ever committed. Secrets rot worst in the copies you forgot you made.

The config backups were the part I’d already done right, and it’s the part I would tell anyone to do first. My switch and firewall configs get backed up automatically, and a raw Cisco config is a credential goldmine: enable secrets, SNMP strings, user hashes. The backup playbook runs every config through Ansible Vault before it ever touches the repo, so the repo has never held a plaintext config in any commit. The vault password isn’t committed either. Locally it gets read from a file in my home directory, and on the CI runner it comes out of the runner’s encrypted credential store.

Token scope got an overhaul too. The old setup used one personal access token for everything, which meant my entire account’s power in one string. Now the CI runner has a project access token that can clone exactly one repo and nothing else, and the backup job has a separate write token that can push to exactly one repo. If either leaks, the blast radius is one project instead of my whole account. Same reason you don’t hand every tech the enable password.

The token I found in that remote URL got revoked the same hour. Git moved to SSH keys, and API access got a fresh token stored in a permissions-locked file. Rotation feels like busywork right up until the day it isn’t.

The last thing I’d point out is that gitignore is a floor, not a ceiling. An ignored file still sits on disk in plaintext, still gets swept up when you copy a directory, still lands in backups. Gitignore keeps secrets out of git. It doesn’t make them safe. Encrypt what you can, lock down permissions on what you can’t, and delete the stale copies outright.

None of this needed special tooling. Grep, Ansible Vault, and token scoping GitLab already ships. The expensive part was going and looking in the places where past me took shortcuts, because every one of those shortcuts was invisible until the day the repos were about to leave the house.