Essay 10 / Digital

[OpenPGP - offline primary] Keep the Secret Off the Machine

Keep the OpenPGP primary private key offline, use subkeys for daily operations, and test recovery before removing any key.

A disconnected low-voltage circuit board, multimeter probes and empty battery holder arranged for diagnosis.
A smaller fault is an easier question. An original illustrative scene.

Keep the OpenPGP primary private key off your daily computer. Use separate subkeys for signing, decryption, and authentication. This limits what an attacker can do after compromising the computer: the primary key is required to certify new subkeys and identity information.

This procedure uses GnuPG on Linux, an encrypted removable volume, and temporary keyrings under /dev/shm. It assumes that you can identify the correct key and storage device. Practise with a disposable key before changing an existing identity.

The examples retain the RSA-8192 primary and RSA-4096 subkeys from the original walkthrough. RSA-8192 is an example configuration, not a requirement for keeping a primary key offline. Replace the example fingerprint with your full fingerprint. Enter passphrases through pinentry; keep them out of scripts and shell history.

Basics · Primary key and subkey roles

Publish the public key so others can verify signatures or encrypt messages to you. Protect the private keys that authorize your operations. Separate their capabilities:

Capability Purpose Key used here
Certify [C] Certify user IDs and subkeys; certify other public keys Primary
Sign [S] Sign messages, files, or Git commits Signing subkey
Encrypt [E] Receive encrypted data; decrypt it with the private subkey Encryption subkey
Authenticate [A] Authenticate to services, such as SSH through gpg-agent Authentication subkey

Separate certification from daily use

  • Give the primary key the certification capability only.
  • Create separate signing, encryption, and authentication subkeys.
  • Keep a tested backup of all private keys on encrypted removable media.
  • Remove the primary private key from the daily keyring after testing recovery.
  • Optionally move the operational subkeys to an OpenPGP smartcard.

Use full fingerprints to identify keys. A keygrip is a separate identifier used by gpg-agent for a private-key component. Inspect both before any deletion:

gpg --list-secret-keys --fingerprint --with-keygrip
Expert · Create the primary key and three subkeys

Save this template as primary_gpg_key_constructor.tpl, replacing the name and email address. Expire-Date: 0 makes this example primary non-expiring; choose an expiry if your key policy requires one. GnuPG requests the passphrase through pinentry.

%echo Generating a RSA key 8192
Key-Type: RSA
Key-Length: 8192
Key-Usage: cert
Name-Real: My Name
Name-Email: something@something
Expire-Date: 0
%commit
%echo done
gpg --batch --generate-key --enable-large-rsa primary_gpg_key_constructor.tpl
gpg --list-secret-keys

The example uses --enable-large-rsa for the 8192-bit key. Confirm that the primary shows [C] only. The output below is illustrative:

sec   rsa8192 2023-01-14 [C]
      E6B0933DD47E3B2F823334F0A567B3B17C27BF6D
uid           [ultimate] My Name <something@something>
gpg --expert --edit-key E6B0933DD47E3B2F823334F0A567B3B17C27BF6D

At the gpg> prompt, add the subkeys:

  • Signing: addkey, RSA (sign only), 4096 bits, then an expiry.
  • Encryption: addkey, RSA (encrypt only), 4096 bits, then an expiry.
  • Authentication: addkey, RSA (set your own capabilities). Disable S and E, enable A, then choose the size and expiry.

Menu numbers can vary by version. Select by capability and verify the result. Use save to write the changes, then list all fingerprints:

gpg --fingerprint --fingerprint E6B0933DD47E3B2F823334F0A567B3B17C27BF6D
pub   rsa8192 2023-01-14 [C]
      E6B0 933D D47E 3B2F 8233  34F0 A567 B3B1 7C27 BF6D
uid           [ultimate] My Name <something@something>
sub   rsa4096 2023-01-14 [S] [expires: 2030-01-12]
      61DF 146E F7F6 0971 5E60  46B8 8C75 87C8 6EAC 2F2F
sub   rsa4096 2023-01-14 [E] [expires: 2030-01-12]
      DBBC AE3F 8166 9FF4 75B7  5F35 51D8 8833 7727 23AD
sub   rsa4096 2023-01-14 [A] [expires: 2030-01-12]
      F4CF F234 E969 2304 CC58  D55B D13A E6B6 5E8B E723

When replacing an existing primary, you can certify the new key with the old key before retiring it. Explicitly select the old signing key; do not rely on GnuPG’s default selection:

gpg --local-user "$OLD_FINGERPRINT" --sign-key "$NEW_FINGERPRINT"
Intermediate · Choose a setup you can maintain

If you do not need OpenPGP certification, consider age for file encryption and dedicated SSH keys for login. If you keep an OpenPGP primary on your daily computer, protect it with a passphrase, preserve a revocation certificate, and test an encrypted backup. That setup exposes the primary to the daily computer; document that limitation.

Back up and test before removing the primary

Export the private keys before removing the primary or moving subkeys to a card. Later exports may contain references to unavailable keys rather than recoverable private material. A successful export command is not proof of recovery.

The RAM keyrings below reduce persistent writes. /dev/shm may use swap, and removing its files does not guarantee secure erasure. Use a trusted environment with disabled or encrypted swap and no hibernation. A RAM keyring on a connected or compromised computer is not an offline environment.

Expert · Export to encrypted media and test restoration

Run these examples in Bash. Confirm the active GnuPG home first:

gpgconf --list-dirs homedir

Set the destination and fingerprint. Confirm that the expected encrypted device is mounted. mountpoint checks that a mount exists; it does not verify encryption or device identity.

set -euo pipefail
umask 077
ENCRYPTED_MOUNT="/run/media/user/Encrypted_Device"
EXPORT_DIR="$ENCRYPTED_MOUNT/secrets/gpg"
EXPORT_FILENAME="MySelf_Master_MyComment_private_rsa8192"
EXPORT_FINGERPRINT="E6B0933DD47E3B2F823334F0A567B3B17C27BF6D"
if ! mountpoint -q -- "$ENCRYPTED_MOUNT"; then
    echo "Encrypted device is not mounted at $ENCRYPTED_MOUNT" >&2
    exit 1
fi
install -d -m 0700 -- "$EXPORT_DIR"

Use new filenames for each backup version. Keep the last tested backup until the replacement passes restoration checks. Export the full private key, operational subkeys, public key, and ownertrust:

gpg --armor --output "$EXPORT_DIR/$EXPORT_FILENAME.private-key.sec" \
    --export-secret-keys "$EXPORT_FINGERPRINT"
gpg --armor --output "$EXPORT_DIR/$EXPORT_FILENAME.subkeys.sec" \
    --export-secret-subkeys "$EXPORT_FINGERPRINT"
gpg --armor --output "$EXPORT_DIR/$EXPORT_FILENAME.pubkey.asc" \
    --export "$EXPORT_FINGERPRINT"
gpg --list-secret-keys --fingerprint --with-keygrip "$EXPORT_FINGERPRINT" \
    > "$EXPORT_DIR/$EXPORT_FILENAME.keygrip.txt"
gpg --export-ownertrust > "$EXPORT_DIR/gnupg-ownertrust.txt"

Generate revocation certificates while the primary private key is available. Store them securely: anyone who obtains one can publish the revocation. These examples cover the four revocation reasons:

printf '%s\n' y 0 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.no-reason.asc" --gen-revoke "$EXPORT_FINGERPRINT"
printf '%s\n' y 1 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.compromised.asc" --gen-revoke "$EXPORT_FINGERPRINT"
printf '%s\n' y 2 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.superseded.asc" --gen-revoke "$EXPORT_FINGERPRINT"
printf '%s\n' y 3 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.no-longer-used.asc" --gen-revoke "$EXPORT_FINGERPRINT"
chmod 0600 -- "$EXPORT_DIR"/*
  • private-key.sec: primary and subkey private material for full recovery.
  • subkeys.sec: operational private subkeys; primary private material excluded.
  • pubkey.asc: public certificate for distribution.
  • keygrip.txt: identifiers for checking key components.
  • gnupg-ownertrust.txt: local trust assignments; inspect before importing into another keyring.
  • revoke.*.asc: certificates for revoking the primary.

Import the full private backup into a separate temporary keyring:

umask 077
TEST_GNUPGHOME="$(mktemp -d /dev/shm/gnupg-test.XXXXXX)"
chmod 0700 "$TEST_GNUPGHOME"
gpg --homedir "$TEST_GNUPGHOME" \
    --import "$EXPORT_DIR/$EXPORT_FILENAME.private-key.sec"
gpg --homedir "$TEST_GNUPGHOME" \
    --list-secret-keys --fingerprint --with-keygrip "$EXPORT_FINGERPRINT"

Check the full fingerprint, a usable sec, and all three ssb entries. Test signing, decryption of a previously encrypted test file, and authentication where configured. Also test that the restored primary can certify a disposable test key. Key listings alone do not prove that the private material works.

Stop the temporary agents and remove only the test directory created above:

case "$TEST_GNUPGHOME" in
    /dev/shm/gnupg-test.*)
        gpgconf --homedir "$TEST_GNUPGHOME" --kill all
        rm -rf -- "$TEST_GNUPGHOME"
        ;;
    *) echo "Unexpected test keyring path" >&2; exit 1 ;;
esac

After recovery succeeds, remove only the primary private key from the daily keyring. The quoted ! selects that component. Omitting it also deletes the secret subkeys. Confirm the active home and fingerprint before proceeding. GnuPG command reference.

gpg --delete-secret-keys "${EXPORT_FINGERPRINT}!"
gpg --list-secret-keys "$EXPORT_FINGERPRINT"
sec#  rsa8192 2023-01-14 [C]
      E6B0933DD47E3B2F823334F0A567B3B17C27BF6D
uid           [ultimate] My Name <something@something>
ssb   rsa4096 2023-01-14 [S] [expires: 2030-01-12]
ssb   rsa4096 2023-01-14 [E] [expires: 2030-01-12]
ssb   rsa4096 2023-01-14 [A] [expires: 2030-01-12]

sec# means the primary private key is unavailable in this keyring. It does not prove that no copy remains in another keyring, backup, filesystem snapshot, or recoverable disk data. For new identities, generate the primary in the offline environment and import only operational subkeys onto the daily computer.

Use the primary in a separate environment

Use a trusted offline environment for certification, identity changes, and subkey management. Connect the encrypted backup only for the required operation. Store it separately from the daily computer and keep another tested copy in a separate location.

sec    actual primary secret available
sec#   primary secret unavailable in this keyring
ssb    secret subkey locally available
ssb#   secret subkey unavailable
ssb>   secret subkey on a smartcard
Expert · Restore, make changes, and test a new backup

Start from the normal GnuPG home, with no GNUPGHOME override. In the trusted offline environment, verify the encrypted mount and import the backup into a temporary home:

umask 077
ENCRYPTED_MOUNT="/run/media/user/Encrypted_Device"
EXPORT_DIR="$ENCRYPTED_MOUNT/secrets/gpg"
EXPORT_FILENAME="MySelf_Master_MyComment_private_rsa8192"
EXPORT_FINGERPRINT="E6B0933DD47E3B2F823334F0A567B3B17C27BF6D"
mountpoint -q -- "$ENCRYPTED_MOUNT" || exit 1
NORMAL_GNUPGHOME="$(gpgconf --list-dirs homedir)"
GNUPGHOME="$(mktemp -d /dev/shm/gnupg.XXXXXX)"
export GNUPGHOME
chmod 0700 "$GNUPGHOME"
gpg --import "$EXPORT_DIR/$EXPORT_FILENAME.private-key.sec"
gpg --import-ownertrust "$EXPORT_DIR/gnupg-ownertrust.txt"
gpg --list-secret-keys --fingerprint --with-keygrip "$EXPORT_FINGERPRINT"

Verify the primary fingerprint and usable private components before editing:

gpg --edit-key "$EXPORT_FINGERPRINT"

Changes to this keyring do not update the backup files. Export a new candidate after changing a passphrase, expiry, user ID, certification, or subkey:

NEW_BACKUP="$EXPORT_DIR/$EXPORT_FILENAME.private-key.NEW.sec"
gpg --armor --output "$NEW_BACKUP" --export-secret-keys "$EXPORT_FINGERPRINT"
chmod 0600 "$NEW_BACKUP"

Restore the candidate into a second temporary keyring and repeat the checks. Refresh and test the subkey and public exports too. Only then archive the previous version and replace the backup set. Publish the updated public certificate where others retrieve it.

case "$GNUPGHOME" in
    /dev/shm/gnupg.*)
        gpgconf --homedir "$GNUPGHOME" --kill all
        rm -rf -- "$GNUPGHOME"
        ;;
    *) echo "Unexpected working keyring path" >&2; exit 1 ;;
esac
unset GNUPGHOME
gpgconf --list-dirs homedir

Confirm that the final path matches NORMAL_GNUPGHOME. On the daily computer, import only the updated public certificate and operational subkeys:

gpg --import "$EXPORT_DIR/$EXPORT_FILENAME.pubkey.asc"
gpg --import "$EXPORT_DIR/$EXPORT_FILENAME.subkeys.sec"
gpg --import-ownertrust "$EXPORT_DIR/gnupg-ownertrust.txt"

Check sec# on the daily computer and test its operational subkeys. Importing private-key.sec there makes the primary private key available again.

Revoke the affected key

Expert · Select the subkey before revoking it

If a subkey is compromised, load the primary in the offline environment and edit the certificate:

gpg --expert --edit-key E6B0933DD47E3B2F823334F0A567B3B17C27BF6D

Use key N to select the affected subkey. Verify the selection marker and fingerprint, run revkey, then save. Export and distribute the updated public certificate. Removing a local key with delkey does not inform others that it is revoked.

For a compromised or retired primary, use the appropriate primary revocation certificate and distribute the revoked public certificate. Keep old encryption private keys if you still need to decrypt historical data, subject to your retention and compromise policy.

Verify the result

The setup is complete when you have a tested full backup, accessible revocation certificates, working operational subkeys, and a daily keyring that cannot use the primary private key. Record the backup locations and restoration procedure. Repeat the recovery checks after changing the keys.

Next essay →Back to the beginning ↑