I was getting ready to mirror my self-hosted GitLab repositories to GitHub. That meant every commit made at home could be copied to an outside service within seconds, so I stopped and checked what was actually in the repositories first.
The current files were only part of the audit. A credential removed from the latest version can still be present in Git history and in old clones unless the history is rewritten and every copy is cleaned up.
Scan the history, not just the current files
Many providers use recognizable prefixes for their tokens. That makes a basic history scan easy to run with tools already installed:
git log -p --all | grep -nE \
'ghp_[A-Za-z0-9]{36}|glpat-[A-Za-z0-9_-]{20}|AKIA[0-9A-Z]{16}|BEGIN (RSA|OPENSSH|EC) PRIVATE KEY'
I ran it against each repository. For a larger group of repositories, gitleaks provides a much broader pattern library and is easier to automate. The history scan came back clean.
Check the places Git never sees
The live systems had two problems that a repository scan could not find because neither credential had been committed.
One repository still had an access token embedded in its remote URL from an old "make it work" setup:
git remote -v
# origin https://oauth2:glpat-XXXXXXXX@gitlab.lab/me/repo.git
The other problem was a stale copy of a repository with an API token hardcoded in a config file. A container was still mounting that old directory. I removed the copy, revoked the exposed token, and moved Git access to SSH keys.
Put automation credentials in Vault
I originally kept a few automation passwords in files protected with mode 600. That kept other local users out, but the values were still plaintext on disk. I replaced those files with HashiCorp Vault.
The credentials now live under separate paths such as secret/lab/hostinger, secret/lab/gitlab, and secret/lab/ansible. Scripts authenticate with AppRole and receive short-lived Vault tokens. Each consumer gets a policy limited to the secret paths it needs.
For example, local automation captures one field into a process variable without putting the value into a repository or command-line argument:
wp_cred="$(/usr/local/bin/vault-get.sh lab/hostinger wp_app_password)"
The script can pass that variable directly to the API client and discard it when the process exits. The credential never needs to be written back to disk or printed into a log.
Encrypt configuration backups before Git sees them
HashiCorp Vault protects the credentials used by automation. Ansible Vault still handles configuration files that need to be stored in Git.
A raw Cisco configuration can contain enable secrets, SNMP strings, and user hashes. The backup job encrypts each configuration before committing it:
ansible-vault encrypt backups/switch01.cfg
ansible-vault view backups/switch01.cfg
Before those files are backed up, the job also runs gitleaks across them. That gives me another check for token and private-key patterns before anything reaches GitLab or its GitHub mirror.
The password needed to decrypt those backups is retrieved from HashiCorp Vault. It is no longer stored in a home-directory password file.
Limit what each token can do
The old setup used one personal access token for several jobs. I replaced it with separate credentials for separate tasks. A CI runner can read the one repository it needs, while a backup job can write only to its backup repository.
GitLab: Project > Settings > Access Tokens
runner token: role Reporter, scope read_repository
backup token: role Developer, scope write_repository
If one credential leaks, it does not provide access to the whole account. It is the same reason I would not give every device or technician the same enable password.
Gitignore only protects the repository
An ignored file still exists in plaintext. It can be copied with a directory, captured in a backup, or mounted into a container long after everyone forgets why it was created.
The final pass removed the old plaintext credential files, moved service credentials into Vault, added gitleaks checks on Windows and WSL, and gave each automated job its own limited access. The repositories were clean, but the forgotten local copies and remote URLs were where the real problems were hiding.
