Signed Commits and Git Security¶
Overview¶
Compliance frameworks (SOC 2, FedRAMP) increasingly require proof that commits came from authorized developers. Signed commits cryptographically bind identity to changes. Combined with branch protection, secret scanning, and access controls, signing forms a defense-in-depth strategy for Git security in DevOps pipelines.
This is Tutorial 19 in Module 6: Advanced & DevOps of the REBASH Academy Git series.
Prerequisites¶
Learning Objectives¶
By the end of this tutorial, you will be able to:
- Generate GPG and SSH signing keys
- Configure Git to sign commits and tags automatically
- Verify signatures locally and on GitHub/GitLab
- Enforce signed commits via branch protection
- Apply Git security best practices for secrets and access
- Understand Sigstore/gitsign as cloud-native alternative
- Respond to leaked credential incidents in Git history
Signing Flow Diagram¶
flowchart LR
DEV[Developer]
KEY["Signing Key<br/>GPG or SSH"]
COMMIT[Signed Commit]
REMOTE["GitHub / GitLab"]
VERIFY[Signature Verification]
DEV -->|git commit -S| KEY
KEY --> COMMIT
COMMIT -->|push| REMOTE
REMOTE --> VERIFY```
## Theory
### Why Sign Commits?
Git's default author metadata is trivially forgeable — anyone can set `user.email` to another person. **Signing** proves the committer possessed a private key associated with a verified identity.
Benefits:
- **Non-repudiation** — audit trail for compliance
- **Supply chain security** — verify code origin in CI
- **Protected branch enforcement** — reject unsigned commits
### GPG Signing
Traditional approach using OpenPGP:
```bash
gpg --full-generate-key
gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git commit -S -m "feat: signed commit" Export public key to GitHub/GitLab → Settings → SSH and GPG keys.
Verify:
SSH Commit Signing (Git 2.34+)¶
Simpler for teams already using SSH keys:
ssh-keygen -t ed25519 -C "signing@example.com" -f ~/.ssh/id_ed25519_sign
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_sign.pub
git config --global commit.gpgsign true
Add signing public key to GitHub (separate from authentication key).
Signed Tags¶
Release tags should be annotated and signed:
Production deploy pipelines should verify tag signature before deploy.
Platform Verification¶
GitHub shows Verified badge when:
- Signature valid
- Key associated with committer account
- For web commits, GitHub signs on your behalf
GitLab shows similar verification in commit view.
Enforcing Signed Commits¶
Branch protection rules:
- Require signed commits
- Require linear history
- Require status checks
- Restrict push to specific teams
Combine with rulesets (GitHub) or push rules (GitLab self-managed).
Secret Management in Git¶
Never commit:
- API keys, passwords, tokens
- Private keys, kubeconfig with credentials
.tfstatefiles
Prevention stack:
.gitignore+.gitattributes- pre-commit
detect-private-key - CI secret scanning (Gitleaks, GitHub secret scanning)
- Regular audits
If secret committed:
- Revoke credential immediately
- Remove from history (
git filter-repo/ BFG) - Force push coordinated with team
- Post-incident review
Access Control¶
- Least privilege — read vs write vs admin
- SSH deploy keys — read-only for CI clone
- PAT rotation — short-lived tokens
- 2FA enforced on Git hosting orgs
- Audit logs — review anomalous push events
gitsign / Sigstore¶
Cloud-native keyless signing using identity from OIDC (GitHub Actions):
No long-lived GPG key management — growing adoption in supply chain security.
Supply Chain Frameworks¶
- SLSA — provenance attestations for builds
- SBOM — software bill of materials stored alongside releases
- Git signing is one layer — not sufficient alone
Hands-on Lab¶
Step 1 – Configure SSH signing (if key exists)¶
Command:
mkdir -p /tmp/git-sign-lab && cd /tmp/git-sign-lab
git init -b main
git config user.name "Sign Lab"
git config user.email "sign@example.com"
# Create signing key for lab
ssh-keygen -t ed25519 -f /tmp/sign_key -N "" -C "sign@example.com"
git config gpg.format ssh
git config user.signingkey /tmp/sign_key.pub
git config commit.gpgsign true
git config tag.gpgsign true
Step 2 – Create signed commit¶
Command:
echo "secure config" > app.conf
git add app.conf
git commit -m "feat: add config with signing enabled"
git log --show-signature -1 2>&1 | head -15
Explanation: Without key on GitHub, shows "Good signature" locally but unverified on platform.
Step 3 – Verify signature status¶
Command:
Explanation: %G? shows signature status: G=good, N=none, B=bad.
Step 4 – Signed tag¶
Command:
Step 5 – Simulate unsigned commit rejection policy¶
Command:
git config commit.gpgsign false
echo "unsigned" >> app.conf
git commit -am "test: unsigned commit"
git log --format="%H %G?" -2
git config commit.gpgsign true
Step 6 – Clean up¶
Command:
Commands & Code¶
| Command | Description | Example |
|---|---|---|
git commit -S | Sign single commit | Explicit sign |
git config commit.gpgsign true | Auto-sign all commits | Global policy |
git log --show-signature | Display signatures | Audit commits |
git verify-commit SHA | Verify commit signature | CI gate |
git tag -s v1.0.0 | Signed annotated tag | Release |
gpg --list-secret-keys | List GPG keys | Key management |
git config gpg.format ssh | Use SSH signing | Modern alternative |
CI signature verification¶
#!/usr/bin/env bash
# verify-signed-commits.sh — fail if recent commits unsigned
set -euo pipefail
RANGE="${1:-origin/main..HEAD}"
while read -r sha status; do
if [ "$status" != "G" ]; then
echo "ERROR: Commit $sha is not signed (status=$status)"
exit 1
fi
done < <(git log --format='%H %G?' $RANGE)
echo "All commits in range are signed."
Common Mistakes¶
Signing key without backup
Lost key cannot sign; old signatures become unverifiable after key rotation. Backup securely.
Same key for auth and signing
Compromise exposes both. Use separate SSH keys.
Assuming signing replaces secret scanning
Signed malicious commits are still malicious. Sign AND scan.
Not revoking leaked secrets immediately
History rewrite takes time — revocation is first action.
Best Practices¶
Enable commit.gpgsign globally from day one
Habits form early; retrofitting signing across team is harder.
Require signed commits on main via branch protection
Policy without enforcement gets ignored.
Use SSH signing for simpler developer experience
One less GPG agent headache on macOS/Linux.
Integrate secret scanning in CI and platform
GitHub Advanced Security, GitLab secret detection, or Gitleaks.
Troubleshooting¶
| Issue | Cause | Solution |
|---|---|---|
gpg: signing failed | Agent not running | Start gpg-agent; pinentry |
| Unverified on GitHub | Key not uploaded | Add GPG/SSH signing key to account |
| Wrong key used | Multiple keys | Set user.signingkey explicitly |
| CI cannot sign | No key in runner | Use bot account or gitsign |
error: gpg failed to sign | Passphrase prompt in CI | Use gpg-agent or SSH signing |
| Secret in history | Committed before hook | filter-repo; revoke secret |
Summary¶
- Signed commits cryptographically prove authorship — GPG or SSH keys
- Configure commit.gpgsign and upload public keys to hosting platform
- Branch protection can require signed commits on main
- Secret scanning and
.gitignorecomplement signing — not substitutes - Signed tags anchor trusted release deployments
- gitsign/Sigstore offers keyless signing for CI environments
Interview Questions¶
- Why are signed commits important for compliance?
- What is the difference between GPG and SSH commit signing?
- How do you configure Git to sign all commits automatically?
- How do you verify a commit signature locally?
- What should you do if a secret is committed to Git?
- How do branch protection rules enforce signing?
- What is the difference between author and committer in signed commits?
- What is gitsign/Sigstore?
- Why use separate keys for authentication and signing?
- What layers form Git security defense-in-depth?
Sample Answers (Questions 1 and 5)
Q1 — Compliance value: Signed commits provide cryptographic proof that a specific key holder created the change, supporting non-repudiation requirements in SOC 2 and similar frameworks. Combined with branch protection and audit logs, organizations demonstrate access control and change integrity for production infrastructure code.
Q5 — Secret committed: Immediately revoke/rotate the credential in the target system — this works even before history cleanup. Then remove from Git history using git filter-repo or BFG Repo-Cleaner. Force-push cleaned history with team coordination. Run secret scan to verify removal. Document incident and improve pre-commit/CI scanning.
Related Tutorials¶
- Git Submodules and Subtrees (previous)
- Git in CI/CD and DevOps (next — finale)
- Git Installation and Configuration
- Git Hooks and Automation
- gitignore and gitattributes