Secrets Management Patterns
Secrets management patterns I’ve actually used. Every entry has a date and context.
2026-04-02: gocryptfs Vault for Credentials
Problem: Sensitive files (Claude Code credentials, SSH keys, API tokens) need encryption at rest on workstation. Plaintext credentials in ~/.config/ are a liability if the disk is imaged or the machine is compromised while locked.
Context: P16g deployment, gocryptfs vault setup for credential isolation.
The Fix:
# Initialize vault (first time only — prompts for master password)
gocryptfs -init ~/.credentials-encrypted
# Mount vault (prompts for master password)
gcvault mount credentials
# Mounts ~/.credentials-encrypted to ~/credentials/
# Symlink from mounted vault to expected paths
ln -s ~/credentials/claude-code/credentials.json ~/.config/claude-code/credentials.json
Rule: Use gocryptfs for at-rest encryption of credentials. gcvault wrapper handles mount/unmount. Symlink from the mounted vault to application-expected paths. When unmounted, the symlinks are dangling — applications fail gracefully instead of exposing plaintext.
Worklog: WRKLOG-2026-04-02
2026-04-02: gopass Root Store Path Fix
Problem: gopass operations fail after bootstrap on a new machine — "entry not found" for entries that exist. Root store path points to legacy ~/.password-store instead of the rsync’d location.
Context: P16g deployment, gopass config rsync’d from Razer. The Razer migrated from pass to gopass and the config still referenced the old path.
The Fix:
# BEFORE — identify the wrong path
grep 'path.*password-store' ~/.config/gopass/config
# FIX — update to actual root store location
sed -i 's|path = /home/evanusmodestus/.password-store|path = /home/evanusmodestus/.local/share/gopass/stores/root|' ~/.config/gopass/config
# VERIFY — check the full config
cat ~/.config/gopass/config
# Test decryption
export GPG_TTY=$(tty)
gopass ls | head -10
gopass show v3/domains/d000/identity/ssh/github
Rule: After gopass bootstrap on a new machine, always verify gopass config shows the correct root store path. The rsync’d config may reference a legacy path from the source machine.
Worklog: WRKLOG-2026-04-02
2026-04-02: pinentry-auto for SSH vs Desktop
Problem: GPG pinentry prompt style differs between SSH sessions (needs curses-based TUI) and desktop sessions (needs Qt/GTK graphical prompt). Using the wrong pinentry causes "Inappropriate ioctl for device" errors.
Context: P16g deployment, gopass used from both SSH and Hyprland desktop. pinentry-qt fails over SSH, pinentry-curses shows a TUI in the middle of Hyprland.
The Fix:
# pinentry-auto wrapper script — detects display environment
#!/bin/bash
if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then
exec pinentry-qt "$@"
else
exec pinentry-curses "$@"
fi
# Set in gpg-agent.conf
echo "pinentry-program /usr/local/bin/pinentry-auto" >> ~/.gnupg/gpg-agent.conf
# Restart gpg-agent to pick up the change
gpgconf --kill gpg-agent
Rule: Use a pinentry-auto wrapper that detects $DISPLAY or $WAYLAND_DISPLAY. Set it in ~/.gnupg/gpg-agent.conf. This handles SSH (curses) and desktop (Qt) seamlessly without manual switching.
Worklog: WRKLOG-2026-04-02
2026-04-02: age Encryption for SSH Config
Problem: SSH config contains sensitive host information (internal IPs, jump hosts, proxy commands) — must be encrypted before committing to git. Plaintext SSH config in a public repo is an information disclosure risk.
Context: dots-quantum stow package, SSH config as a private (gitignored) package with only the .age encrypted version tracked.
The Fix:
# Encrypt before commit
age -e -R ~/.age/recipients/self.txt -o ssh/.ssh/config.age ssh/.ssh/config
# Decrypt after clone on new machine
age -d -i ~/.age/identities/personal.key ssh/.ssh/config.age >| ssh/.ssh/config
# Then stow the SSH package
stow -t ~ ssh
Rule: SSH config is gitignored (plaintext). Only the .age encrypted version is tracked in git. Re-encrypt after EVERY edit — it is NOT automatic. The >| (clobber) operator overwrites any existing file without prompting.
Worklog: WRKLOG-2026-04-02
2026-04-02: GPG Lock File Cleanup After rsync
Problem: GPG operations hang with "waiting for lock" after rsync’ing ~/.gnupg/ from another machine. Stale lock files from the source machine block gpg-agent startup.
Context: P16g bootstrap, GPG keys rsync’d from Razer. Lock files (.lock, .#) reference the Razer’s PID and hostname.
The Fix:
# Remove stale locks
find ~/.gnupg -name "*.lock" -delete 2>/dev/null
find ~/.gnupg -name ".#*" -delete 2>/dev/null
# Kill any stale agent and restart clean
gpgconf --kill all
# Verify keys are accessible
gpg -K
Rule: After rsync’ing ~/.gnupg/, always clear lock files and restart gpg-agent before using GPG. Lock files contain source machine PIDs that will never release on the destination.
Worklog: WRKLOG-2026-04-02