Signing, Trust, Secret Removal, fsck, Safe Directories, and Repository Integrity: Guided Hands-On Workflow and Core Operations
Build a disposable signing and incident-response lab with signed and unsigned objects, fake secret detection, evidence preservation, fsck/unreachable-object analysis, and the current git-filter-repo sensitive-data path.
Learning objectives
- Create and verify signed commit/tag objects with an ephemeral local backend when available.
- Distinguish signature verification failure from object corruption.
- Find a fake marker in reachable history after it is removed from the current snapshot.
- Preserve refs/history/bundle evidence and interpret unreachable objects without pruning.
- Use git-filter-repo sensitive-data-removal as the required history-rewrite path when installed.
1. Disposable incident lab and backend preflight
The laboratory never uses a real credential. It creates an isolated repository, an isolated signing home, and a fake marker that is safe to publish. Signing is optional when no free local backend is installed; the repository-integrity and incident-response portions still work.
Git Bash / Bash / zsh
mkdir git-integrity-lab
cd git-integrity-lab
git --version
command -v gpg || echo "OpenPGP signing unavailable"
command -v ssh-keygen || echo "SSH signing tool unavailable"
git filter-repo --version 2>/dev/null || echo "git-filter-repo not installed yet"
PowerShell
New-Item -ItemType Directory git-integrity-lab | Out-Null
Set-Location git-integrity-lab
git --version
Get-Command gpg -ErrorAction SilentlyContinue
Get-Command ssh-keygen -ErrorAction SilentlyContinue
git filter-repo --version
2. Create a repository with explicit branch and fake identity
git init -b trunk source
git -C source config user.name "Learner Example"
git -C source config user.email "learner@example.invalid"
cd source
printf "service=payments\nstatus=baseline\n" > app.conf
git add app.conf
git commit -m "baseline: unsigned configuration"
UNSIGNED=$(git rev-parse HEAD)
git status --short --branch
git log --oneline --decorate
This first commit is intentionally unsigned. An unsigned object can still be a valid Git object; the absence of a signature means a signature-based provenance policy cannot authenticate it.
3. Optional OpenPGP path — create an ephemeral training key
Git Bash / Bash / zsh
cd ..
export GNUPGHOME="$PWD/gnupg-home"
mkdir -m 700 "$GNUPGHOME"
gpg --batch --passphrase '' \
--quick-generate-key \
'Learner Example <learner@example.invalid>' ed25519 sign 0
KEY=$(gpg --batch --with-colons --list-secret-keys \
'learner@example.invalid' |
awk -F: '$1=="fpr" {print $10; exit}')
printf 'training signing fingerprint: %s\n' "$KEY"
PowerShell concept
$env:GNUPGHOME = Join-Path (Get-Location) 'gnupg-home'
New-Item -ItemType Directory $env:GNUPGHOME | Out-Null
gpg --batch --passphrase '' --quick-generate-key `
'Learner Example <learner@example.invalid>' ed25519 sign 0
# Obtain the generated fingerprint with: gpg --with-colons --list-secret-keys
Some GPG versions/platforms may differ in supported key algorithms or agent/pinentry behavior. If this command is unavailable, skip to the unsigned/fsck portion rather than importing a real key.
4. Configure signing locally for the disposable repository
cd source
git config --local gpg.format openpgp
git config --local user.signingKey "$KEY"
git config --show-origin --get gpg.format
git config --show-origin --get user.signingKey
user.signingKey selects the training key; it does not
change author identity metadata and does not grant server
authorization.
5. Create and verify a signed commit
printf "status=signed-baseline\n" >> app.conf
git add app.conf
git commit -S -m "baseline: signed configuration"
SIGNED=$(git rev-parse HEAD)
git verify-commit "$SIGNED"
echo "verify-commit exit=$?"
git log -1 --show-signature --format='%h %G? %GS %s'
Expected observation with a working OpenPGP setup: verification reports a good signature from the ephemeral learner identity. The exact trust wording is backend/keyring-specific.
6. Create and verify a signed annotated tag
git tag -s signed-v1 "$SIGNED" -m "Signed training release"
git verify-tag signed-v1
echo "verify-tag exit=$?"
git cat-file -p signed-v1 | sed -n '1,18p'
The tag object contains its own signature. A tag signature attests to the tag payload, including the object it names; it is different from a signed commit.
7. Verify that an unsigned commit does not magically become signed
git verify-commit "$UNSIGNED"
echo "unsigned verify exit=$?"
Expected: non-zero. That result means “no verifiable commit
signature under this command,” not “corrupt object.” Use
git cat-file -t "$UNSIGNED" or
git fsck for object integrity questions.
8. Inspect safe-directory policy without weakening it
git config --show-origin --get-all safe.directory || \
echo "no explicit safe.directory entries"
git rev-parse --show-toplevel
This lab does not add *. If Git reports dubious
ownership in a real environment, first verify the path and owner.
Correct the ownership when appropriate or add one exact reviewed
path in protected configuration.
9. Seed an intentionally fake secret marker
printf "api_token=FAKE_TOKEN_DO_NOT_USE_2026\n" >> app.conf
git add app.conf
git commit -m "incident: accidentally add fake token"
LEAK=$(git rev-parse HEAD)
sed -i 's/FAKE_TOKEN_DO_NOT_USE_2026/ROTATED_FAKE_VALUE/' app.conf
git add app.conf
git commit -m "containment: remove fake token from current snapshot"
TIP_BEFORE=$(git rev-parse HEAD)
The second commit simulates current-file cleanup. The marker is harmless, but the incident procedure treats it as though it were real.
10. Prove that current-tree cleanup did not clean history
grep -n 'FAKE_TOKEN_DO_NOT_USE_2026' app.conf || echo "current file clean"
git log -S'FAKE_TOKEN_DO_NOT_USE_2026' --all --oneline -- app.conf
git grep 'FAKE_TOKEN_DO_NOT_USE_2026' $(git rev-list --all)
Expected: the current file no longer contains the fake marker, but historical search finds the commit/blob where it existed.
11. Containment comes before rewriting
printf "credential=FAKE_TOKEN_DO_NOT_USE_2026\nstate=REVOKED_SIMULATION\n" \
> ../incident-rotation-record.txt
cat ../incident-rotation-record.txt
Never put a real replacement secret into the repository or incident transcript.
12. Preserve evidence and a recovery copy before rewrite
git status --short --branch
git for-each-ref --format='%(refname) %(objectname)' > ../refs-before.txt
git log --all --oneline --decorate --graph > ../history-before.txt
git bundle create ../pre-rewrite.bundle --all
git bundle verify ../pre-rewrite.bundle
The bundle is for this disposable training lab. In a real secret incident, backup handling must itself respect the fact that the backup contains the exposed secret.
13. Run fsck before rewriting
git fsck --no-progress
git fsck --strict --no-progress
A clean result says the object graph and objects pass the selected Git integrity checks. It says nothing about whether the fake token is present or whether the signer is authorized.
14. Create a harmless unreachable commit and interpret it
TREE=$(git write-tree)
UNREACHABLE=$(printf 'temporary forensic note\n' | git commit-tree "$TREE")
printf 'unreachable commit: %s\n' "$UNREACHABLE"
git fsck --unreachable --no-reflogs --no-progress | \
grep "$UNREACHABLE" || true
The commit object exists but no ref names it. Do not prune it merely because fsck reports it. During investigation, unreachable objects can contain lost work or evidence.
15. Current recommended rewrite path — preflight
git filter-repo
Git's own filter-branch documentation recommends using
git filter-repo instead because filter-branch has
difficult safety/performance pitfalls. The filter-repo project's
sensitive-data mode adds incident-specific checks and instructions.
git filter-repo --version
16. Rewrite only in a fresh disposable clone
cd ..
git init --bare -b trunk origin.git
git -C source remote add training-origin ../origin.git
git -C source push training-origin --all
git -C source push training-origin --tags
git clone --no-local origin.git cleanup
cd cleanup
printf 'literal:FAKE_TOKEN_DO_NOT_USE_2026==>REMOVED_FAKE_TOKEN\n' \
> ../replacement-expressions.txt
--no-local avoids local hardlink shortcuts and makes
the cleanup copy clearly independent. The rewrite remains isolated
from the original source repository and pre-rewrite bundle.
17. Rewrite the fake marker with sensitive-data mode
git filter-repo \
--sensitive-data-removal \
--replace-text ../replacement-expressions.txt
Current filter-repo documentation notes that sensitive-data mode gathers information useful for cleaning other copies. It can fetch additional refs and modifies repository history broadly; read its terminal report instead of treating it as a silent text replacer.
18. Verify the sensitive marker is absent from reachable rewritten history
git log -S'FAKE_TOKEN_DO_NOT_USE_2026' --all --oneline -p
git grep 'FAKE_TOKEN_DO_NOT_USE_2026' $(git rev-list --all) \
&& echo "unexpected marker found" \
|| echo "marker absent from reachable rewritten history"
git fsck --strict --no-progress
git log --graph --decorate --oneline --all --max-count=30
Also compare known pre-rewrite commit IDs with the rewritten graph. Descendants of a changed historical blob should have new commit IDs.
19. Verify signature consequences, then create a new post-cleanup attestation
Filter-repo cannot preserve signatures on rewritten commit/tag payloads. Expect old signed commits/tags to lose that signature relationship. If signing policy requires a clean release attestation, create a new signed tag after verifying the sanitized graph:
git config user.name "Learner Example"
git config user.email "learner@example.invalid"
git config gpg.format openpgp
git config user.signingKey "$KEY"
git tag -s sanitized-v1 -m "Sanitized training release"
git verify-tag sanitized-v1
git log -1 --show-signature
Do not interpret re-signing the new tip as proof that the old leaked credential was never exposed; the incident record remains necessary.
20. Challenge — choose the control that answers the question
- You need to know whether a commit's signature matches its payload. Which command?
- You need to know whether the object graph is internally valid. Which command family?
- You need to know whether an old secret string still exists in reachable history. Which search?
- You need to make a real leaked credential unusable. Is that a Git rewrite operation?
- You need to remove sensitive text from historical objects. Which current recommended rewrite tool path?
21. Cleanup
cd ../..
pwd
rm -rf git-integrity-lab
PowerShell equivalent:
Set-Location ../..; Remove-Item -Recurse -Force
git-integrity-lab.
22. Knowledge check
Question 1. Why does verify-commit fail on the
intentionally unsigned baseline commit?
Question 2. Why is removing the fake marker from the latest file insufficient?
Question 3. Why preserve refs/history/bundle before the rewrite?
Question 4. What does an unreachable fsck report mean?
Question 5. Why must post-rewrite signatures be created anew?
23. Summary
You separated signature verification from object integrity, detected a fake secret beyond the working tree, simulated credential rotation before cleanup, preserved evidence, used fsck without pruning, and followed the current filter-repo path for sensitive-data rewriting.
Authoritative references
git-verify-commit
git-verify-tag
git-fsck
filter-branch warning
git-filter-repo project
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.