Addressing Secret leaks, on-demand
Across an Entire Owned Remote Git Repositories
Discovering that hardcoded API keys, database passwords, or corporate credentials have leaked into your source control requires rapid, aggressive remediation. When secrets are scattered across dozens of repositories and multiple git branches, handling it manually is impossible.
This article details a battle-tested blueprint for an on-demand emergency remediation pipeline engineered inside a Linux (Debian) environment. We will cover the exact workflow to programmatically download every repository, scan them with gitleaks, parse the findings to overwrite history with REDACTED across all branches and timelines, and successfully force-push the clean results back to GitLab.
Technical Context & Real-World Challenges
During an actual emergency execution, standard documentation quickly falls apart due to environment edge cases. This architectural blueprint accounts for several specific obstacles encountered during a real remediation run:
- Environmental Context: Run completely inside Development Environment (example Linux, Debian).
- The False-Positive Loop:
gitleaksscans the workspace root and flags its ownreport.jsonoutput as a leak. We fix this by isolating the workspace directory completely outside of the script repository tree. - Permissions Blockades: Forcing modifications down protected server branches defaults to a
403 Forbiddenerror. We map the explicit path to unprotect these targets inside GitLab. - Future Hardening: During the rewriting phase, we dynamically inject a robust
.gitignoreboilerplate to prevent heavy web application environments (like Node.js, Python, or PHP frameworks) from leaking environment variables or certificates again.
Phase 1: Environment Provisioning (Debian Linux here)
Before processing repositories, ensure your Debian environment inside Windows Subsystem for Linux has the core tools and binaries installed.
1. Install Gitleaks & Git-Filter-Repo
Do not rely on outdated apt repositories. Fetch the latest gitleaks pre-compiled binary directly, and install git-filter-repo via Python's package manager:
# Update local packages
sudo apt update && sudo apt install -y curl git jq python3-pip python3-venv
# Download and install latest, but stable and pinned Gitleaks version
GITLEAKS_VERSION="8.21.0"
curl -sSLo gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
tar -xzf gitleaks.tar.gz gitleaks
sudo mv gitleaks /usr/local/bin/
rm gitleaks.tar.gz
# Install git-filter-repo globally
pip3 install git-filter-repo --break-system-packages
2. Isolate the Workspace Directory
To prevent gitleaks from detecting its own JSON reports, do not run scans inside your active scripts directory. Establish an isolated workspace directory at a completely neutral system path (e.g., /tmp/secure_remediation_vault).
Phase 2: The Two-Script Solution
Our architecture splits the solution into two scripts to ensure precision:
- Script 1 (
scan_fleet.sh): Handles recursive discovery, cloning, and outputs a target report. - Script 2 (
remediate_fleet.sh): Evaluates the report, rewrites history comprehensively, amends local hygiene files, and handles remote delivery synchronization.
Script 1: Fleet Discovery and Secret Extraction
This script loops through your GitLab instance using your Personal Access Token (PAT), clones repositories into the isolated vault, and runs a comprehensive gitleaks check across full repository histories.
#!/usr/bin/env bash
set -euo pipefail
GITLAB_URL="https://gitlab.com"
API_TOKEN="your_gitlab_pat_token"
ORG_ID="your_gitlab_group_or_user_id"
# ISOLATION: Workspace paths are kept out of this script directory to prevent false-positives
WORKSPACE_DIR="/tmp/secure_remediation_vault"
REPORTS_DIR="${WORKSPACE_DIR}/gitleaks_reports"
mkdir -p "$WORKSPACE_DIR"
mkdir -p "$REPORTS_DIR"
echo "=== PHASE 1: Fetching Repository Catalog from GitLab ==="
# Fetch projects via GitLab API
REPOS=$(curl -s --header "PRIVATE-TOKEN: ${API_TOKEN}" "${GITLAB_URL}/api/v4/groups/${ORG_ID}/projects?per_page=100" | jq -r '.[].ssh_url_to_repo')
cd "$WORKSPACE_DIR"
for repo_url in $REPOS; do
repo_name=$(basename "$repo_url" .git)
echo "Processing Catalog Destination: ${repo_name}"
if [ ! -d "$repo_name" ]; then
# Cloning all branches and full historical timelines via a mirror clone
git clone --mirror "$repo_url" "$repo_name"
fi
echo "Scanning history for ${repo_name}..."
# Scan the mirrored repository directory directly
gitleaks detect --source="./${repo_name}" --report-path="${REPORTS_DIR}/${repo_name}_report.json" || true
done
echo "=== Phase 1 Complete. Individual reports compiled safely in ${REPORTS_DIR} ==="
Script 2: Comprehensive Remediation & Synchronization
This script reads the respective parsed JSON reports generated by Phase 1. It extracts the raw values of compromised secrets, formats a replacements rule file, executes git-filter-repo across all branches and history tags, appends standard tracking ignore rules, and triggers forced remote synchronization updates. The key feature of the script is it looks up the scanned report json to identify fix the secrets instead of hard-coded values. Which however, can be added to be addressed as well. It also adds typical files and directories to .gitignore which shouldn't be pushed to remote at all, but inadvertently, sometimes, gets pushed to remote repo.
#!/usr/bin/env bash
set -euo pipefail
WORKSPACE_DIR="/tmp/secure_remediation_vault"
REPORTS_DIR="${WORKSPACE_DIR}/gitleaks_reports"
GITLAB_URL="https://gitlab.com"
API_TOKEN="your_gitlab_pat_token"
if [ ! -d "$WORKSPACE_DIR" ]; then
echo "Workspace vault directory missing. Execute the discovery sequence first."
exit 1
fi
cd "$WORKSPACE_DIR"
for repo_dir in */ ; do
[ -d "$repo_dir" ] || continue
repo_name="${repo_dir%/}"
# Skip processing our report directory tracking structure
if [ "$repo_name" == "gitleaks_reports" ]; then continue; fi
report_file="${REPORTS_DIR}/${repo_name}_report.json"
echo "--------------------------------------------------------"
echo "Remediating Target Repository Timeline: ${repo_name}"
echo "--------------------------------------------------------"
cd "$repo_name"
# Check if any legitimate leaks were documented
if [ -f "$report_file" ] && [ "$(jq '. | length' "$report_file")" -gt 0 ]; then
echo "Leaks found. Extracting matching strings..."
# Build text replacement file mapping secret strings to 'REDACTED'
expressions_file="../${repo_name}_expressions.txt"
rm -f "$expressions_file"
jq -r '.[].Secret' "$report_file" | sort -u | while read -r secret_string; do
if [ -z "$secret_string" ]; then continue; fi
echo "${secret_string}==>REDACTED" >> "$expressions_file"
done
echo "Executing total history scrub via git-filter-repo..."
# Rewrites ALL commits on ALL branches simultaneously
git filter-repo --replace-text "$expressions_file" --force
rm -f "$expressions_file"
# Restore remote target because git-filter-repo removes origins for safety
git remote add origin "${GITLAB_URL}/your_group_or_user/${repo_name}.git" || true
else
echo "Clean validation scan report. Skipping filtration rewrite steps."
git remote add origin "${GITLAB_URL}/your_group_or_user/${repo_name}.git" || true
fi
# RESTORE WORKSPACE HYGIENE: Update .gitignore with production best practices
# We must briefly convert bare repos to normal or handle metadata. If working tree exists:
echo "Injecting hardened .gitignore patterns..."
cat << 'EOF' >> .gitignore
# Emergency Secret Remediation Hardening Boilerplate
.env
.env.*
*.log
*.pem
*.key
*.crt
*.p12
/certs/
/config/jwt/
/vendor/
/node_modules/
EOF
# Stage amendments and commit changes into the rewritten lineage
git add .gitignore
git commit -m "chore: harden repository workspace safety and .gitignore patterns" || true
echo "Pushing purged data assets back to GitLab..."
git push origin --all --force
git push origin --tags --force
cd ..
done
echo "=== Emergency Fleet Recovery Script Finalized ==="
Phase 3: Addressing Server Permissions & Automation
When Script 2 attempts to rewrite remote tracking history with git push origin --all --force, GitLab will block the operation with a branch protection error for default branches (like main or master).
The GitLab UI Flow
To bypass this manual configuration blocker:
- Open your target group or repository space in the GitLab UI.
- Navigate to the left sidebar margin: Settings (
ightarrow) Repository. - Locate the Protected branches configuration section and select Expand.
- Scroll down to find your restricted main tracking entries (e.g.,
main). - Click Unprotect to temporarily allow administrative forced push access.
- Rerun Script 2. The script will now rewrite all server records across those branches smoothly.
- Return to the configuration layout immediately and restore branch protection constraints.
Automating Protections with GitLab API (Alternative)
If you have dozens of repositories, clicking through the UI is tedious. You can automate toggling branch protections before and after running Script 2 using the GitLab API:
# To UNPROTECT a branch (Run before Script 2)
curl --request DELETE --header "PRIVATE-TOKEN: ${API_TOKEN}" "${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/protected_branches/main"
# To RE-PROTECT a branch (Run immediately after Script 2)
curl --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/protected_branches?name=main&push_access_level=40&merge_access_level=40"
(Note: 40 corresponds to Maintainer access rules in GitLab).
Team Instructions: Recovering from a Force Push
Because you rewrote the history tracking elements on the remote server, your engineering team will experience errors the next time they attempt to run git pull. Their local commit tracking history no longer matches the server.
Provide your development team with these exact commands to safely reconcile their workspaces without re-introducing the deleted secrets:
# WARNING: Do NOT run 'git pull' or 'git merge'. It will re-upload the leaked secret!
# Run these steps instead:
# 1. Fetch the newly cleaned history from the remote tracking host
git fetch origin
# 2. Reset your active local branch to match the remote server exactly
# (CAUTION: This discards uncommitted changes. Stash them first if necessary via 'git stash')
git reset --hard origin/$(git branch --show-current)
Verification Strategy
Once the pipeline scripts conclude operations, verify your history is clean by forcing an on-demand validation check.
Clear your local cache paths completely, download a fresh copy of any target repository, and rerun a targeted gitleaks execution profile:
cd /tmp
git clone https://gitlab.com/your-org/your-remediated-repo.git
gitleaks detect --source="./your-remediated-repo" -v
The validation terminal output should read:INF no leaks found
Your repositories are now clean, your histories are completely purged of credentials, your hygiene files are updated, and your team's tracking targets are securely locked down against future leaks.