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