Understanding the GPG Key Hierarchy Before You Generate Anything
Most breakdowns in GPG usage trace back to one mistake: conflating the master (primary) key with subkeys. The primary key certifies other keys and carries your identity. Subkeys do the actual work: encryption, signing, and authentication. If a subkey is compromised, you revoke and replace it without touching your primary key or its web of trust.
The correct production architecture is: primary key offline (cold storage), three subkeys online (sign, encrypt, authenticate). This is not theoretical hardening - it is standard practice at any organization that has recovered from a key compromise. Generating this setup takes five extra minutes and saves days of pain.
GPG key types worth knowing in 2026: RSA 4096 still works everywhere, Ed25519 is faster and produces smaller signatures with equivalent security, Curve25519 (cv25519) is the standard for encryption subkeys. Our recommendation: Ed25519 primary and sign subkey, cv25519 encrypt subkey. This combination is supported in GnuPG 2.1.0 and later, which covers every distribution you are running.
gpg --version | head -2
# GnuPG 2.4.5
# libgcrypt 1.10.3
Generating a Production-Grade Key with Subkeys
Use `--expert` and `--full-generate-key` together to get full control over algorithm selection. The interactive flow is not difficult once you know the menu numbers.
For the primary key, select option 11 (ECC, set your own capabilities), then toggle off Sign and Encrypt so the primary key is Certify-only. Choose Curve 25519. Set expiry to 2 years - you can extend it, but expiry forces periodic review. For UID, use your real name and a stable email address. If you manage infrastructure under a project identity rather than a personal name, choose that identity carefully; a service like nicename.me can help establish a consistent namespace before you bake an identity into keys that get distributed to partners.
After the primary key, add subkeys immediately in the same session.
gpg --expert --full-generate-key
# Select: (11) ECC (set your own capabilities)
# Toggle: disable Sign, disable Encrypt, keep Certify
# Curve: (1) Curve 25519
# Expiry: 2y
# Then add subkeys:
gpg --expert --edit-key YOUR_KEY_ID
> addkey
# (10) ECC (sign only) -> Ed25519 -> 1y
> addkey
# (12) ECC (encrypt only) -> Curve 25519 -> 1y
> addkey
# (11) ECC (set your own capabilities) -> toggle Auth only -> Ed25519 -> 1y
> save
Exporting, Backing Up, and Stripping the Primary Key
Once your key is generated, export everything to cold storage before you do anything else. A USB drive kept offline is the minimum. A hardware token (YubiKey 5, Nitrokey 3) for the primary key is better.
Export the full keypair including the primary private key to your backup medium first. Then export a version of the secret keyring that contains only subkeys - this is what lives on your workstation and servers.
GnuPG does not have a `--export-secret-subkeys-only` flag that automatically strips the primary. You use `--export-secret-subkeys` which exports subkeys with stubs for the primary. Import that into a separate GNUPGHOME to verify it works before deleting the primary from your working keyring.
# Export full keypair to backup
gpg --export-secret-keys --armor YOUR_KEY_ID > /mnt/backup/full-secret.asc
gpg --export --armor YOUR_KEY_ID > /mnt/backup/public.asc
gpg --export-secret-subkeys --armor YOUR_KEY_ID > ~/subkeys-only.asc
# Test the subkeys-only export in an isolated keyring
mkdir -m 700 /tmp/test-gnupghome
GNUPGHOME=/tmp/test-gnupghome gpg --import ~/subkeys-only.asc
GNUPGHOME=/tmp/test-gnupghome gpg --list-secret-keys
# Primary key should show as 'sec#' (stub), subkeys as 'ssb'
# Delete primary from working keyring after confirming backup
gpg --delete-secret-key YOUR_KEY_ID
gpg --import ~/subkeys-only.asc
gpg --list-secret-keys
# Confirm sec# stub is present, not sec
Encrypting and Decrypting Files
Basic file encryption targets a recipient by their key ID or email. `--armor` produces ASCII output suitable for email or config management systems. Without `--armor`, GPG writes binary, which is smaller and fine for local use.
For symmetric encryption without a recipient key - useful for backup archives - use `--symmetric` with `--cipher-algo AES256`. Do not rely on the default cipher; specify it explicitly for auditability.
Decryption is straightforward: `gpg --decrypt`. GPG auto-detects the key and prompts for the passphrase via gpg-agent. If you are working with multiple secret keys and want to be explicit, use `--try-secret-key KEYID`.
# Asymmetric encryption to a recipient
gpg --encrypt --armor --recipient ops@example.com secret.txt
# Produces secret.txt.asc
# Encrypt to multiple recipients (both can decrypt)
gpg --encrypt --armor \
--recipient ops@example.com \
--recipient backup@example.com \
secret.txt
# Encrypt to yourself for local storage
gpg --encrypt --armor --recipient YOUR_KEY_ID secret.txt
# Symmetric encryption (passphrase only)
gpg --symmetric --cipher-algo AES256 --armor archive.tar.gz
# Decrypt any GPG-encrypted file
gpg --decrypt --output secret.txt secret.txt.asc
# Decrypt to stdout (pipe into other tools)
gpg --decrypt secret.txt.asc | sha256sum
Signing Files, Commits, and Packages
GPG signing proves that a specific key produced or approved a given artifact. Three signing modes matter in practice: detached signatures (sign a file without embedding), clearsign (embed plaintext with signature, useful for announcements), and binary signatures embedded in the file.
For git commit signing, configure git to use your signing subkey. GPG will automatically use the most recently added signing-capable subkey, but you can pin a specific subkey by appending an exclamation mark to the key ID in your git config.
Package maintainers using `dpkg-buildpackage` or `rpmbuild` hook into GPG automatically. The key insight: both tools call GPG with the key ID from your build config, and they expect gpg-agent to be running so no passphrase prompt blocks the build.
# Detached signature
gpg --detach-sign --armor release-v2.1.0.tar.gz
# Produces release-v2.1.0.tar.gz.asc
# Verify a detached signature
gpg --verify release-v2.1.0.tar.gz.asc release-v2.1.0.tar.gz
# Clearsign a text document
gpg --clearsign ANNOUNCEMENT.txt
# Git signing config
git config --global user.signingkey YOUR_SIGNING_SUBKEY_ID!
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Sign an existing commit
git commit --amend --no-edit -S
# Verify signed commits in a repo
git log --show-signature -5
Configuring gpg-agent for Persistent Passphrase Caching
gpg-agent is the daemon that holds decrypted private keys in memory and handles passphrase caching. Without it configured correctly, every GPG operation prompts for a passphrase, which breaks automation and annoys humans equally.
The agent starts automatically when you first invoke GPG 2.x. Its socket lives at `$GNUPGHOME/S.gpg-agent`, typically `~/.gnupg/S.gpg-agent`. The configuration file is `~/.gnupg/gpg-agent.conf`.
Key settings: `default-cache-ttl` controls how long a passphrase stays cached after last use (seconds). `max-cache-ttl` is the hard ceiling regardless of use. For developer workstations, 8 hours is reasonable. For servers running automated jobs, use a pinentry-loopback approach or a hardware token.
The `pinentry-program` directive tells the agent which program to use for passphrase prompts. On headless servers, use `pinentry-curses` or `pinentry-tty`. On GUI systems, `pinentry-gnome3` or `pinentry-qt` prevents popups from appearing on wrong displays.
# ~/.gnupg/gpg-agent.conf
default-cache-ttl 28800
max-cache-ttl 28800
pinentry-program /usr/bin/pinentry-curses
enable-ssh-support
# Reload the agent after config changes
gpgconf --kill gpg-agent
gpgconf --launch gpg-agent
# Check agent status
gpgconf --list-components | grep agent
# Pre-load a passphrase into the agent (for automation)
# Use gpg-preset-passphrase from gnupg-utils
KEYGRIP=$(gpg --with-keygrip --list-secret-keys YOUR_KEY_ID | grep Keygrip | head -1 | awk '{print $3}')
echo "your-passphrase" | /usr/lib/gnupg/gpg-preset-passphrase --preset $KEYGRIP
GPG in Automated Pipelines Without Storing Passphrases
The fundamental tension in automation is that GPG by default requires interactive passphrase entry. Three patterns solve this at different security levels.
Pattern 1: Hardware token (YubiKey). The signing or decryption key lives on the token. The token requires a PIN, not a passphrase. You can configure OpenSC and gpg-agent to use the token via PKCS#11 or the native OpenPGP application. This is the highest security option and works in CI if the runner has USB access.
Pattern 2: Passphrase in a secrets manager, injected at runtime. HashiCorp Vault, AWS Secrets Manager, or a comparable system holds the passphrase. Your pipeline script retrieves it at runtime, uses `gpg-preset-passphrase` to load it into the agent, performs the operation, then flushes the agent. The passphrase is never written to disk in the pipeline.
Pattern 3: Dedicated signing key with no passphrase, stored in a secrets manager as an armored export, with strict access controls on who can retrieve it. This is the pragmatic choice for fully automated CI pipelines where a hardware token is not feasible. The risk is acceptable if your secrets manager has audit logging and rotation capability.
If you are building automated signing into a DevOps workflow, tools like taskbotshub.ai can orchestrate the vault lookup, key import, signing step, and cleanup as a pipeline action without requiring custom shell scripting in every repo.
For the passphrase injection pattern, the critical detail is using `--batch` and `--pinentry-mode loopback` flags so GPG reads the passphrase from stdin rather than launching a pinentry dialog that will fail in a headless environment.
# Pattern 2: Passphrase injection at pipeline runtime
export GPG_TTY=$(tty)
export GNUPGHOME=$(mktemp -d)
chmod 700 $GNUPGHOME
# Import the private key from secrets manager
vault kv get -field=gpg_private_key secret/signing | gpg --batch --import
# Get passphrase and preset into agent
PASSPHRASE=$(vault kv get -field=gpg_passphrase secret/signing)
KEYGRIP=$(gpg --with-keygrip --list-secret-keys | grep Keygrip | awk '{print $3}' | head -1)
echo "$PASSPHRASE" | /usr/lib/gnupg/gpg-preset-passphrase --preset $KEYGRIP
# Sign the artifact
gpg --batch --pinentry-mode loopback \
--detach-sign --armor \
release.tar.gz
# Cleanup
gpgconf --kill gpg-agent
rm -rf $GNUPGHOME
Encrypting Secrets in Config Management and Ansible
Ansible Vault uses its own symmetric encryption, but there are cases where you need GPG: secrets shared with humans outside Ansible's control, artifacts signed for external consumption, or multi-recipient encryption where different team members need decryption access.
The `ansible-gpg-lookup` pattern: store encrypted files in your repository, use a lookup plugin or a `vars` file that decrypts via a shell command. This works but has friction. A cleaner approach is `git-crypt`, which uses GPG to manage per-file encryption in a git repository. Each team member's public key is added to the `.git-crypt/keys/default/` directory, and the tool handles transparent encrypt on commit, decrypt on checkout.
For Ansible specifically, community.crypto collection includes modules that can encrypt and decrypt GPG data within a playbook, which keeps the GPG operations inside Ansible's execution context rather than requiring pre-processed files.
The `pass` password manager (passwordstore.org) also uses GPG natively and has a git backend, making it a reasonable choice for team secret sharing when Vault is overhead for the use case.
# git-crypt setup for a repository
git-crypt init
git-crypt add-gpg-user ops@example.com
git-crypt add-gpg-user backup@example.com
# .gitattributes to specify which files get encrypted
echo 'secrets/** filter=git-crypt diff=git-crypt' >> .gitattributes
echo '.env filter=git-crypt diff=git-crypt' >> .gitattributes
git add .gitattributes
git commit -m "configure git-crypt"
# Unlock on a new clone
git clone git@repo:org/project.git
git-crypt unlock
# Ansible: decrypt a GPG file inline
- name: Load encrypted config
set_fact:
db_password: "{{ lookup('pipe', 'gpg --batch --decrypt secrets/db.gpg') }}"
Key Management Hygiene: Rotation, Revocation, and Expiry
Generate a revocation certificate immediately after key creation and store it with your offline backup. If you lose access to your private key or it is compromised, the revocation certificate is the only way to formally invalidate the key on keyservers and in trust networks.
Subkey expiry is your friend. Annual expiry on subkeys forces rotation without invalidating the primary key's trust. To extend a subkey that is approaching expiry, bring your primary key back online from cold storage, run `gpg --edit-key`, select the subkey with `key 1`, and run `expire`.
Keyservers in 2026: keys.openpgp.org is the recommended server because it requires email verification before publishing UIDs, which reduces spam and protects against fake key uploads. Ubuntu's keyserver (keyserver.ubuntu.com) is still widely used for package verification. Avoid the old SKS pool; it has been effectively deprecated.
To distribute your public key: publish to keys.openpgp.org, include your fingerprint in your email signature and on your team's wiki, and export it to any relevant internal keyservers. Never rely on keyservers alone - include the fingerprint out-of-band for first contact.
# Generate revocation certificate immediately
gpg --gen-revoke --armor YOUR_KEY_ID > revocation.asc
chmod 400 revocation.asc
# Store this offline alongside your primary key backup
# Publish public key to keys.openpgp.org
gpg --keyserver keys.openpgp.org --send-keys YOUR_KEY_ID
# Or export and upload via web interface
gpg --export --armor YOUR_KEY_ID | curl -T - https://keys.openpgp.org
# Extend expiry of a subkey
gpg --edit-key YOUR_KEY_ID
> key 1 # select first subkey
> expire
# enter new expiry: 1y
> key 1 # deselect
> key 2 # select second subkey
> expire
> save
# Revoke a compromised subkey
gpg --edit-key YOUR_KEY_ID
> key 1
> revkey
> save
gpg --keyserver keys.openpgp.org --send-keys YOUR_KEY_ID
Verifying Package and Release Signatures
Signature verification is the most common GPG operation on production servers. Kernel.org, Debian, Fedora, PostgreSQL, HashiCorp, and most serious software projects ship signed release tarballs or signed checksums.
The workflow is: import the project's public key, verify the signature against the artifact, check that the key fingerprint matches what the project documents out-of-band. The last step is the one most people skip, and it is the one that actually provides security.
For Debian packages, `apt` handles this automatically via `/etc/apt/trusted.gpg.d/`. For third-party repositories, you add the key with `gpg --dearmor` piped to the correct location. The old `apt-key add` command is deprecated since Debian 11.
For manual verification of a release tarball - PostgreSQL 16 as an example - the process is representative of what you encounter with most projects.
# Import a project signing key by fingerprint
gpg --keyserver keys.openpgp.org --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8
# Verify the fingerprint matches project documentation
gpg --fingerprint B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8
# Verify a detached signature
gpg --verify postgresql-16.3.tar.gz.sha256.asc postgresql-16.3.tar.gz.sha256
# Good signature from "PostgreSQL Debian Repository" - this is what you want
# Trust level depends on whether you've signed the key or set owner trust
# Add a third-party apt repo key correctly (Debian 11+)
curl -fsSL https://example.com/signing.asc | \
gpg --dearmor | \
sudo tee /etc/apt/trusted.gpg.d/example.gpg > /dev/null
# Verify your apt keyring
gpg --no-default-keyring \
--keyring /etc/apt/trusted.gpg.d/example.gpg \
--list-keys