GPG: Git Commit Signing on Linux
This guide sets up a Linux machine so Git signs every commit with a GPG key, and GitHub marks those commits Verified.
Overview
This guide sets up a Linux machine so Git signs every commit with a GPG key, and GitHub marks those commits Verified.
A signature proves who authored a commit and that it hasn't been altered since. GitHub shows the green badge when the signing key is on your account and the commit's email matches a verified email on that account.
Everything here uses the gpg command line, so it works the same on any desktop or over SSH. A GUI key manager such as Kleopatra (KDE) or Seahorse ("Passwords and Keys", GNOME) uses the same ~/.gnupg keyring, so keys you create in either show up in both.
Prerequisites
You need Git, GnuPG 2.x and a pinentry program (the passphrase prompt). Most desktops ship GnuPG already; install whatever is missing:
| Distro | Command |
|---|---|
| Debian / Ubuntu / Mint | sudo apt install git gnupg pinentry-gnome3 (KDE: pinentry-qt) |
| Fedora | sudo dnf install git gnupg2 pinentry-gnome3 (KDE: pinentry-qt) |
| Arch / Manjaro | sudo pacman -S git gnupg pinentry |
| NixOS | Declare it in /etc/nixos/configuration.nix: environment.systemPackages = [ pkgs.git pkgs.gnupg ]; and programs.gnupg.agent = { enable = true; pinentryPackage = pkgs.pinentry-gnome3; }; (KDE: pkgs.pinentry-qt; headless: pkgs.pinentry-curses), then sudo nixos-rebuild switch. On releases before 24.05, use pinentryFlavor = "gnome3"; instead. |
| Headless server / SSH only | add pinentry-curses (Debian/Ubuntu/Fedora package name) |
Also needed:
- A GitHub account with at least one verified email address (Settings → Emails).
- Know which email your commits use:
git config --global user.email. It must match the email you put on the GPG key. - Check the version with
gpg --version. The commands below assume 2.2 or newer.
Part 1 — Generate a GPG key pair
gpg --full-generate-key
Answer the prompts:
- Key type: accept the default, ECC (sign and encrypt), then Curve 25519. GitHub supports it. On older GnuPG that defaults to RSA, choose RSA and RSA at 4096 bits instead.
- Expiry: pick a period that fits your rotation policy, e.g.
2y. - Real name and email: use the email that matches a verified email on GitHub and your Git
user.email. Leave the comment blank. - Confirm with
O, then set a strong passphrase in the pinentry prompt.
GnuPG creates the key in ~/.gnupg and saves a revocation certificate automatically (see Key maintenance).
Part 2 — Find your key ID and export the public key
gpg --list-secret-keys --keyid-format=long
Look for the sec line, for example:
sec ed25519/3AA5C34371567BD2 2026-10-22 [SC] [expires: 2028-10-21]
3AA5C34371567BD2 is the long key ID (yours will differ). The 40-character fingerprint on the next line also works anywhere a key ID is asked for. Export the public key:
gpg --armor --export 3AA5C34371567BD2
To send it straight to the clipboard, pipe it into wl-copy (Wayland) or xclip -selection clipboard (X11).
Part 3 — Add the public key to GitHub
- Copy the whole exported block, including the
-----BEGIN PGP PUBLIC KEY BLOCK-----and-----END PGP PUBLIC KEY BLOCK-----lines. - In GitHub go to Settings → SSH and GPG keys → New GPG key.
- Give it a title (e.g. the machine name), paste the block and click Add GPG key.
Only ever paste the public key here. A block that starts with PRIVATE KEY BLOCK must never leave your machine.
Part 4 — Configure Git and the passphrase prompt
4.1 Set your identity, key and signing defaults
git config --global user.email "you@example.com"
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
git config --global tag.gpgsign true
user.email must match the email on the GPG key. tag.gpgsign is optional and signs annotated tags too. Unlike Windows, you normally don't need gpg.program: Git finds gpg on the PATH.
4.2 Set GPG_TTY
GPG needs to know which terminal to prompt in. Without this, signing fails with Inappropriate ioctl for device, especially over SSH. Add this line to ~/.bashrc (or ~/.zshrc):
export GPG_TTY=$(tty)
Then reload it with source ~/.bashrc. Fish users: set -gx GPG_TTY (tty) in ~/.config/fish/config.fish.
4.3 Choose the pinentry (optional)
The default usually works. To force a specific prompt, for example a terminal prompt on a headless machine, edit ~/.gnupg/gpg-agent.conf:
pinentry-program /usr/bin/pinentry-curses
default-cache-ttl 3600
max-cache-ttl 7200
The two cache lines (in seconds) control how long the passphrase is remembered; the default is 10 minutes. Use pinentry-gnome3 or pinentry-qt for a desktop dialog. Check the path with which pinentry-curses, then reload the agent:
gpgconf --kill gpg-agent
4.4 Make a test commit and verify it
git commit --allow-empty -m "Test signed commit"
git log --show-signature -1
You'll be asked for your passphrase, and git log should report a Good signature. Push, then check the commit on GitHub for the green Verified badge.
Part 5 — Troubleshooting
For any signing error, first run echo test | gpg --clearsign. It shows GPG's real error, which Git hides behind gpg failed to sign the data.
| Symptom | Likely cause and fix |
|---|---|
gpg failed to sign the data | Run the test above for the real error. Most often GPG_TTY is unset (Part 4.2) or the agent is stuck: gpgconf --kill gpg-agent. |
Inappropriate ioctl for device | GPG_TTY isn't set in this shell. Add it to your shell config (Part 4.2) and open a new terminal. |
| No passphrase prompt; the commit hangs | The pinentry can't open, e.g. a GUI dialog over SSH. Set pinentry-curses (Part 4.3) and reload the agent. |
No secret key | user.signingkey has a typo, or the key isn't in this user's keyring. Compare with gpg --list-secret-keys --keyid-format=long. |
WARNING: unsafe permissions on homedir | Fix with chmod 700 ~/.gnupg and chmod 600 ~/.gnupg/*. |
| GitHub shows Unverified | The commit's user.email doesn't match an email on the key, that email isn't verified on GitHub, or the public key isn't on your account. |
| Signing fails after working before | The key has expired. Extend it (Key maintenance) and re-add the public key on GitHub. |
| Works in a terminal but not in VS Code | VS Code has no terminal for a curses prompt: use a GUI pinentry (Part 4.3). The Flatpak or Snap build may not reach your host's gpg-agent; a native package avoids that. |
Part 6 — Setting up a new machine
Either generate a new key per machine (each added to GitHub as an extra key) or reuse one key by importing your backup. A key per machine limits the damage if one machine is lost.
- Install Git, GnuPG and a pinentry (Prerequisites).
- Get a key onto the machine, either way:
- New key: follow Parts 1–3.
- Existing key: copy your backup over securely (e.g.
scp), then rungpg --import private-key.asc. Mark it as yours withgpg --edit-key 3AA5C34371567BD2, then typetrust, choose5(ultimate), andquit. Delete the copied backup file afterwards.
- Set
user.email,user.signingkey,commit.gpgsignandGPG_TTY(Part 4). - Make a test commit and confirm the Verified badge.
Key maintenance
-
Back up the private key. Run
gpg --armor --export-secret-keys 3AA5C34371567BD2 > private-key.ascand store the file offline or in a password manager, never in a repo. Without it you can't sign new commits as this identity and must create and register a new key. -
Keep the revocation certificate. GnuPG saves one automatically in
~/.gnupg/openpgp-revocs.d/. Copy it alongside your backup so you can revoke the key if it is ever compromised. -
Before expiry, set a calendar reminder. Once the key expires,
gpgrefuses to sign and commits fail. To extend the primary key and its subkeys by two years, using the 40-character fingerprint:gpg --quick-set-expire <fingerprint> 2y gpg --quick-set-expire <fingerprint> 2y '*'Then delete and re-add the public key on GitHub so it sees the new date.
-
Old commits stay Verified. GitHub records verification when a commit is pushed, so commits signed while the key was valid keep their badge after it expires.
- #git
- #linux