Signed Commits — The Fake Commit That Almost Deployed Malware
A malicious actor pushed a commit impersonating a trusted maintainer.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Git 2.0+, GPG (GnuPG) 2.2+, basic command-line proficiency, a GitHub/GitLab/Bitbucket account, and a code editor.
1. Generate a GPG key: gpg --full-generate-key. 2. Add it to GitHub: gpg --armor --export → Settings → SSH and GPG keys. 3. Configure Git: git config --global user.signingkey and git config --global commit.gpgsign true.
Signed commits are like a wax seal on a letter. Anyone can write 'From: Albert Einstein' on an envelope. But a wax seal with Einstein's signet ring can only be created by someone with that ring. GPG signing stamps each commit with your unique digital seal. GitHub checks the seal and shows 'Verified' if it matches your account. No seal = anyone could have written that commit.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Git commits have an author field that can be set to ANY name and email. git config user.name 'Linus Torvalds' && git config user.email 'torvalds@linux.com' — there's no authentication. This is a feature (Git is trust-based) but also a vulnerability. A malicious actor with push access can impersonate anyone. GPG signing solves this by attaching a cryptographic signature that GitHub/GitLab verifies against your public key. This guide covers setup, the incident where unsigned commits almost deployed malware, and how to enforce signing in your CI/CD pipeline.
Why GPG Signing Matters in Production
In a collaborative codebase, every commit carries the author's name and email. But Git allows anyone to claim any identity. Without cryptographic verification, a malicious actor can impersonate a trusted developer and introduce backdoors. GPG signing binds each commit to a specific key, ensuring authenticity and non-repudiation. In production environments, this is not optional—it's a compliance requirement for SOC 2, PCI-DSS, and FedRAMP. I've seen teams waste days auditing commits after a breach because they couldn't prove who pushed what. GPG signing eliminates that ambiguity. It also enables tools like GitHub's 'Verified' badge, which signals to CI/CD pipelines that the commit is trustworthy. If your pipeline blindly trusts unsigned commits, you're one compromised credential away from a supply chain attack.
Generating a GPG Key Pair
Before signing commits, you need a GPG key pair. Use gpg --full-generate-key for a production-grade key. Choose RSA with 4096 bits—anything less is insecure. Set an expiration date (e.g., 1 year) to enforce rotation. Use a strong passphrase; store it in a password manager, not in plaintext. The key's email must match the email you use in Git commits. If you have multiple emails, create separate keys or add UIDs. After generation, export the public key: gpg --armor --export <key-id>. Upload this to GitHub/GitLab under Settings > SSH and GPG keys. Never share your private key. For teams, consider using a hardware security key (e.g., YubiKey) to store the private key—this prevents exfiltration even if your machine is compromised.
gpg --edit-key <key-id> > addkey. This allows you to revoke the subkey without invalidating your primary key.Configuring Git to Sign Commits
Once you have a GPG key, tell Git to use it. Set user.signingkey to your key ID. Enable automatic signing with commit.gpgsign = true. For tag signing, set tag.gpgsign = true. If you use multiple machines, replicate this config on each. Use --global for system-wide settings, or per-repo if you have different keys for work and personal projects. Verify with git config --list | grep gpg. On macOS, you may need to install pinentry-mac for passphrase prompts. On Linux, pinentry-gtk or pinentry-tty works. If you use a smartcard, configure gpg-agent to cache the passphrase for a session. Without caching, you'll be prompted for every commit—annoying but secure.
default-cache-ttl and max-cache-ttl in ~/.gnupg/gpg-agent.conf to avoid repeated passphrase prompts. Restart agent with gpgconf --kill gpg-agent.Signing Commits and Tags
With Git configured, every commit is automatically signed. You can also sign manually with git commit -S. For tags, use git tag -s. The -s flag signs the tag. To verify a signed commit, use git log --show-signature. This displays the GPG signature status. If the key is trusted, you'll see 'Good signature'. If not, 'Can't check signature: public key not found'. Always import the signer's public key to verify. For batch verification, use git verify-commit HEAD or git verify-tag v1.0. In a team, enforce signing with server-side hooks or branch protection rules. GitHub and GitLab can require signed commits on protected branches. This prevents unsigned commits from being merged.
-S or -s flags, and verify signatures with git log --show-signature.Managing Multiple GPG Keys
Developers often have multiple identities: work, personal, open-source. You can manage multiple GPG keys by associating each with a different Git email. Use gpg --list-secret-keys to see all keys. For each repo, set user.email and user.signingkey locally (without --global). Alternatively, use Git's includeIf directive in ~/.gitconfig to conditionally load config based on directory. For example, all repos under ~/work/ use the work key. This avoids manual per-repo setup. When rotating keys, add the new key to your GitHub account and keep the old one until all commits are re-signed. Revoke old keys with gpg --revoke-key. Notify your team to import the new public key.
git rebase --exec 'git commit --amend --no-edit -S' but only if you control the history. Otherwise, just start signing new commits.includeIf to automatically select the correct GPG key based on the repository path.Enforcing Signed Commits in CI/CD
Server-side enforcement is critical. On GitHub, enable 'Require signed commits' on protected branches. GitLab has similar settings. For self-hosted Git servers, use a pre-receive hook that rejects unsigned commits. The hook checks git log --show-signature and exits non-zero if the signature is missing or invalid. You can also verify that the commit was signed by a key in an allowed list. This prevents even administrators from bypassing signing. In CI pipelines, use git verify-commit to check the latest commit before building. If verification fails, fail the pipeline. This ensures that only verified code reaches production. For extra security, use a hardware key for the CI signing key and store it in a secure vault.
Troubleshooting Common GPG Issues
Common issues: 'secret key not available' means the private key isn't in the keyring. Import it with gpg --import private.key. 'Can't check signature: public key not found' means the verifier lacks the public key. Share your public key via keyserver or upload to GitHub. 'gpg: signing failed: No pinentry' indicates missing pinentry program. Install pinentry-mac or pinentry-tty. 'gpg: signing failed: Operation cancelled' often means the passphrase dialog timed out. Increase cache TTL in gpg-agent.conf. If commits show 'Good signature' but GitHub shows 'Unverified', ensure the commit email matches the key's UID email exactly. Also, the key must be uploaded to GitHub. For Windows, use Gpg4win and configure Git to use its GPG binary.
export GPG_TTY=$(tty) in your shell profile to fix passphrase prompt issues in terminal-based workflows.Revoking and Rotating GPG Keys
Keys expire or get compromised. Generate a revocation certificate immediately after creating a key: gpg --gen-revoke <key-id>. Store it offline. To revoke, import the certificate: gpg --import revoke.asc. Then upload the revoked public key to keyservers and GitHub. For rotation, create a new key, add it to GitHub, and update Git config. Old commits remain signed with the old key—they'll show as 'Verified' if the key hasn't been revoked. If the old key is compromised, revoke it and re-sign all commits with the new key using git filter-repo or git rebase. This rewrites history, so coordinate with your team. After rotation, delete the old private key securely: gpg --delete-secret-key <key-id>.
Integrating GPG with Git Hosting Platforms
GitHub, GitLab, and Bitbucket all support GPG verification. Upload your public key in user settings. For organizations, you can require members to have verified keys. GitHub shows a 'Verified' badge next to signed commits. GitLab shows 'Verified' in the commit details. Bitbucket supports GPG but requires server-side hooks for enforcement. For self-hosted GitLab, enable 'Require GPG signatures' in project settings. For Gitea, install a plugin. If you use multiple platforms, upload the same public key to each. Some platforms also support SSH signing as an alternative, but GPG is more widely adopted. Ensure your CI system's signing key is also uploaded to the platform so that CI-generated commits show as verified.
Auditing Signed Commits in Production
Regular audits ensure signing compliance. Use git log --show-signature to review all commits in a release. Automate this with a script that fails if any commit is unsigned or signed by an untrusted key. For compliance, export the audit log: git log --format='%H %aN <%aE> %G?' --show-signature. The %G? shows signature status: 'G' for good, 'B' for bad, 'U' for untrusted, 'N' for no signature. Parse this to generate reports. Store the audit logs in a secure, immutable bucket. In case of an incident, you can prove exactly who signed what. If a key is revoked, re-audit all commits signed with that key. Tools like git-evtag can verify signed tags in a reproducible way.
Advanced: Signing with Hardware Security Keys
For maximum security, store your GPG private key on a hardware token like YubiKey or Nitrokey. This prevents key extraction even if your machine is compromised. Generate the key on the device using gpg --card-edit or generate offline and transfer. The key never leaves the token. Signing operations happen on the device. Configure gpg-agent to use the token. On macOS, install yubikey-personalization and gnupg. On Linux, install pcscd and scdaemon. Test with gpg --card-status. The token requires a PIN for each signing operation. This adds friction but is necessary for high-security environments. Some tokens support biometrics. Hardware keys are tamper-resistant and can be revoked by destroying the token.
Automating GPG Key Distribution
In a team, you need a way to distribute public keys. Use a keyserver like keyserver.ubuntu.com or host your own with Hockeypuck. Automate key import in CI: gpg --keyserver keyserver.ubuntu.com --recv-keys <key-id>. For air-gapped environments, distribute keys via a secure channel (e.g., encrypted email). Use a keys/ directory in your repository containing ASCII-armored public keys. In CI, import all keys: gpg --import keys/*.asc. Mark keys as ultimately trusted: echo <key-id>:6: | gpg --import-ownertrust. This prevents 'untrusted' warnings. For large teams, consider using a central key management system like Vault's PKI or a GPG key server with access control.
Impersonated Commit Almost Deployed Malicious Code
git config user.name 'Lead Maintainer' && git config user.email 'lead@project.com'. 3. Attacker created a commit that introduced malicious code. 4. The commit appeared in git log as authored by the lead maintainer. 5. Code review missed the malicious code (it was buried in a large diff). 6. The commit was merged and triggered a CI build. 7. Only a team member noticing the odd commit time prevented the deploy.git log --format='%G?' -1 | grep -q 'G' 5. Added a CI check: git log --format='%G?' --all | grep -v 'G' | wc -l must be 0.- Git author/committer fields are self-reported and can be faked.
- GPG signing is the ONLY way to cryptographically verify identity.
- Enable signed commit requirements in branch protection rules.
- Add CI checks that reject unsigned commits.
commit.gpgsign truegit rebase --exec 'git commit --amend --no-edit -S' to retroactively sign (rewrites history)| File | Command / Code | Purpose |
|---|---|---|
| check-git-config.sh | git config --global user.signingkey || echo "No signing key set" | Why GPG Signing Matters in Production |
| generate-gpg-key.sh | gpg --full-generate-key | Generating a GPG Key Pair |
| configure-git-signing.sh | git config --global user.signingkey 1A2B3C4D5E6F7G8H | Configuring Git to Sign Commits |
| sign-commit.sh | git commit -m "Fix critical bug in auth module" | Signing Commits and Tags |
| conditional-git-config.sh | [includeIf "gitdir:~/work/"] | Managing Multiple GPG Keys |
| pre-receive-hook.sh | while read oldrev newrev refname; do | Enforcing Signed Commits in CI/CD |
| fix-gpg-issues.sh | gpg-agent --daemon | Troubleshooting Common GPG Issues |
| revoke-key.sh | gpg --gen-revoke 1A2B3C4D5E6F7G8H > revoke.asc | Revoking and Rotating GPG Keys |
| upload-key.sh | gpg --armor --export 1A2B3C4D5E6F7G8H > public.asc | Integrating GPG with Git Hosting Platforms |
| audit-signatures.sh | for commit in $(git rev-list HEAD); do | Auditing Signed Commits in Production |
| setup-yubikey.sh | gpg --card-status | Advanced |
| import-team-keys.sh | for keyfile in keys/*.asc; do | Automating GPG Key Distribution |
Key takeaways
commit.gpgsign true to auto-sign every commitFrequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Git. Mark it forged?
5 min read · try the examples if you haven't