gopass v3 + YubiKey GPG Runbook
Overview
This runbook covers setting up gopass v3 with GPG keys stored on YubiKey hardware. Every secret decryption requires physical YubiKey touch.
Security model:
┌─────────────────────────────────────────────┐
│ gopass show secret │
│ ↓ │
│ GPG decrypt request │
│ ↓ │
│ YubiKey touch required ← physical presence │
│ ↓ │
│ Secret revealed │
└─────────────────────────────────────────────┘
Laptop stolen? No YubiKey = no secrets.
Prerequisites
Hardware
| Key | Model | Purpose |
|---|---|---|
Primary |
YubiKey 5C NFC (or 5 series) |
Daily driver - on keychain |
Backup |
YubiKey 5C NFC (or 5 series) |
Identical config - home safe |
Software
Required Packages
| Package | Purpose | Min Version |
|---|---|---|
gnupg |
GNU Privacy Guard - handles all encryption/decryption. GPG keys live on YubiKey, gpg-agent talks to the hardware. |
2.4+ |
yubikey-manager |
CLI tool ( |
5.0+ |
gopass |
Password manager that uses GPG for encryption. Stores secrets as GPG-encrypted files in a git repo. |
1.15+ |
git |
Version control for the gopass store. Enables sync across machines via multiple remotes. |
2.40+ |
pcscd |
Smart card daemon - required for YubiKey communication. Runs as systemd service. |
1.9+ |
Check Existing Installation
# command -v: Returns path if command exists, empty if not
# &>/dev/null: Redirect BOTH stdout (&) and stderr (>) to /dev/null (discard)
# - Equivalent to: >/dev/null 2>&1
# - Suppresses all output, we only care about exit code
# $?: Exit code of last command (0 = success, non-zero = failure)
for cmd in gpg ykman gopass git pcscd; do
if command -v "$cmd" &>/dev/null; then
# command found - print path
# $(command -v $cmd): Command substitution - runs command, captures output
echo "✓ $cmd: $(command -v $cmd)"
else
echo "✗ $cmd: NOT INSTALLED"
fi
done
Install (Arch Linux)
# pacman -S: Sync/install packages
# ccid: USB smart card driver (required for YubiKey CCID interface)
sudo pacman -S gnupg yubikey-manager gopass git ccid
# systemctl enable: Start service at boot
# --now: Also start it immediately (don't wait for reboot)
# pcscd.service: PC/SC Smart Card Daemon
sudo systemctl enable --now pcscd.service
Verify Versions (with Exit Code Checks)
# Pattern breakdown:
# gpg --version : Run command, outputs to stdout
# 2>&1 : Redirect stderr (2) to stdout (1) - capture errors too
# | head -1 : Pipe (|) stdout to head, show first line only
# && echo "..." : If command succeeds (exit 0), run the echo
gpg --version 2>&1 | head -1 && echo "Exit code: $?"
# awk '{print $NF}': Print last field ($NF = Number of Fields = last column)
# Useful when version is always at the end but column position varies
ykman --version 2>&1 | awk '{print "ykman version:", $NF}'
# awk '{print $2}': Print second field (space-delimited)
# gopass outputs: "gopass 1.15.14 go1.23.0 linux amd64"
# $1 $2 $3 $4 $5
gopass --version 2>&1 | awk '{print "gopass version:", $2}'
# git outputs: "git version 2.47.2"
# $1 $2 $3
git --version 2>&1 | awk '{print "git version:", $3}'
Comprehensive Version Check Loop
# Advanced loop with conditional formatting
# printf: Formatted print (like C's printf)
# "%-8s": Left-align (-), 8 characters wide, string (s)
# "%d": Decimal integer (for exit code)
# version=$(...): Capture command output into variable
for tool in gpg ykman gopass git; do
# Capture version output; if command succeeds, print success
if version=$($tool --version 2>&1 | head -1); then
printf "✓ %-8s %s\n" "$tool:" "$version"
else
# $? contains exit code from the failed command
printf "✗ %-8s FAILED (exit code: %d)\n" "$tool:" $?
fi
done
✓ gpg: gpg (GnuPG) 2.4.7 ✓ ykman: YubiKey Manager (ykman) version: 5.5.1 ✓ gopass: gopass 1.15.14 go1.23.0 linux amd64 ✓ git: git version 2.47.2
Verify pcscd (Socket Activation)
# Socket activation: systemd starts service on-demand when socket is accessed
# The socket listens, service only runs when something connects
# This is why pcscd.service shows "inactive" but YubiKey still works
systemctl is-active pcscd.socket && echo "Socket ready - service starts on demand"
# Advanced: Check both socket and service, format with awk
# paste - -: Merge two lines into one (socket status + service status)
# Ternary in awk: (condition ? "if_true" : "if_false")
systemctl is-active pcscd.socket pcscd.service 2>&1 | paste - - | awk '{
print "pcscd.socket:", $1, ($1=="active" ? "✓" : "✗")
print "pcscd.service:", $2, "(starts on demand, inactive is OK)"
}'
Verify YubiKey Detection
# ykman list: Show all connected YubiKeys
# awk 'NF': Only print lines with content (NF = Number of Fields > 0)
# NF is truthy when line has fields, falsy for empty lines
ykman list 2>&1 | awk 'NF {print "YubiKey found:", $0}'
# Extract serial number using field separator
# -F'Serial: ': Split line on "Serial: " (the text before the number)
# /Serial/: Only process lines containing "Serial"
# $2: Second field (everything after "Serial: ")
ykman list 2>&1 | awk -F'Serial: ' '/Serial/ {print "Serial:", $2}'
# GPG card status - shows OpenPGP applet info
# head -5: First 5 lines contain Reader, Application ID, Version
gpg --card-status 2>&1 | head -5
# Multi-pattern awk: Match multiple patterns, extract specific fields
# /Reader/: Match lines containing "Reader"
# /Serial/: Match lines containing "Serial"
# /PIN retry/: Match PIN retry counter line
gpg --card-status 2>&1 | awk '
/Reader/ {print "Reader:", $NF}
/Serial/ {print "Serial:", $NF}
/PIN retry/ {print "PIN retries:", $4, $5, $6}
'
Full Diagnostic Script
#!/bin/bash
# yubikey-check - Full YubiKey GPG diagnostic
# Save as: ~/bin/yubikey-check && chmod +x ~/bin/yubikey-check
echo "=== YubiKey GPG Diagnostic ==="
echo ""
echo "--- Tools ---"
# ver=$(...) && printf ... || printf ...: Ternary pattern
# If command succeeds, print version; else print "missing"
for cmd in gpg ykman gopass git; do
ver=$($cmd --version 2>&1 | head -1) && \
printf "✓ %s\n" "$ver" || \
printf "✗ %s missing\n" "$cmd"
done
echo ""
echo "--- pcscd ---"
# $(...): Command substitution - output becomes the string
printf "Socket: %s\n" "$(systemctl is-active pcscd.socket)"
echo ""
echo "--- YubiKey ---"
# ||: If left side fails (non-zero exit), run right side
ykman list 2>&1 || echo "No YubiKey detected"
echo ""
echo "--- GPG Card ---"
# Pipe to awk, filter specific lines, handle failure
gpg --card-status 2>&1 | awk '/Reader|Serial|PIN retry/ {print}' || \
echo "Card not accessible"
Save this diagnostic as ~/bin/yubikey-check for quick verification anytime.
|
Understanding Redirection
| Pattern | Meaning |
|---|---|
|
Redirect stdout to file (overwrite) |
|
Redirect stdout to file (append) |
|
Redirect stderr to file |
|
Redirect stderr (fd 2) to wherever stdout (fd 1) is going |
|
Redirect BOTH stdout and stderr (shorthand for |
|
Discard all output (silent execution) |
|
Pipe stdout to head, show first line only |
|
OR: If cmd fails (non-zero exit), run the echo |
|
AND: If cmd succeeds (exit 0), run the echo |
|
Exit code of last command (0=success, non-zero=error) |
|
Command substitution: Run cmd, capture its stdout as string |
Phase 1: GPG Master Key Generation
| Generate the master key on a secure machine. Air-gapped preferred, or at minimum a trusted workstation with no network during generation. |
1.1 Configure GPG for Strong Defaults
# Create GPG home directory with restrictive permissions
# 700 = rwx------ (only owner can read/write/execute)
mkdir -p ~/.gnupg
chmod 700 ~/.gnupg
# Heredoc: << 'EOF' ... EOF
# Single quotes around EOF: No variable expansion (literal text)
# Without quotes: Variables like $HOME would be expanded
cat > ~/.gnupg/gpg.conf << 'EOF'
# Cipher preferences: AES-256 first (strongest)
personal-cipher-preferences AES256 AES192 AES
# Digest (hash) preferences: SHA-512 first (strongest)
personal-digest-preferences SHA512 SHA384 SHA256
# Compression preferences
personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed
# Default preferences for new keys
default-preference-list SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed
# Use SHA-512 for certifying other keys
cert-digest-algo SHA512
# String-to-key (passphrase hashing) settings
s2k-digest-algo SHA512
s2k-cipher-algo AES256
# Display settings
charset utf-8
no-comments
no-emit-version
no-greeting
keyid-format 0xlong
list-options show-uid-validity
verify-options show-uid-validity
with-fingerprint
# Security settings
require-cross-certification
no-symkey-cache
use-agent
# Privacy: Hide recipient key IDs in encrypted messages
throw-keyids
EOF
# Verify config was written
# wc -l: Count lines
# cat -n: Show with line numbers (for verification)
wc -l ~/.gnupg/gpg.conf && echo "Config written successfully"
1.2 Generate Master Key (Certify Only)
# --expert: Enable expert mode (needed for capability toggling)
# --full-generate-key: Interactive key generation with all options
gpg --expert --full-generate-key
Interactive prompts - select these options:
Please select what kind of key you want: (8) RSA (set your own capabilities) <-- SELECT THIS Current allowed actions: Sign Certify Encrypt (S) Toggle the sign capability (E) Toggle the encrypt capability (A) Toggle the authenticate capability (Q) Finished # Toggle OFF: S, E, A (press each letter) # Only "Certify" should remain # Press Q when done What keysize do you want? (3072) 4096 <-- ENTER 4096 Key is valid for? (0) 0 <-- 0 = never expires (or 2y) Real name: Your Full Name Email address: your.email@example.com Comment: <-- Leave blank
# After generation, capture the key ID
# grep -A1 "sec": Show line containing "sec" plus 1 line After
# tail -1: Take the last line (the fingerprint/ID line)
# awk '{print $1}': Extract first field (the key ID)
export KEYID=$(gpg --list-keys --keyid-format long 2>&1 | \
grep -A1 "^sec" | tail -1 | awk '{print $1}')
echo "Master Key ID: $KEYID"
# Alternative: More robust extraction using fingerprint
# sed -n: Silent mode (only print when told)
# '/fpr/p': Print lines containing "fpr" (fingerprint)
# tail -c 17: Last 16 chars + newline (long key ID)
KEYID=$(gpg --list-keys --with-colons 2>&1 | \
awk -F: '/^pub/ {getline; print $10}' | head -1)
echo "Master Key ID: $KEYID"
1.3 Add Subkeys
# Edit the key to add subkeys
# Subkeys do the actual work; master key only certifies
gpg --expert --edit-key $KEYID
Add three subkeys (one at a time):
gpg> addkey (8) RSA (set your own capabilities) # Toggle to ONLY "Sign" (S on, E off, A off) Keysize: 4096 Valid for: 1y gpg> addkey (8) RSA (set your own capabilities) # Toggle to ONLY "Encrypt" (S off, E on, A off) Keysize: 4096 Valid for: 1y gpg> addkey (8) RSA (set your own capabilities) # Toggle to ONLY "Authenticate" (S off, E off, A on) Keysize: 4096 Valid for: 1y gpg> save
1.4 Verify Key Structure
# List keys with long format IDs
# sec = Secret key (master)
# ssb = Secret subkey
# [C] = Certify, [S] = Sign, [E] = Encrypt, [A] = Authenticate
gpg --list-keys --keyid-format long $KEYID
# Detailed view with awk formatting
gpg --list-keys --with-colons $KEYID 2>&1 | awk -F: '
/^pub/ {print "Master (Certify):", $5}
/^sub/ {
# $12 contains capabilities (s, e, a)
cap = $12
gsub(/s/, "Sign", cap)
gsub(/e/, "Encrypt", cap)
gsub(/a/, "Auth", cap)
print "Subkey:", $5, "(" cap ")"
}
'
Expected output:
sec rsa4096/0x1234567890ABCDEF 2026-02-17 [C] uid [ultimate] Your Name <your.email@example.com> ssb rsa4096/0xAAAAAAAAAAAAAAAA 2026-02-17 [S] [expires: 2027-02-17] ssb rsa4096/0xBBBBBBBBBBBBBBBB 2026-02-17 [E] [expires: 2027-02-17] ssb rsa4096/0xCCCCCCCCCCCCCCCC 2026-02-17 [A] [expires: 2027-02-17]
Phase 2: Backup Master Key
CRITICAL: Do this BEFORE moving keys to YubiKey. Moving is one-way - keys are deleted from disk.
2.1 Export Master Key
# Create secure backup directory
# 700 permissions: Only owner can access
mkdir -p ~/gpg-backup && chmod 700 ~/gpg-backup
# --armor: ASCII armor output (text, not binary)
# --export-secret-keys: Export ALL secret keys (master + subkeys)
gpg --armor --export-secret-keys $KEYID > ~/gpg-backup/master-secret.asc
# Export public key (safe to share)
gpg --armor --export $KEYID > ~/gpg-backup/public.asc
# Export ONLY subkeys (useful for recovery without exposing master)
gpg --armor --export-secret-subkeys $KEYID > ~/gpg-backup/subkeys-secret.asc
# Generate revocation certificate (use if key is compromised)
# This is CRITICAL - store securely
gpg --gen-revoke $KEYID > ~/gpg-backup/revoke.asc
# Verify all files were created
ls -la ~/gpg-backup/ | awk 'NF > 2 {print $NF, $5, "bytes"}'
2.2 Secure the Backup
# Create encrypted tarball
# tar -czf -: Create gzipped tar, output to stdout (-)
# | gpg --symmetric: Pipe to GPG for symmetric encryption (passphrase)
# --cipher-algo AES256: Use AES-256 encryption
tar -czf - ~/gpg-backup | gpg --symmetric --cipher-algo AES256 \
> ~/gpg-master-backup-$(date +%Y%m%d).tar.gz.gpg
# Verify backup is valid (list contents without extracting)
# gpg --decrypt: Decrypt the file
# | tar -tzf -: List (-t) gzipped (-z) tar from stdin (-f -)
gpg --decrypt ~/gpg-master-backup-*.tar.gz.gpg 2>/dev/null | tar -tzf - | head -10
Copy to multiple locations:
-
Encrypted USB drive (LUKS or VeraCrypt)
-
Offline storage (safe deposit box)
-
Another encrypted volume
Phase 3: Move Subkeys to YubiKey
3.1 Configure YubiKey for GPG
# Verify YubiKey is detected by GPG
# Should show Reader, Application ID, card capabilities
gpg --card-status 2>&1 | head -10
# Enter card edit mode
gpg --card-edit
In the GPG card shell:
gpg/card> admin # Enable admin commands Admin commands are allowed gpg/card> passwd # Change PINs
Set PINs (CHANGE FROM DEFAULTS):
# PIN menu: 1 - change PIN # Default: 123456 → Change to 6+ digits 3 - change Admin PIN # Default: 12345678 → Change to 8+ digits 4 - set Reset Code # Optional: Recovery if PIN locked gpg/card> quit
# Verify PIN retries (should be 3/3/3)
gpg --card-status 2>&1 | awk '/PIN retry/ {print "PIN retries:", $4, $5, $6}'
3.2 Move Subkeys to YubiKey
| This operation is ONE-WAY. Keys are MOVED (deleted from disk), not copied. Ensure backup is complete! |
gpg --edit-key $KEYID
Move each subkey to card:
# Select first subkey (Sign) gpg> key 1 # Asterisk appears next to key 1 gpg> keytocard Please select where to store the key: (1) Signature key # <-- Select 1 Your selection? 1 # Deselect key 1, select key 2 (Encrypt) gpg> key 1 # Deselect gpg> key 2 # Select key 2 gpg> keytocard (2) Encryption key # <-- Select 2 Your selection? 2 # Deselect key 2, select key 3 (Authenticate) gpg> key 2 # Deselect gpg> key 3 # Select key 3 gpg> keytocard (3) Authentication key # <-- Select 3 Your selection? 3 gpg> save # IMPORTANT: Save changes
3.3 Verify Keys on YubiKey
# Card status should now show your subkeys
gpg --card-status 2>&1 | awk '
/Signature key/ {print "Sign key:", $NF}
/Encryption key/ {print "Encrypt key:", $NF}
/Authentication key/ {print "Auth key:", $NF}
'
# Full verification - keys should show "card-no:" indicating on hardware
gpg --list-secret-keys --keyid-format long 2>&1 | grep -E "(sec|ssb|card-no)"
3.4 Configure Touch Requirement
# Require physical touch for ALL cryptographic operations
# This is the key security feature - no touch = no decrypt
ykman openpgp keys set-touch sig on # Signing requires touch
ykman openpgp keys set-touch enc on # Encryption/decryption requires touch
ykman openpgp keys set-touch aut on # Authentication requires touch
# Verify touch policy
ykman openpgp info 2>&1 | awk '/Touch policy/ {found=1} found {print}'
Phase 4: Setup Backup YubiKey
Repeat Phase 3 with your backup YubiKey, using the backup you created in Phase 2.
4.1 Restore Keys from Backup
# On the same or different machine
# Import the full secret key (master + original subkeys)
gpg --import ~/gpg-backup/master-secret.asc
# Import public key (needed for trust)
gpg --import ~/gpg-backup/public.asc
# Verify import
gpg --list-secret-keys --keyid-format long 2>&1 | head -10
Phase 5: gopass v3 Initialization
5.1 Initialize gopass with GPG Key
# Initialize v3 store encrypted with your GPG key
# --store v3: Create store named "v3"
# $KEYID: Your GPG key ID from earlier
gopass init --store v3 $KEYID
# Verify initialization
gopass --version && gopass ls v3
5.2 Create Domain-Aligned Structure
# Create metadata entry for network credentials
# Heredoc with gopass insert:
# -f: Force (overwrite if exists)
# -m: Multiline mode
cat << 'EOF' | gopass insert -f -m v3/domains/d000/network/.meta
---
description: Network infrastructure credentials
scope: d000 (Personal Infrastructure)
categories:
- devices: Switch, router, firewall logins
- radius: RADIUS shared secrets
- snmp: Community strings
- routing: OSPF, EIGRP, BGP keys
- tacacs: TACACS+ secrets
created: 2026-02-17
EOF
# Verify structure was created (requires YubiKey touch)
gopass ls v3
5.3 Configure Multi-Remote Sync
# gopass stores are git repos - add multiple remotes for redundancy
# git -C PATH: Run git command in specified directory
STORE_PATH="$HOME/.local/share/gopass/stores/v3"
# Add remotes (replace USER with your username)
git -C "$STORE_PATH" remote add origin git@github.com:USER/gopass-v3.git
git -C "$STORE_PATH" remote add gitlab git@gitlab.com:USER/gopass-v3.git
git -C "$STORE_PATH" remote add gitea git@gitea.inside.domusdigitalis.dev:user/gopass-v3.git
# Verify remotes
git -C "$STORE_PATH" remote -v | awk '{print $1, $2}'
# Push to all remotes
# -u origin main: Set origin/main as upstream for future pushes
git -C "$STORE_PATH" push -u origin main
git -C "$STORE_PATH" push gitlab main
git -C "$STORE_PATH" push gitea main
# Create sync-all script for daily use
cat > ~/bin/gopass-sync-all << 'EOF'
#!/bin/bash
STORE_PATH="$HOME/.local/share/gopass/stores/v3"
echo "Syncing gopass v3 to all remotes..."
for remote in origin gitlab gitea; do
git -C "$STORE_PATH" push $remote main 2>&1 | \
awk -v r="$remote" '{print r": "$0}'
done
EOF
chmod +x ~/bin/gopass-sync-all
5.4 Test YubiKey Integration
# Generate test secret (24 characters)
# YubiKey should blink during encryption
gopass generate v3/test/yubikey-test 24
# Retrieve secret (requires touch to decrypt)
# YubiKey blinks - touch to authorize
gopass show v3/test/yubikey-test
# Clean up test secret
gopass rm -f v3/test/yubikey-test
gopass v3 Architecture
Full Structure
v3/
├── domains/
│ ├── d000/ # Personal Infrastructure
│ │ ├── hardware/ # IPMI, printers, IoT
│ │ ├── identity/ # AD, IPA, Keycloak, LDAP
│ │ ├── network/
│ │ │ ├── devices/ # Switch/router/firewall logins
│ │ │ ├── radius/ # RADIUS shared secrets
│ │ │ ├── snmp/ # Community strings
│ │ │ ├── routing/ # OSPF/EIGRP/BGP keys
│ │ │ └── tacacs/ # TACACS+ secrets
│ │ ├── servers/
│ │ ├── storage/ # NAS, S3, backup
│ │ └── wifi/ # PSKs
│ └── d001/ # Client domain
│ ├── identity/
│ └── network/
├── keys/
│ ├── ssh/
│ │ ├── personal/ # github, gitlab, gitea
│ │ └── service/ # deploy keys, automation
│ ├── age/ # Age encryption keys
│ ├── gpg/ # GPG passphrases
│ └── encryption/ # borg, veracrypt, LUKS
├── certificates/ # Cert passphrases, PFX
├── licenses/ # Software licenses
└── personal/ # Consumer/personal
├── finance/
├── shopping/
├── social/
├── streaming/
├── gaming/
├── email/
├── documents/
├── recovery/ # 2FA backup codes
├── travel/
├── health/
└── government/
Daily Operations
Retrieve Secret
# Show secret with metadata
gopass show v3/domains/d000/network/devices/switch-01
# Password only - first line (-o = output only password)
# Useful for piping to other commands
gopass show -o v3/domains/d000/network/devices/switch-01
# Copy to clipboard (clears after 45 seconds)
gopass -c v3/domains/d000/network/devices/switch-01
# Use in scripts with command substitution
PASSWORD=$(gopass show -o v3/domains/d000/network/devices/switch-01)
echo "Password length: ${#PASSWORD}"
unset PASSWORD # Clear from memory
Add New Secret
# Generate random password (24 chars)
gopass generate v3/domains/d000/servers/newserver 24
# Insert with YAML metadata using heredoc
cat << 'EOF' | gopass insert -f -m v3/domains/d000/servers/newserver
SuperSecretPassword123!
---
username: admin
ip: 10.50.1.100
port: 22
notes: Production server
created: 2026-02-17
EOF
Troubleshooting
"No secret key" Error
# Check YubiKey is inserted and detected
gpg --card-status 2>&1 | head -5 || echo "Card not detected"
# If key stubs are missing, reimport public key
gpg --import public.asc
# Then refresh stubs by reading card
gpg --card-status
Touch Not Working
# Check touch policy is enabled
ykman openpgp info 2>&1 | grep -i touch
# Restart gpg-agent (may have stale state)
gpgconf --kill gpg-agent && gpgconf --launch gpg-agent
Appendix: Security Checklist
| Item | Status | Verification Command |
|---|---|---|
Master key backed up |
[ ] |
|
Subkeys on primary YubiKey |
[ ] |
|
Subkeys on backup YubiKey |
[ ] |
Test with backup YubiKey inserted |
Touch required |
[ ] |
|
PIN changed from default |
[ ] |
|
Admin PIN changed |
[ ] |
Test with |
Backup YubiKey stored securely |
[ ] |
Physical verification (safe, not with laptop) |
Revocation cert backed up |
[ ] |
|
gopass v3 syncing |
[ ] |
|