Git records who wrote a commit as a string that whoever runs the command supplies. Nothing checks it. Everything about repository trust is built on top of that uncomfortable fact.
Topic 1: The Author Field Is Just Text
git -c user.name="Lead Engineer" \
-c user.email="lead@corp.example" \
commit -m "approve production deploy"
That commit is accepted, and git log and the forge’s UI present it as theirs. On many forges it will even show their avatar, because the avatar is looked up by email address.
This is not a vulnerability in Git; it is a consequence of a distributed system in which anyone can create any object locally. Trust has to come from somewhere else, in three layers.
Topic 2: Signing Commits
A signature proves that the holder of a specific private key created the commit object.
SSH signing is the modern, low-friction option — you almost certainly already have the key:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
Then upload the public key to your forge as a signing key (distinct from an authentication key, even if it is the same key).
GPG signing is the older option, and still required by some projects and processes:
gpg --full-generate-key # ed25519 is a fine choice
gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
Verification:
git log --show-signature -3
git verify-commit HEAD
git verify-tag v1.1.0
git log --format='%h %G? %an %s' -10 # G=good, B=bad, U=unknown, N=none
For local verification to mean anything, Git needs to know which keys you trust:
# ~/.config/git/allowed_signers
alice@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
bob@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers
Without that file, SSH-signed commits verify as “unknown signer” — the signature is valid, but you have no statement about whose key it is.
What signing does and does not prove:
- It proves the key holder created that commit object.
- It does not prove the code is correct, reviewed, or safe.
- It does not survive a rebase or squash — the rewriting party signs the new commits, which is why “require signed commits” and “squash merge” interact: the forge signs the merge result with its own key.
Topic 3: Branch Protection — Where Enforcement Lives
Signing is evidence. Protection is enforcement. The settings worth having on any branch that gets deployed:
□ Require a pull request before merging
□ Require N approvals
□ Dismiss stale approvals when new commits are pushed
□ Require review from CODEOWNERS for protected paths
□ Require status checks to pass (name them explicitly)
□ Require branches to be up to date before merging
□ Require signed commits (once your team is set up)
□ Block force-pushes
□ Block deletions
□ Include administrators ← the one everyone leaves off
“Include administrators” is the setting that decides whether this is a control or a suggestion. A rule that senior people can bypass is a rule that gets bypassed during exactly the incident where it mattered.
CODEOWNERS routes review to the people who understand a path:
# .github/CODEOWNERS
* @org/engineering
/infra/ @org/platform
/services/payments/ @org/payments @org/security
*.tf @org/platform
/.github/workflows/ @org/platform
Two entries earn their keep immediately: infrastructure code, and the CI workflow files themselves — because a pull request that edits the pipeline can disable every other check you configured.
Topic 4: Credentials and Access
Never embed credentials in a remote URL. They end up in .git/config, in shell history, in screen shares, and in the output of git remote -v.
git config --global credential.helper osxkeychain # macOS
git config --global credential.helper libsecret # Linux
git config --global credential.helper manager # Windows
Deploy keys and machine users for automation: a deploy key is scoped to one repository and can be read-only, which is what a CI checkout needs. A personal access token belonging to a human engineer, used by a pipeline, is a permission grant that survives their departure and appears in an audit as their activity.
Scope tokens narrowly and set an expiry. Fine-grained tokens on major forges can be limited to specific repositories and specific permissions; a classic token with repo scope can read and write everything the person can.
Signed pushes, where the forge supports them, extend the guarantee from “this commit was signed” to “this push was made by this key” — which closes the gap where an attacker with repository write access replays or reorders legitimate signed commits.
Topic 5: Supply-Chain Concerns in the Repository
Version control is where most supply-chain compromises are visible, if anyone looks:
- Pin dependencies by hash where the ecosystem allows it. A tag can be moved; a hash cannot. This applies to your CI actions as much as to your libraries —
uses: actions/checkout@v4is a moving target,@<sha>is not. - Review dependency updates like code. An automated bump PR that nobody reads is a supply chain with no controls at all.
- Watch for changes to CI configuration, which is the most valuable file in the repository to an attacker: it runs with credentials, on every push.
- Treat forks and pull requests from outside carefully. A workflow triggered by
pull_request_targetruns with repository secrets in the context of the base repository, and that specific footgun has caused real compromises. - Enable secret scanning with push protection, so the credential never lands.
Topic 6: A Trust Baseline You Can Apply Today
For a repository that matters, in ascending order of effort:
1. Branch protection on main: PR required, force-push blocked,
deletions blocked, administrators included. (5 minutes)
2. Required status checks, named explicitly. (5 minutes)
3. CODEOWNERS for infra, CI config and security-sensitive paths.
4. Secret scanning + push protection.
5. Dependency update automation, with a human review requirement.
6. SSH commit signing across the team, then require it.
7. Signed, annotated release tags, verified by consumers.
8. Pin CI actions by SHA.
Steps 1 and 2 remove most of the realistic risk for ten minutes of work, and they are the ones most often missing on repositories that have grown organically.
Try it yourself: forge a commit as a colleague in a scratch repository and look at how convincingly the forge renders it — avatar included. That five-second experiment is the most persuasive argument for signing you will ever make to a team.
Common mistake: enabling “require signed commits” before the team is set up to sign, then disabling it two hours later because nobody can push. Roll it out in the other order: signing configured and verified for everyone first, then the requirement — and remember that squash and rebase merges are signed by the forge, not by the author, which is a question to answer before someone raises it in an audit.