Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Sign Your Git Commits with GPG or SSH

By Kokil Thapa | Last reviewed: September 2026

Unsigned Git commits are easy to forge. Anyone who knows your email can push code under your name without touching your account. When you sign your Git commits with GPG or SSH, Git attaches a cryptographic proof that only your private key could produce. Platforms like GitHub and GitLab display a green Verified badge. Supply-chain audits and CI pipelines can reject unsigned work before it merges. This guide walks through both methods on Linux and macOS, with copy-paste commands you can run today on a Ubuntu development or deploy server.

Why should you sign your Git commits with GPG or SSH?

Commit metadata is plain text. The author name and email in git log carry no built-in proof of identity. A disgruntled contractor, a compromised laptop, or a misconfigured CI job can all push commits that look like they came from you.

Signed commits change that. Git computes a hash of the commit object and encrypts it with your private key. Anyone with your public key can verify the signature locally or on the remote. On GitHub, verified commits show a green badge next to the SHA.

In my experience working on production Laravel applications, signed commits matter most on long-lived client repos. A law-firm portal or eCommerce codebase may pass through three developers over five years. Signing makes authorship auditable without trusting email headers alone.

How Git Commit Signing WorksDeveloperPrivate keyGit CommitHash + signatureRemote HostPublic key matchVerifiedGreen badgeWithout Signing: Forged Author EmailAttacker sets git config user.email to your addressPush succeeds — log shows your name, no proofSigned commits block this: signature fails verification
Git commit signing ties each commit hash to a private key only you hold, producing a Verified status on GitHub and GitLab.

Signed tags protect release points the same way. A tag signature proves the person who cut v2.4.1 actually controlled the signing key. That pairs well with conventional commits and semantic versioning workflows where automated changelogs depend on trustworthy history.

How do GPG and SSH commit signing compare?

Git supports two signing backends. GPG has been the standard for over a decade. SSH commit signing arrived in Git 2.34 and uses the same Ed25519 keys many developers already use for authentication.

For most new setups in 2026, SSH signing wins on simplicity. You skip keyservers, pinentry popups, and GPG agent quirks. GPG still makes sense when your organisation mandates OpenPGP or you need cross-tool signature compatibility.

CriteriaGPG SigningSSH Signing
Minimum Git versionAny recent GitGit 2.34+
Key generationgpg --full-generate-keyssh-keygen -t ed25519
Agent overheadGPG agent + pinentryssh-agent (often already running)
GitHub supportUpload GPG public keyUpload SSH key as signing key
CI/CD fitExport secret key carefullySame SSH key patterns as deploy keys
Best forLegacy teams, OpenPGP policyNew projects, solo developers

Both methods produce the same Verified badge on GitHub when configured correctly. Pick one per machine and stay consistent. Mixing GPG on your laptop and SSH on CI is fine as long as each identity maps to the right public key on the remote.

GPG vs SSH Signing DecisionNeed commit signing?Org requires OpenPGP?Yes → GPGGit 2.34 or newer?Yes → SSH pathAlready use ssh-agent?Yes → SSH signingUse GPGKeyserver uploadUse SSHSimpler daily workflowBoth produce Verified commits on GitHub and GitLab
Choose SSH signing for new Git 2.34+ setups; choose GPG when OpenPGP compliance or legacy tooling requires it.

How do you sign Git commits with an SSH key?

SSH signing reuses the key format you already know from SSH key-only authentication. Git 2.34 introduced gpg.format ssh. The workflow below takes about five minutes on Ubuntu 22 or 24.

Generate a dedicated signing key

Do not reuse your authentication key for signing. Generate a separate Ed25519 key so a compromised CI token cannot both push and sign.

ssh-keygen -t ed25519 -C "signing@yourdomain.com" -f ~/.ssh/id_ed25519_signing
chmod 600 ~/.ssh/id_ed25519_signing
chmod 644 ~/.ssh/id_ed25519_signing.pub

Add the private key to your agent. On Linux with systemd, ssh-add ~/.ssh/id_ed25519_signing persists until reboot unless you configure keychain integration.

Configure Git for SSH signing

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_signing.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true

The signing key must point to the .pub file path or the key fingerprint string. Git reads the public key to embed the signer identity in the signature block.

Upload the public key to GitHub

  1. Copy the public key: cat ~/.ssh/id_ed25519_signing.pub
  2. Open GitHub → Settings → SSH and GPG keys → New SSH key
  3. Select Signing Key as the key type
  4. Paste and save

GitLab follows the same pattern under User Settings → SSH Keys, checking the signing option. After upload, create a test commit and push. The commit page should show Verified within seconds.

Verify locally before pushing

git commit -S -m "test: verify SSH signing"
git log --show-signature -1

Good output includes Good "git" signature with your signing key fingerprint. Bad output usually means the public key is not on the remote or user.signingkey points to the wrong file.

How do you sign Git commits with GPG?

GPG signing remains the path for teams that already distribute public keys through keyservers or internal PKI. The official Git documentation on signing your work still centres on GPG as the default format.

Install GnuPG and generate a key

sudo apt update && sudo apt install -y gnupg
gpg --full-generate-key

Choose RSA 4096 or Ed25519 if your GnuPG version supports it. Use the same email address as your GitHub account. Set an expiry date — one to two years is reasonable. You can extend it later with gpg --edit-key.

Export the key ID and configure Git

gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey YOUR_LONG_KEY_ID
git config --global commit.gpgsign true
git config --global tag.gpgsign true

Tell Git which program signs commits. This line prevents "gpg: signing failed" errors on headless servers:

git config --global gpg.program gpg

Upload your GPG public key to GitHub

gpg --armor --export YOUR_LONG_KEY_ID

Copy the armoured block including BEGIN PGP PUBLIC KEY BLOCK. Paste it under GitHub Settings → SSH and GPG keys → New GPG key. The email on the key must match a verified email on your account.

Fix common GPG agent problems on Linux

Headless SSH sessions often fail because pinentry cannot open a GUI. Add this to ~/.gnupg/gpg-agent.conf:

default-cache-ttl 34560000
max-cache-ttl 34560000

Reload the agent with gpgconf --kill gpg-agent. For WSL or remote tmux sessions, install pinentry-curses so the passphrase prompt renders in the terminal. I've encountered this during production deployments on shared EC2 boxes where Deployer runs Git operations over SSH.

Commit Signing Setup Steps1. GenerateKey pair2. ConfigureGit config3. UploadPublic key4. TestSigned commitVerifiedSSH Path (Git 2.34+)ssh-keygen -t ed25519gpg.format sshUpload as Signing KeyUses existing ssh-agentGPG Path (Classic)gpg --full-generate-keyuser.signingkey KEYIDUpload armored public keyNeeds gpg-agent + pinentry
Both GPG and SSH signing follow the same four-step pipeline: generate, configure Git, upload the public key, and verify with a test commit.

How do you enforce signed commits in CI and on GitHub?

Local signing is voluntary until the remote rejects unsigned pushes. GitHub organisation owners can require signed commits under Rulesets or branch protection. GitLab offers a similar push rule.

GitHub branch protection

Navigate to Settings → Rules → Rulesets → New ruleset. Target your default branch. Enable Require signed commits. Unsigned pushes return a 403 with a clear error message.

Pair this with Git hooks that automate checks before commit and push. A local pre-commit hook cannot replace server-side enforcement, but it catches missing signatures before CI wastes a runner minute.

Verify signatures in a CI pipeline

GitLab CI and GitHub Actions can audit the last N commits on a merge request. A shell one-liner checks each commit in a range:

git merge-base origin/main HEAD | xargs -I{} git log {}..HEAD --pretty=format:"%H" | while read sha; do
  git verify-commit "$sha" 2>&1 | grep -q "Good signature" || exit 1
done

For SSH-signed commits, ensure the CI runner has the signer's public key in allowed_signers. Create ~/.ssh/allowed_signers:

signing@yourdomain.com ssh-ed25519 AAAAC3Nza... comment

Then set:

git config gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers

This file format is documented in the GitHub commit signature verification guide. Without it, git verify-commit returns "No principal matched" even when GitHub shows Verified.

Signing commits from CI bots

Automated release bots need their own signing key stored as a CI secret. Never embed a human developer's private key in a pipeline variable. Generate a bot-specific SSH or GPG key, upload the public half to GitHub as a signing key, and restrict the private half to protected branch contexts.

On projects I've maintained with Deployer 7 and GitLab CI, bot-signed release tags integrate cleanly with semantic-release. The tag signature proves the pipeline — not a random shell session — cut the release. See also sign commits with GPG and SSH for supply chain trust for a broader security framing.

What are the common mistakes when you sign Git commits with GPG or SSH?

Most signing failures come from configuration drift, not cryptography. The list below covers the issues I see most often on client repos and sister-site deploy pipelines.

  • Email mismatch. The GPG key email or SSH key comment must match a verified GitHub email. Unverified addresses produce Unverified commits even with a valid signature.
  • Reusing auth keys for signing. Keep authentication and signing keys separate. Rotation becomes painful when one key serves both roles.
  • Forgetting commit.gpgsign true. Without global or per-repo config, you must pass -S on every commit. One forgotten flag ships an unsigned merge.
  • Missing allowed_signers for SSH. Local verification fails silently in scripts unless you maintain the allowed signers file.
  • Expired GPG keys. Set a calendar reminder before expiry. An expired key still verifies old commits but cannot sign new ones.
  • Rebase without re-signing. Interactive rebase rewrites commit hashes. Git re-signs automatically if signing is enabled, but old signatures on rewritten SHAs disappear from the log.

Store backup key material offline. Export GPG private keys with gpg --export-secret-keys --armor KEYID. For SSH, back up ~/.ssh/id_ed25519_signing to encrypted storage. A lost signing key does not lock you out of the repo, but your old commits may show as signed by an unknown key after account migration.

Use a strong passphrase on your signing key even in automated workflows where the CI secret store provides the second factor. Treat signing keys with the same care as production database credentials. Run periodic scans with secrets scanning in Git and CI with Gitleaks to catch accidental private-key commits.

Common Signing Failures and FixesUnverified on GitHubEmail not verified on accountFix: Verify emailAdd key with matching addressgpg: signing failedAgent or pinentry not runningFix: Start gpg-agentInstall pinentry-cursesNo principal matchedSSH allowed_signers missingFix: allowedSignersFileMap email to public key403 on pushBranch requires signed commitsFix: Enable gpgsignUpload public key to host
Most Git commit signing errors trace back to email mismatch, missing agents, or absent allowed_signers configuration — all fixable in minutes.

For teams managing multiple deploy keys across servers, document which machine holds which signing identity. A shared dotfiles repo tracked in Git can version your global Git config while keeping private keys out of the tree.

Key Takeaways

  • Enable commit.gpgsign true globally so every commit is signed by default, not only when you remember -S.
  • Prefer SSH signing on Git 2.34+ for simpler agent management; use GPG when policy or tooling demands OpenPGP.
  • Upload the public key to GitHub or GitLab as a signing key, not just an authentication key.
  • Enforce signed commits with GitHub Rulesets or GitLab push rules — local hooks alone are not enough.
  • Maintain allowed_signers for SSH if CI pipelines run git verify-commit.
  • Back up private signing keys securely and rotate them on the same schedule as deploy credentials.

People Also Ask

Does GitHub show verified commits for both GPG and SSH?

Yes. GitHub verifies both GPG and SSH signatures since SSH signing support launched. The badge reads Verified when the signature matches a key registered to your account and the commit email is verified.

Can I sign commits in GitHub Codespaces or VS Code?

Yes. Configure signing in your dotfiles or Codespaces secrets the same way as a local machine. VS Code respects global Git config, so once commit.gpgsign is true, GUI commits are signed automatically.

Do signed commits slow down my workflow?

The overhead is negligible — typically under 200 milliseconds per commit when your agent caches the passphrase. SSH signing through an active ssh-agent adds virtually no perceptible delay during normal development.

What happens to signatures after a rebase or squash merge?

Rebase creates new commit objects with new hashes. Git re-signs them if signing is enabled. Squash merges produce one new signed commit; individual branch signatures do not carry into the squashed result.

Make every commit verifiable on your next project

When you sign your Git commits with GPG or SSH, you add a low-cost layer of supply-chain accountability that scales from solo freelance work to multi-developer agency teams. SSH signing on Git 2.34+ takes minutes to configure. GPG remains the right call when compliance requires it. Either way, turn on global signing, upload your public key, and enforce verification on the default branch.

If you want help hardening Git workflows, CI pipelines, or server access on a production codebase, review the Linux system administration service or browse the Adventure Third Pole Trek portfolio project where signed commits protect a live Laravel booking platform. For passphrase generation on new keys, use the password generator tool. Ready to audit your team's setup? Contact us and we can wire signing into your existing support and maintenance workflow.

Frequently Asked Questions

Git commit metadata is plain text. Anyone who knows your name and email can forge authorship without access to your account. Signing attaches a cryptographic proof only your private key could produce. GitHub and GitLab display a green Verified badge when the signature checks out. Supply-chain audits and CI pipelines can reject unsigned work before it merges. On long-lived client repos I maintain, signing makes authorship auditable years later when multiple developers have touched the same codebase.

Git supports two signing backends. GPG has been the standard for over a decade and suits teams that mandate OpenPGP or rely on keyservers and internal PKI. SSH commit signing arrived in Git 2.34 and reuses Ed25519 keys many developers already generate for authentication, but with a separate signing key. SSH skips GPG agent quirks, pinentry popups, and keyserver uploads. GPG still fits legacy tooling and compliance policies. Both produce the same Verified badge on GitHub when configured correctly. Pick one method per machine and stay consistent.

Generate a dedicated Ed25519 signing key with ssh-keygen, separate from your authentication key. Add the private key to ssh-agent. Set gpg.format to ssh, point user.signingkey at the public key file path or fingerprint, and enable commit.gpgsign and tag.gpgsign globally. Upload the public key to GitHub or GitLab as a Signing Key, not an authentication key. Create a test commit with git commit -S and confirm git log --show-signature shows a Good git signature before pushing. The commit page should show Verified within seconds.

Install GnuPG, then run gpg --full-generate-key. Choose RSA 4096 or Ed25519 if supported, use the same email as your GitHub account, and set a one-to-two-year expiry. Export the long key ID with gpg --list-secret-keys, set user.signingkey, enable commit.gpgsign and tag.gpgsign, and set gpg.program to gpg for headless servers. Export the armoured public key and upload it under GitHub Settings. The key email must match a verified address on your account or commits stay Unverified despite a valid signature.

Git 2.34 or newer. SSH signing uses gpg.format ssh and was not available in earlier releases.

Yes. GitHub verifies both since SSH signing support launched. The badge reads Verified when the signature matches a registered key and the commit email is verified.

No meaningful delay. Overhead is typically under 200 milliseconds per commit with a cached passphrase. SSH signing through ssh-agent adds virtually no perceptible delay.

No. Generate a separate Ed25519 signing key from your authentication key. If one key serves both roles, a compromised CI token or leaked deploy credential could both push code and sign commits as you. Separate keys simplify rotation: you can roll an auth key without invalidating your signing identity, or revoke signing access without breaking SSH deploy access. The article recommends a dedicated file such as id_ed25519_signing with chmod 600 on the private half.

Local signing is voluntary until the remote rejects unsigned pushes. On GitHub, organisation owners enable Require signed commits under Settings, Rules, Rulesets, targeting the default branch. Unsigned pushes return a 403 with a clear error. GitLab offers a similar push rule. Pair server-side rules with local pre-commit hooks that catch missing signatures early, but hooks alone cannot replace remote enforcement because any developer can bypass them. For CI, add a pipeline step that runs git verify-commit on each commit in a merge range.

The most common cause is email mismatch. The GPG key email or SSH key comment must match a verified email on your GitHub account. Unverified addresses produce Unverified commits even when the cryptography is valid. Other frequent issues include uploading the public key as an authentication key instead of a signing key, pointing user.signingkey at the wrong file, or an expired GPG key that can verify old commits but cannot sign new ones. Check git log --show-signature locally first; Good signature locally but Unverified on GitHub almost always means a remote key or email registration problem.

For SSH-signed commits, local verification with git verify-commit requires an allowed_signers file mapping email addresses to public keys. Without it, verification returns No principal matched even when GitHub shows Verified. Create the file with lines like signing@yourdomain.com followed by the ssh-ed25519 public key, then set gpg.ssh.allowedSignersFile in Git config. CI runners that audit signatures need this file plus the signer's public key loaded. GitHub's web UI verifies against keys registered to your account, but shell scripts and pipelines depend on allowed_signers for offline checks.

Rebase creates new commit objects with new hashes, so original signatures do not carry forward on those SHAs. Git re-signs automatically if commit.gpgsign is enabled, but signatures on the old rewritten commits disappear from the log. Squash merges produce one new signed commit on the target branch; individual branch commit signatures do not survive into the squashed result. This is expected behaviour, not a signing failure. Keep global signing enabled before interactive rebase so every rewritten commit gets a fresh valid signature.

GitLab CI and GitHub Actions can audit commits in a merge request range. A shell loop fetches each SHA between merge-base and HEAD, runs git verify-commit, and exits non-zero unless Good signature appears. For SSH-signed commits, the runner must have the allowed_signers file configured and the signer's public key present. Automated release bots need their own bot-specific signing key stored as a CI secret, with the public half uploaded to GitHub as a signing key. Never embed a human developer's private key in pipeline variables.

Yes. Configure signing the same way as on a local machine. In Codespaces, store signing config in dotfiles or Codespaces secrets so new environments inherit the settings. VS Code respects global Git config, so once commit.gpgsign is true, GUI commits are signed automatically without passing -S manually. Ensure ssh-agent or GPG agent is available in the remote environment. Headless Codespaces sessions may hit the same pinentry issues as SSH servers; use gpg-agent cache settings or pinentry-curses for terminal passphrase prompts.

Headless SSH sessions often fail because pinentry cannot open a GUI, producing gpg: signing failed errors. Set gpg.program to gpg in global Git config. Add default-cache-ttl and max-cache-ttl entries to gpg-agent.conf, then reload with gpgconf --kill gpg-agent so passphrases stay cached across deploy operations. On WSL or remote tmux sessions, install pinentry-curses so the passphrase prompt renders in the terminal. I have hit this on shared EC2 boxes where Deployer runs Git operations over SSH during production deployments. The fix takes minutes once you know it is an agent problem, not a bad key.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: