Once code is pushed to a cloud Mac, CI typically checks it out, builds it, and runs tests immediately. The problem is that the author name and email in a Git commit are only editable text: they indicate who the commit claims to be from, but they do not prove that the developer actually signed the object. A more reliable approach is to sign commits with SSH keys, then have CI use a controlled list of public keys to verify the entire commit range from the merge baseline to the current commit.
This gate does not replace code review or determine whether the code is safe. It addresses a narrower but important question: whether the Git objects entering the build pipeline are intact, whether their signatures are valid, and whether the signing keys belong to people currently authorized to submit code.
Define the Verification Trust Boundary
Commit verification involves at least three layers of checks:
- Whether the Git object content matches its signature.
- Whether the signing public key appears in the allowed signers file.
- Whether that identity was still authorized to submit code when the commit was created.
The first layer is enforced by cryptographic signatures, the second by a repository-maintained list, and the third still depends on the team’s access revocation process. Simply running git log --show-signature and seeing “Good signature” is not enough, because a valid but unauthorized key can also produce a valid signature.
Email addresses are for display and notifications, public keys are for verification, and the allowed signers file is for authorization. Do not treat them as the same thing.
Keep the list in a separate directory where changes are tightly controlled, such as .ci/trusted_signers. Changes to the list itself should require additional review so that a contributor cannot add their own public key and approve their code in the same change.
Configure Git SSH Commit Signing
Git 2.34 and later can sign directly with SSH keys. Developers should first configure the signing format and public key path locally:
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
This configuration points to the public key file; the actual signature is created with the corresponding private key. After creating a commit, confirm that the object contains a signature:
git cat-file commit HEAD | sed -n '/^gpgsig /,/^[^ ]/p'
Each line in the allowed signers file contains an identity, optional constraints, and a public key. Use stable team identifiers rather than display names that may change frequently:
ci-release namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
ios-team namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5BBBB...
After CI checks out the repository, point Git to this file:
git config --local gpg.format ssh
git config --local gpg.ssh.allowedSignersFile .ci/trusted_signers
git verify-commit HEAD
Never store private keys in the repository. CI only needs public keys for verification; private keys are required only in environments that create signed commits or tags.
Gate the Entire Commit Range
Verifying only HEAD is a common mistake. Malicious or unauthorized changes can be hidden in an earlier commit and then obscured by a final signed commit. The gate must inspect every commit after the trusted baseline.
#!/bin/bash
set -euo pipefail
base="${MERGE_BASE_SHA:?missing MERGE_BASE_SHA}"
head="${HEAD_SHA:?missing HEAD_SHA}"
git cat-file -e "${base}^{commit}"
git cat-file -e "${head}^{commit}"
count=0
while IFS= read -r commit; do
git verify-commit "$commit"
count=$((count + 1))
done < <(git rev-list --reverse "${base}..${head}")
printf 'verified_commits=%s\n' "$count"
MERGE_BASE_SHA should be calculated by CI from the target branch and the branch being merged, or supplied through a trusted mechanism. It must not simply be set to HEAD~1. For merge commits, also confirm that the baseline selection matches the team’s policy; otherwise, objects introduced through the second parent may be missed.
Gate output should record commit hashes and the stage at which a failure occurred, but it should not copy entire commit messages or environment variables into public logs. Stopping the build on failure is easier to audit than continuing to produce an artifact whose origin has not been verified.
Handle Shallow Clones, Tags, and Key Rotation
Shallow clones often cause false failures. If the baseline object is not available locally, rev-list cannot determine the complete range. Fetch the required target-branch history before verification, and use git cat-file -e to explicitly confirm that the baseline exists. If the baseline cannot be found, do not fall back to verifying only HEAD.
| Scenario | Correct handling | Shortcut to avoid |
|---|---|---|
| Baseline missing from a shallow clone | Fetch the required history and recalculate the range | Verify only the final commit |
| Release tag | Use a signed annotated tag and run git verify-tag |
Check only the tag name |
| Transition between old and new keys | Keep both public keys temporarily | Replace the key immediately and break historical jobs |
| Compromised key | Remove it immediately and review the range it signed | Change only the displayed identity |
| Departing team member | Remove their allowed signer entry | Disable only their routine login access |
Decide in advance whether historical commits should continue to pass after a key is removed. The simplest strict mode is to verify against the current list, which is appropriate for merge gates. If historical releases must remain verifiable over the long term, preserve trust records with validity intervals and archive the release tag, commit hash, and version of the list used at the time.
Preflight Checks and Troubleshooting
Before making the gate block merges, run it in an observation period that records failures while still preventing unsigned releases from proceeding. During this period, focus on the following checks:
- Developers use Git versions that support SSH signing.
- Regular commits, merge commits, and automatically generated commits all have clearly identified signers.
- CI retrieves the complete baseline instead of relying on a fixed clone depth.
- Changes to the allowed signers file require independent review.
- Release tags and the commits they reference are verified separately.
- Bot identities use dedicated keys that are not shared with individuals.
- Key rotation, key compromise, and team departures all have actionable procedures.
When troubleshooting a failure, first use git verify-commit --raw <hash> to distinguish among an unsigned object, a damaged signature, and a public key missing from the allowed list. Then inspect the repository-level settings for gpg.format and gpg.ssh.allowedSignersFile, along with the namespace, key type, and line endings in the list. This separates identity authorization issues from incomplete Git history instead of repeatedly re-signing the same commit.
Once per-commit verification, tag verification, and review of list changes are all integrated into the pipeline, the build system gains an auditable chain of origin. It cannot prove that the code is defect-free, but it can clearly show which objects were signed by which authorized keys and why unverified objects were prevented from entering subsequent build stages.
Frequently asked questions
Why is a correct author email not enough to trust a commit?
The author name and email are ordinary text fields that anyone creating a commit can set. A valid signature proves possession of a private key, while the allowed signers file determines whether that key belongs to an approved identity.
Is verifying only the branch tip sufficient?
No. Every commit introduced after the trusted base can change the resulting source tree, so the gate should run git verify-commit across the complete range. Signed release tags should also pass git verify-tag.
How can a signing key be rotated without breaking every build?
Add the new public key to the versioned allowed signers file first, accept both keys during a short transition, verify commits made with the new key, and then remove the old key.
Configure a Cloud Mac for Your Next Development Task
Choose Oak M4, Oak M4 Plus, or Oak M4 Pro, then configure a rental plan across five physical nodes based on your team's location.