skip to content

gpg — OpenPGP Encryption & Signing

Practical GnuPG cheat sheet — generate keys, sign and verify files, encrypt for a recipient, sign git commits and tags, and manage trust without the bureaucracy.

19 min read 60 snippets deep dive

gpg — OpenPGP Encryption & Signing#

What it is#

gpg is the command-line interface to GnuPG (GNU Privacy Guard), the free implementation of the OpenPGP standard (RFC 4880) maintained by Werner Koch and the GnuPG team since 1997. It provides public-key cryptography for files, email, and git — generating keypairs, signing artefacts, verifying signatures, and encrypting/decrypting data with strong asymmetric and symmetric ciphers. Reach for gpg when you need detached signatures (release tarballs, git commits), email encryption (PGP/MIME), or password-store backends; for modern file-only encryption with smaller keys, age is a simpler alternative.

Install#

GnuPG is preinstalled on most Linux distributions and macOS via Homebrew. The current development line is 2.5.x (2.5.19 was released April 2026 and introduces post-quantum ML-KEM support); 2.4.x went unmaintained two months after 2.5.19 landed, so plan on moving to 2.5+ for new keys. Legacy gpg1 (1.4) remains for verifying very old signatures but should not be used for new key generation.

# Debian/Ubuntu
sudo apt install gnupg

# RHEL/Fedora
sudo dnf install gnupg2

# macOS
brew install gnupg

# Verify
gpg --version

Output:

gpg (GnuPG) 2.5.19
libgcrypt 1.11.0
Copyright (C) 2026 g10 Code GmbH
Home: /home/alice/.gnupg
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA, KYBER
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, ...
AEAD: EAX, OCB
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2

Syntax#

GPG dispatches by long flags rather than subcommands. Output goes to stdout by default unless redirected with -o; --armor switches binary output to base64-armoured text for transport over email or pasted into a web form.

gpg [OPTIONS] --<action> [ARGS...]
gpg --gen-key            # interactive
gpg --encrypt -r ALICE FILE
gpg --decrypt FILE.gpg
gpg --sign FILE
gpg --verify FILE.sig FILE

Output: (none — exits 0 on success)

Essential options#

FlagMeaning
-a / --armorASCII-armoured (base64) output instead of binary
-o FILE / --output FILEWrite to FILE instead of stdout
-r USER / --recipient USEREncrypt for this public key
-u USER / --local-user USERSign with this secret key
-s / --signMake a signature
-b / --detach-signMake a detached signature (separate .sig)
--clearsignSign a text file, leaving the body human-readable
-e / --encryptEncrypt for one or more -r recipients
-c / --symmetricSymmetric encryption (passphrase, no keypair)
-d / --decryptDecrypt (also verifies if signed)
--verifyVerify a signature only
--list-keys / -kList public keys
--list-secret-keys / -KList secret keys
--fingerprintShow key fingerprints
--export / --export-secret-keysPrint key to stdout
--importImport a key from stdin or file
--edit-key USEROpen the interactive key editor
--delete-key / --delete-secret-keyRemove a key from the keyring

The GnuPG home directory#

GnuPG stores keys, the trust database, and the agent socket in ~/.gnupg/ (overridable with GNUPGHOME or --homedir). Treat this directory as sensitive — back it up before any destructive operation.

ls -la ~/.gnupg/
GNUPGHOME=/tmp/gpgtest gpg --gen-key       # isolated keyring for testing

# Back up the entire home dir before key edits
tar czf ~/gnupg-backup-$(date +%F).tar.gz -C ~ .gnupg

Output:

drwx------  5 alice alice 4096 May 24 14:00 .
drwx------ 30 alice alice 4096 May 24 14:00 ..
-rw-------  1 alice alice 1280 May 24 14:00 gpg.conf
-rw-------  1 alice alice 9100 May 24 14:00 pubring.kbx
drwx------  2 alice alice 4096 May 24 14:00 private-keys-v1.d
drwx------  2 alice alice 4096 May 24 14:00 openpgp-revocs.d
-rw-------  1 alice alice 1600 May 24 14:00 trustdb.gpg

Generate a keypair#

--full-generate-key walks through every option — algorithm, key size, expiry, identity. For most users the safer-by-default --quick-gen-key produces a modern Ed25519 signing key with an ECDH encryption subkey in one line. Always set a real expiry (1–2 years) — you can extend it later, but a key with no expiry can never be retired cleanly if you lose the secret.

# Interactive — pick algorithm, size, expiry, identity
gpg --full-generate-key

# One-shot: modern ed25519 keypair, 2-year expiry
gpg --quick-gen-key "Alice Dev <alice@example.com>" ed25519 default 2y

# Batch (unattended) — useful in scripts
cat > keyparams.conf <<EOF
%echo Generating Alice Dev key
Key-Type: EDDSA
Key-Curve: ed25519
Subkey-Type: ECDH
Subkey-Curve: cv25519
Name-Real: Alice Dev
Name-Email: alice@example.com
Expire-Date: 2y
Passphrase: change-me
%commit
%echo Done
EOF
gpg --batch --gen-key keyparams.conf

Output:

gpg: key 9F1B2C3D4E5F6789 marked as ultimately trusted
gpg: revocation certificate stored as '/home/alice/.gnupg/openpgp-revocs.d/A1B2...rev'
public and secret key created and signed.

pub   ed25519 2026-05-24 [SC] [expires: 2028-05-24]
      A1B2C3D4E5F6789012345678901234567890ABCD
uid                      Alice Dev <alice@example.com>
sub   cv25519 2026-05-24 [E] [expires: 2028-05-24]

List, fingerprint, and identify keys#

Keys are referenced by fingerprint (40 hex chars), long ID (last 16), short ID (last 8 — avoid; collisions exist), or user ID substring. Always prefer fingerprints for anything important.

gpg --list-keys                      # all public keys
gpg --list-keys alice@example.com    # by user ID substring
gpg --list-secret-keys               # secret keys you hold
gpg --fingerprint alice@example.com  # show fingerprint
gpg --list-keys --keyid-format=long  # show 16-char long IDs

# Machine-readable
gpg --list-keys --with-colons | awk -F: '/^pub/ {print $5}'

Output:

/home/alice/.gnupg/pubring.kbx
------------------------------
pub   ed25519 2026-05-24 [SC] [expires: 2028-05-24]
      A1B2 C3D4 E5F6 7890 1234  5678 9012 3456 7890 ABCD
uid           [ultimate] Alice Dev <alice@example.com>
sub   cv25519 2026-05-24 [E] [expires: 2028-05-24]

Export and import keys#

--export writes the public key; --export-secret-keys writes the secret half (treat this output as highly sensitive). -a produces ASCII-armoured output that’s safe to paste into emails or commits.

# Export public key
gpg --export -a alice@example.com > alice.pub.asc

# Export secret key (BACK UP THIS FILE, then store it offline)
gpg --export-secret-keys -a alice@example.com > alice.sec.asc

# Export revocation certificate (run this immediately after key gen)
gpg --gen-revoke -a alice@example.com > alice.revoke.asc

# Import a key
gpg --import bob.pub.asc

# Import from URL
curl -s https://example.com/release.pub.asc | gpg --import

Output:

gpg: key B0B1B2B3B4B5B6B7: public key "Bob Smith <bob@example.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1

Sign a file#

A signature proves the signer held the corresponding secret key at the time of signing. There are three flavours: inline (the original --sign, wraps the data in an OpenPGP packet), clear-signed (text remains readable), and detached (signature lives in a separate .sig / .asc file — the canonical form for release artefacts).

# Inline (binary OpenPGP message containing the original)
gpg --sign report.txt                    # produces report.txt.gpg

# Clear-signed text — body is still readable
gpg --clearsign release-notes.md         # produces release-notes.md.asc

# Detached binary signature
gpg --detach-sign archive.tar.gz         # produces archive.tar.gz.sig

# Detached ASCII signature (typical for tarballs distributed on the web)
gpg --detach-sign --armor archive.tar.gz # produces archive.tar.gz.asc

# Sign with a specific key
gpg -u alice@example.com -ab archive.tar.gz

Output:

$ ls archive.tar.gz*
archive.tar.gz
archive.tar.gz.asc

Verify a signature#

--verify checks a signature against the original file. For detached signatures, pass both files; GPG prints the signer’s identity and a Good signature or BAD signature verdict.

# Detached
gpg --verify archive.tar.gz.asc archive.tar.gz

# Inline / clearsigned (extract & verify in one go)
gpg --decrypt release-notes.md.asc       # prints body, exits 0 if valid

# Just the verify step on an inline file
gpg --verify report.txt.gpg

Output:

gpg: Signature made Sun May 24 14:00:00 2026 UTC
gpg:                using EDDSA key A1B2C3D4E5F6789012345678901234567890ABCD
gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
# BAD signature output:
gpg: BAD signature from "Alice Dev <alice@example.com>" [ultimate]

The exit code is 0 on a good signature and non-zero otherwise — fine to use in scripts.

if gpg --verify --quiet archive.tar.gz.asc archive.tar.gz 2>/dev/null; then
  echo "signature OK"
else
  echo "signature FAILED"; exit 1
fi

Output: (none — exits 0 on success)

Encrypt for a recipient#

--encrypt -r ID encrypts a file so that only the holder of the recipient’s secret key can decrypt it. Multiple -r flags add multiple recipients (each gets a copy of the session key). Combine with --sign to make the message both confidential and authenticated.

# Encrypt for Bob; only Bob can decrypt
gpg --encrypt -r bob@example.com message.txt

# Encrypt + sign (recommended) — Bob can verify it really came from Alice
gpg --encrypt --sign -r bob@example.com -u alice@example.com message.txt

# Multiple recipients (yourself + Bob, so you can read it later)
gpg -e -r bob@example.com -r alice@example.com message.txt

# ASCII-armoured for pasting into email
gpg -ea -r bob@example.com message.txt    # produces message.txt.asc

Output:

$ ls message.txt*
message.txt
message.txt.gpg          # binary form
message.txt.asc          # ASCII-armoured form

Decrypt#

--decrypt (or just -d, or passing the encrypted file as input) prints the plaintext to stdout. Use -o to write to a file. If the message was signed, GPG verifies the signature automatically and reports it alongside the decryption.

# Decrypt to stdout
gpg --decrypt message.txt.gpg

# Decrypt to a file
gpg -o message.txt -d message.txt.gpg

# Decrypt a signed-and-encrypted file (verifies sig too)
gpg -d secrets.gpg

Output:

gpg: encrypted with 256-bit ECDH key, ID 0123456789ABCDEF, created 2026-05-24
      "Alice Dev <alice@example.com>"
gpg: Signature made Sun May 24 14:00:00 2026 UTC
gpg:                using EDDSA key A1B2C3D4E5F6789012345678901234567890ABCD
gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
<plaintext body here>

Symmetric (passphrase-only) encryption#

-c / --symmetric skips the recipient model entirely — the file is encrypted with a passphrase you type interactively. Good for “send a single file to one person over an untrusted channel”; bad for multi-recipient or long-term storage.

gpg -c archive.tar              # produces archive.tar.gpg
gpg -ca archive.tar             # ASCII-armoured

# Choose the cipher (AES256 is the modern default; spell it out for old GPG)
gpg --cipher-algo AES256 -c archive.tar

# Non-interactive (for scripts) — read passphrase from file
gpg --batch --passphrase-file pw.txt -c archive.tar

Output:

$ ls archive.tar*
archive.tar
archive.tar.gpg

Edit a key — expiry, subkeys, UIDs#

--edit-key opens an interactive sub-shell where you can extend expiry, add or revoke subkeys, add new user IDs, change the passphrase, and trust other keys. Type help at the prompt to see all sub-commands.

gpg --edit-key alice@example.com

Output:

gpg> expire        # extend the primary key's expiry
gpg> key 1         # select subkey 1
gpg> expire        # extend it too
gpg> adduid        # add an additional name/email
gpg> passwd        # change the passphrase
gpg> trust         # set trust level (1..5)
gpg> save          # commit and exit
# Non-interactive: extend expiry by 2 years
gpg --quick-set-expire ALICE_FPR 2y

# Revoke a key (after losing the secret, use the saved revocation cert)
gpg --import alice.revoke.asc

Output:

gpg: key A1B2C3D4E5F6789012345678901234567890ABCD: "Alice Dev <alice@example.com>" revoked

Trust model#

GPG marks each known key with a trust level. The keys you generated yourself are ultimate; everything else starts unknown until you certify it. The default “web of trust” model only validates a key if it’s been signed by enough keys you trust.

LevelMeaning
unknownNo information
neverDon’t trust this key’s signatures on other keys
marginalTrust signatures partially (n marginal = 1 full)
fullTrust this key to certify others
ultimateSame as full, used for your own keys
# Sign someone's key after verifying their fingerprint in person/by video
gpg --sign-key bob@example.com

# Set trust without signing (private decision)
gpg --edit-key bob@example.com
gpg> trust
gpg> 4               # I trust fully
gpg> save

# Disable trust checks entirely (TOFU model — Trust On First Use)
gpg --trust-model=tofu --verify file.asc file

Output:

gpg: 3 marginal(s) needed, 1 complete(s) needed, classic trust model
gpg: depth: 0  valid:   1  signed:   1  trust: 0-, 0q, 0n, 0m, 0f, 1u

Keyservers#

Public keyservers distribute keys by ID. Modern best practice points to keys.openpgp.org (validates email ownership before publishing) rather than the old SKS pool.

# Set default keyserver (in ~/.gnupg/gpg.conf)
echo 'keyserver hkps://keys.openpgp.org' >> ~/.gnupg/gpg.conf

# Publish your public key
gpg --send-keys A1B2C3D4E5F6789012345678901234567890ABCD

# Search for a key
gpg --search-keys alice@example.com

# Fetch a known key by fingerprint
gpg --recv-keys A1B2C3D4E5F6789012345678901234567890ABCD

# Refresh all local keys (catch revocations)
gpg --refresh-keys

Output:

gpg: sending key A1B2C3D4E5F6789012345678901234567890ABCD to hkps://keys.openpgp.org
gpg: key A1B2C3D4E5F6789012345678901234567890ABCD: public key "Alice Dev <alice@example.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1

Sign git commits and tags#

Git can shell out to gpg to sign commits, tags, and merges; GitHub, GitLab, and Gitea display a “Verified” badge for valid signatures whose key you’ve uploaded to your profile.

# 1. Tell git which key to use
gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey A1B2C3D4E5F67890

# 2. Sign every commit automatically
git config --global commit.gpgsign true
git config --global tag.gpgsign true

# 3. (macOS only) tell gpg to use a TTY-aware pinentry
echo "use-agent" >> ~/.gnupg/gpg.conf
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent

# 4. Make a signed commit / tag explicitly
git commit -S -m "Signed commit"
git tag -s v1.0 -m "Release 1.0"

# Inspect signatures on the log
git log --show-signature
git tag -v v1.0

Output:

commit a1b2c3d4 (HEAD -> main)
gpg: Signature made Sun May 24 14:00:00 2026 UTC
gpg:                using EDDSA key A1B2C3D4E5F6789012345678901234567890ABCD
gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
Author: Alice Dev <alice@example.com>
Date:   Sun May 24 14:00:00 2026 +0000

    Signed commit

Upload gpg --armor --export alice@example.com to your Git host’s “GPG keys” settings page so signatures verify on the web UI.

gpg-agent and pinentry#

gpg-agent caches your passphrase between invocations so you don’t retype it constantly, and delegates passphrase prompts to a pinentry-* program (curses, GTK, Qt, or mac). Tune the cache time in ~/.gnupg/gpg-agent.conf (see Configuration below).

# Reload agent after changing config
gpgconf --kill gpg-agent
gpgconf --launch gpg-agent

# Status
gpg-connect-agent 'keyinfo --list' /bye

Output:

S KEYINFO A1B2C3D4...  D  -  -  P  -  -  -
OK

Configuration#

GnuPG reads three plain-text config files under ~/.gnupg/ (or $GNUPGHOME). Each is loaded by a different component, so options must go in the right file or they’re silently ignored. Settings apply on next invocation for gpg.conf, after gpgconf --reload gpg-agent for gpg-agent.conf, and after gpgconf --reload dirmngr for dirmngr.conf.

FileRead byPurpose
~/.gnupg/gpg.confgpgDefault flags, algorithm preferences, keyserver, output formatting
~/.gnupg/gpg-agent.confgpg-agentPassphrase cache TTL, pinentry binary, SSH support
~/.gnupg/dirmngr.confdirmngrKeyserver, HKP/HKPS options, Tor routing, CRL fetching

gpg.conf — sensible defaults#

These defaults prefer modern algorithms, suppress noisy headers, and avoid leaking the short-ID footgun.

# ~/.gnupg/gpg.conf
keyid-format             0xlong
with-fingerprint
no-emit-version
no-comments
personal-cipher-preferences   AES256 AES192 AES
personal-digest-preferences   SHA512 SHA384 SHA256
personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed
default-preference-list       SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed
cert-digest-algo              SHA512
s2k-cipher-algo               AES256
s2k-digest-algo               SHA512
s2k-mode                      3
s2k-count                     65011712
keyserver                     hkps://keys.openpgp.org
# GnuPG 2.5+: opt in to OCB AEAD for symmetric encryption
use-ocb-sym

gpg-agent.conf — cache and pinentry#

# ~/.gnupg/gpg-agent.conf
default-cache-ttl 3600            # 1 hour after last use
max-cache-ttl     28800           # 8 hours absolute max
pinentry-program  /usr/bin/pinentry-curses
# Uncomment to make gpg-agent serve SSH keys too:
# enable-ssh-support

dirmngr.conf — keyserver and network#

dirmngr is the daemon that actually talks to keyservers and downloads CRLs; gpg --send-keys and gpg --recv-keys are thin clients that hand the request to it. Override the keyserver here (or in gpg.conf) and tunnel over Tor for metadata privacy.

# ~/.gnupg/dirmngr.conf
keyserver         hkps://keys.openpgp.org
hkp-cacert        /etc/ssl/certs/ca-certificates.crt
# Route all keyserver traffic through Tor (requires tor running on 9050):
# use-tor
# Tune timeouts for flaky networks:
connect-timeout   15
http-timeout      30
# Reload daemons after edits
gpgconf --reload gpg-agent
gpgconf --reload dirmngr

# Show everything gpgconf knows about
gpgconf --list-options gpg | head
gpgconf --check-options gpg-agent

Output: (none — exits 0 on success)

The 2026 OpenPGP landscape: LibrePGP vs RFC 9580#

OpenPGP’s standardisation has split. After years of stalemate, the IETF published RFC 9580 (“OpenPGP”, July 2024) defining a v6 key format with mandatory AEAD (authenticated encryption) and obsoleting RFC 4880. GnuPG’s maintainer Werner Koch declined to implement v6 and instead drives LibrePGP — a competing successor based on the older crypto-refresh draft, defining its own incompatible v5 key format. Proton, Sequoia-PGP, and the IETF back RFC 9580; GnuPG ships LibrePGP. The two ecosystems generate keys that the other side cannot fully consume, so most deployments still use the v4 format from RFC 4880 to maximise interoperability.

# Inspect a key's packet version (v4 / v5 / v6)
gpg --list-packets alice.pub.asc | grep -E 'version|public key packet'

Output:

:public key packet:
        version 4, algo 22, created 1716566400, expires 0

Post-quantum cryptography is landing#

GnuPG 2.5.19 (April 2026) was the first stable release with post-quantum encryption — specifically ML-KEM (Kyber, FIPS-203) as a key-encapsulation mechanism, exposed as new Pubkey: KYBER and used in composite (classical + PQ) subkeys following the draft-ietf-openpgp-pqc IETF draft. The 2.4 series went unmaintained two months after 2.5.19 shipped. Sequoia-PGP gained PQ support in late 2025 (also via composite ML-KEM subkeys), so cross-implementation interop for PQ messages is now possible but still draft-stage.

# GnuPG 2.5+: generate a key with a post-quantum encryption subkey
gpg --full-generate-key --expert      # pick "(13) ECC + Kyber" or similar

Output: (none — opens interactive prompt)

Sequoia-PGP and sq as a modern alternative#

Sequoia-PGP is a Rust reimplementation of OpenPGP led by ex-GnuPG developers. Its CLI, sq, is a clean redesign with git-style subcommands (sq key generate, sq encrypt, sq sign) and uses RFC 9580 v6 keys by default. Two related projects let you opt out of GnuPG without losing tooling compatibility:

  • sqv — a tiny signature-verifier; Debian’s apt uses it by default to verify archive signatures on most architectures.
  • sequoia-chameleon-gnupg (Debian package gpg-from-sq) — a drop-in replacement for the gpg binary that speaks the GnuPG CLI but runs on Sequoia underneath. Installing it diverts the real gpg to gpg-g10code.
# Try sq alongside gpg (Debian/Ubuntu)
sudo apt install sq sqv
sq key generate --userid "Alice Dev <alice@example.com>" --output alice-sq.key
sq inspect alice-sq.key

# Drop-in replacement for /usr/bin/gpg
sudo apt install gpg-from-sq
gpg --version       # now reports Sequoia's gpg-sq

Output:

gpg-sq (sequoia-chameleon-gnupg 0.x.y) ...
A reimplementation of the gpg interface using Sequoia.

Reach for sq (or age) for new greenfield deployments, but keep gpg for anything that needs OpenPGP v4 interoperability — email plugins, legacy release pipelines, and the long tail of distros that haven’t migrated yet.

Common pitfalls#

  1. Short IDs collide. Never reference a key by an 8-char short ID; collisions have been demonstrated. Use the full fingerprint, or at minimum the 16-char long ID (--keyid-format=long).
  2. No revocation certificate stored. Generate it the moment you create a keypair (--gen-revoke) and stash it offline — without it, a lost secret key can never be repudiated.
  3. Encrypting for only the recipient. If you encrypt a file with -r bob@example.com and don’t add -r yourself, you can’t read it later. Add yourself as a second recipient for archival files.
  4. --symmetric + bad passphrase. GnuPG won’t tell you the passphrase is wrong, just that it can’t decrypt. Test the passphrase by decrypting immediately after encryption.
  5. pinentry hangs in CI. Set --batch --pinentry-mode loopback and pass --passphrase or --passphrase-file. Otherwise GPG waits forever for a TTY that doesn’t exist.
  6. macOS commits show “unverified”. Install pinentry-mac via Homebrew and point gpg-agent.conf at it, or commits will fail silently because the GUI passphrase prompt can’t show.
  7. Old DSA/1024-bit keys. Anything generated before 2018 with DSA-1024 or RSA-1024 is obsolete. Migrate to Ed25519 / RSA-4096 and revoke the old key.
  8. --allow-secret-key-import is gone in 2.x. Just use --import.

Real-world recipes#

Produce and verify a detached signature for a release#

The most common open-source release flow — ship the artefact plus a .asc signature, document the signing key’s fingerprint on the project page.

# Sign
gpg --detach-sign --armor -u alice@example.com release-1.0.tar.gz
sha256sum release-1.0.tar.gz > release-1.0.tar.gz.sha256

# What the consumer runs
gpg --recv-keys A1B2C3D4E5F6789012345678901234567890ABCD
gpg --verify release-1.0.tar.gz.asc release-1.0.tar.gz
sha256sum -c release-1.0.tar.gz.sha256

Output:

gpg: Good signature from "Alice Dev <alice@example.com>" [ultimate]
release-1.0.tar.gz: OK

Encrypt a backup directory for offsite storage#

Tar + GPG produces a single encrypted blob you can ship to any storage backend.

tar czf - /home/alice/photos \
  | gpg --encrypt -r alice@example.com -o photos-$(date +%F).tar.gz.gpg

# Restore later
gpg -d photos-2026-05-24.tar.gz.gpg | tar xzf - -C /tmp/restore

Output:

$ ls -lh photos-2026-05-24.tar.gz.gpg
-rw------- 1 alice alice 1.4G May 24 14:00 photos-2026-05-24.tar.gz.gpg

Verify a downloaded tarball without trusting any global keyring#

Use a per-project GNUPGHOME so the project’s signing key doesn’t pollute your personal trust db.

PROJECT_HOME=$(mktemp -d)
GNUPGHOME=$PROJECT_HOME gpg --import upstream-release.pub.asc
GNUPGHOME=$PROJECT_HOME gpg --verify release-1.0.tar.gz.asc release-1.0.tar.gz
rm -rf "$PROJECT_HOME"

Output:

gpg: key A1B2...ABCD: public key "Upstream Release <release@example.org>" imported
gpg: Good signature from "Upstream Release <release@example.org>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!

Sign apt repositories#

Most distros verify package indexes with GPG. To host an apt repo you sign the Release file and publish the public key for clients to install in /etc/apt/trusted.gpg.d/.

# Sign the Release file (creates Release.gpg detached + InRelease inline)
gpg --default-key alice@example.com -abs -o Release.gpg Release
gpg --default-key alice@example.com --clearsign -o InRelease Release

# Publish the signer's public key
gpg --export -a alice@example.com > /var/www/repo/keyring.asc

Output:

$ ls Release*
Release          InRelease       Release.gpg

Encrypt a one-off file for a recipient you just met#

You have their public key on a keyserver but no signed trust path yet. --trust-model=always skips the trust check for this one operation.

gpg --recv-keys 1234567890ABCDEF
gpg --trust-model=always -e -r 1234567890ABCDEF message.txt

Output:

$ ls message.txt*
message.txt
message.txt.gpg

Re-encrypt every file in a directory after rotating recipients#

When someone leaves the team you re-issue the key list. Loop over each .gpg file, decrypt, re-encrypt with the new recipient set.

NEW_RECIPS=("-r alice@example.com" "-r charlie@example.com")
for f in *.gpg; do
  base="${f%.gpg}"
  gpg -d "$f" | gpg -e ${NEW_RECIPS[@]} -o "$base.new.gpg"
  mv "$base.new.gpg" "$f"
done

Output:

$ ls *.gpg | wc -l
12

CI: verify a release artefact with no interactivity#

Useful inside Docker build steps or GitHub Actions. The --batch --pinentry-mode loopback combination silences passphrase prompts; the key here has no passphrase since it only verifies, never signs.

mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
curl -fsSL https://example.com/release.pub.asc | gpg --batch --import
gpg --batch --verify release-1.0.tar.gz.asc release-1.0.tar.gz

Output:

gpg: Good signature from "Upstream Release <release@example.org>"

Rotate (extend) an expired key without losing the identity#

When your primary key reaches its expiry, extend it before key consumers fall off the trust path.

gpg --quick-set-expire A1B2C3D4E5F6789012345678901234567890ABCD 2y
# Extend each subkey too
for sub in $(gpg --list-keys --with-subkey-fingerprints alice@example.com \
              | awk '/^fpr:::::::::[A-F0-9]+:/' RS=':' | sed -n 2~1p); do
  gpg --quick-set-expire A1B2C3D4E5F6789012345678901234567890ABCD 2y "$sub"
done

# Republish
gpg --send-keys A1B2C3D4E5F6789012345678901234567890ABCD

Output:

gpg: sending key A1B2C3D4E5F6789012345678901234567890ABCD to hkps://keys.openpgp.org

[!TIP] Add --always-trust to encryption commands only when you’ve manually verified the recipient out-of-band (phone call, in-person fingerprint check). Bypassing the trust db should be a deliberate choice, not the default.

[!TIP] For modern file-only encryption between machines you control, age (github.com/FiloSottile/age) is dramatically simpler — small keys, no web of trust, one binary. Use gpg when you need OpenPGP interoperability (email, package signing, git).

Sources#