2 giờ sáng và cái repo vừa bị lộ secret
Mình vẫn nhớ cái đêm đó. Một developer mới trong team commit file .env thẳng lên main, bao gồm cả database password production và AWS secret key. Dù đã xóa file trong commit kế tiếp, nhưng ai cũng biết — Git không bao giờ thật sự quên. Bất kỳ ai clone repo đều có thể chạy git log -p và thấy nguyên cái secret đó phơi ra trong lịch sử.
GitHub sau đó gửi email cảnh báo: “We found a secret in your repository history.” AWS thì đã tự động vô hiệu hóa key trước khi mình kịp làm gì. Đó là lúc mình học được cách dùng git-filter-repo — và tại sao nên biết công cụ này trước khi cần đến nó.
Tại sao chọn git-filter-repo
Có vài cách để rewrite lịch sử Git, nhưng mỗi cái đều có vấn đề riêng:
- git filter-branch: Có sẵn trong Git nhưng chậm kinh khủng với repo lớn, dễ gây lỗi, và bản thân Git cũng khuyến cáo bỏ từ lâu.
- BFG Repo Cleaner: Nhanh, dễ dùng, nhưng cần Java và không linh hoạt khi gặp case phức tạp.
- git-filter-repo: Viết bằng Python, được Git project chính thức đề xuất thay thế
filter-branch, nhanh hơn 10–50x trong thực tế.
Cụ thể: với repo 2GB lịch sử, filter-branch mất 40 phút. git-filter-repo xong trong chưa đến 3 phút. Không cần thêm lý do nào khác.
Cài đặt git-filter-repo
Kiểm tra phiên bản Git trước — cần từ 2.22.0 trở lên:
git --version
# git version 2.43.0
Cài qua pip (Python 3.6+):
# Cài toàn hệ thống
pip3 install git-filter-repo
# Hoặc trong virtualenv
pip install git-filter-repo
# Kiểm tra cài thành công
git filter-repo --version
# 2.45.0
Trên macOS với Homebrew:
brew install git-filter-repo
Trên Debian/Ubuntu:
sudo apt install git-filter-repo
Quy tắc vàng trước khi bắt đầu: không bao giờ làm thẳng trên repo gốc. Clone ra một bản riêng, backup trước, rồi mới thao tác:
# Mirror clone để backup
git clone --mirror https://github.com/yourorg/your-repo.git repo-backup.git
# Clone fresh để làm việc
git clone https://github.com/yourorg/your-repo.git repo-clean
cd repo-clean
Xóa file nhạy cảm và dữ liệu cụ thể
Xóa hoàn toàn một file khỏi mọi commit
90% trường hợp là chuyện này — xóa file .env, config/secrets.yml, hay bất kỳ thứ gì không nên tồn tại trong repo:
# Xóa file .env khỏi toàn bộ lịch sử
git filter-repo --path .env --invert-paths
# Xóa nhiều file cùng lúc
git filter-repo --path .env --path config/secrets.yml --path credentials.json --invert-paths
# Xóa theo pattern (tất cả file .pem)
git filter-repo --path-glob '*.pem' --invert-paths
# Xóa cả một thư mục
git filter-repo --path secrets/ --invert-paths
Flag --invert-paths nghĩa là “xóa những path này, giữ lại tất cả còn lại” — ngược với hành vi mặc định.
Thay thế nội dung nhạy cảm trong file (không xóa file)
File vẫn cần giữ nhưng muốn scrub giá trị secret bên trong? Tạo một file map chuỗi cần thay thế:
# Tạo file chứa các chuỗi cần thay thế
cat > expressions.txt << 'EOF'
literal:sk-ant-api03-AbCdEf123456789===>***REMOVED***
literal:AKIAIOSFODNN7EXAMPLE===>***AWS_KEY_REMOVED***
EOF
# Áp dụng
git filter-repo --replace-text expressions.txt
Format là chuỗi_cũ===>chuỗi_mới, hỗ trợ cả regex:
cat > expressions.txt << 'EOF'
regex:password=\S+===>password=***REMOVED***
regex:api_key:\s*['"]\S+['"]===>api_key: "***REMOVED***"
EOF
git filter-repo --replace-text expressions.txt
Xóa file dung lượng lớn
Trong team mình, có lần một bạn vô tình commit cả thư mục node_modules kèm một file video demo 500MB. Repo từ 50MB phình lên 600MB, clone mất cả buổi sáng. Cách xử lý:
# Phân tích lịch sử repo, tìm file lớn
git filter-repo --analyze
# Kết quả nằm trong thư mục .git/filter-repo/analysis/
# Xem file lớn nhất
cat .git/filter-repo/analysis/path-all-sizes.txt | sort -rn | head -20
Output trông như này:
=== All paths by reverse size ===
Format: size, packed size, date deleted, path name
524288000 498234112 2024-03-15 assets/demo-video.mp4
145234567 138234089 2023-11-20 node_modules.tar.gz
45678901 43211234 2024-01-08 dist/bundle.min.js
# Xóa file cụ thể theo path
git filter-repo --path assets/demo-video.mp4 --invert-paths
# Xóa tất cả file lớn hơn 10MB
git filter-repo --strip-blobs-bigger-than 10M
Kiểm tra kết quả và đẩy lên remote
Xác nhận dữ liệu đã bị xóa
# Kiểm tra file không còn trong lịch sử
git log --all --full-history -- .env
# Không có output = đã xóa sạch
# Tìm kiếm chuỗi trong toàn bộ lịch sử
git log --all -p | grep -i "sk-ant-api"
# Không có output = secret đã bị xóa
# Kiểm tra dung lượng repo sau khi xóa
git count-objects -vH
Dọn dẹp và garbage collect
git-filter-repo tự động chạy cleanup, nhưng để chắc ăn:
# Force expire tất cả reflogs
git reflog expire --expire=now --all
# Aggressive garbage collect
git gc --prune=now --aggressive
# Kiểm tra dung lượng trước/sau
git count-objects -vH
Force push lên remote
Không có cách nào tránh bước này — rewrite lịch sử đồng nghĩa phải force push:
# Thêm lại remote (git-filter-repo tự xóa remote để tránh push nhầm)
git remote add origin https://github.com/yourorg/your-repo.git
# Force push tất cả branches
git push origin --force --all
# Force push tất cả tags
git push origin --force --tags
Sau khi force push, mọi người trong team phải reset về — repo cũ trên máy họ không dùng được nữa:
# Mỗi thành viên cần chạy
git fetch --all
git reset --hard origin/main
# Hoặc đơn giản hơn: xóa repo cũ và clone lại
rm -rf old-repo/
git clone https://github.com/yourorg/your-repo.git
Revoke và rotate tất cả secret đã lộ
Xóa khỏi lịch sử Git chưa phải là xong. Nếu repo từng public, hoặc ai đó đã clone trước khi bạn kịp xử lý — coi như secret đó đã bị compromise. Không có ngoại lệ:
- Revoke và tạo lại API key, password ngay lập tức
- Rotate AWS credentials, database password
- Kiểm tra access log xem có ai đã dùng key đó chưa
- Bật GitHub secret scanning để cảnh báo sớm từ lần sau
Phòng ngừa cho lần sau
Sau cái đêm đó, mình setup ngay pre-commit hook kiểm tra secret trước khi commit. Dùng gitleaks — nhẹ, không cần runtime phụ trợ:
# Cài gitleaks
brew install gitleaks # macOS
# hoặc
wget https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64.tar.gz
# Scan repo hiện tại
gitleaks detect --source . --verbose
# Thêm vào pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
echo "Gitleaks phát hiện secret! Commit bị chặn."
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
Cuối cùng, thêm .env vào .gitignore. Nghe hiển nhiên, nhưng hay bị bỏ quên nhất:
echo '.env' >> .gitignore
echo '*.pem' >> .gitignore
echo 'credentials.json' >> .gitignore
git add .gitignore
git commit -m "chore: ignore sensitive files"
Từ khi áp dụng pre-commit hook cho cả team 8 người, không còn incident lộ secret nào xảy ra nữa — và mình ngủ yên giấc hơn nhiều.

