Chapter 21Lesson 02~190 minutes

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.

Hands-on signingFake secretEvidencefilter-repo

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

Use only the generated lab key. Do not point this lab at a personal or production signing 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

Real incident rule: revoke/rotate the actual credential at its provider first. Do not delay rotation while designing a perfect Git rewrite. The commands below only record a fictional training event.
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
If this command is unavailable, stop the rewrite step and install git-filter-repo from its maintained project documentation. Do not substitute filter-branch merely to finish the exercise.

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

History rewrite: this changes commit IDs, can remove signatures, rewrites refs, and requires coordination before any publication. Run only in this fresh disposable clone.
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

  1. You need to know whether a commit's signature matches its payload. Which command?
  2. You need to know whether the object graph is internally valid. Which command family?
  3. You need to know whether an old secret string still exists in reachable history. Which search?
  4. You need to make a real leaked credential unusable. Is that a Git rewrite operation?
  5. You need to remove sensitive text from historical objects. Which current recommended rewrite tool path?

21. Cleanup

Confirm you are in the disposable parent before recursive deletion. The training GPG home contains only the ephemeral lab key.
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.

Next

Turn the lab into explicit configuration and policy

Lesson 3 covers default signing controls, backend choice, narrow safe.directory exceptions, transfer/fetch/receive fsck validation, credential helpers, and trust boundaries for untrusted repositories.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.