2026-07-16 22:28:43 +10:00

passv

A Vault-backed, drop-in replacement for pass (the standard unix password-store). passv keeps the exact same on-disk layout — a tree of <name>.gpg binary OpenPGP files under $PASSWORD_STORE_DIR — but performs all encryption and decryption through a vault-plugin-secrets-gpg engine mount. The GPG private key never lives on the client: it stays sealed inside Vault, and every show/edit/grep delegates decryption to the engine.

The only visible difference from pass is what a store is initialized against: instead of a GPG key fingerprint, .gpg-id holds a Vault key reference "<mount>/<key>" (e.g. gpg/app).

passv show email/gmail
   │  read email/gmail.gpg (binary OpenPGP)
   ▼
Vault  gpg/decrypt/app   ──▶  plaintext   (private key never leaves the barrier)

Setup

export VAULT_ADDR=https://vault.example        # standard Vault env
export VAULT_TOKEN=...                          # or ~/.vault-token / `vault login`
export PASSWORD_STORE_DIR=~/.password-store     # optional (this is the default)

# create a key in the engine once (admin side)
vault write gpg/keys/app algorithm=rsa-4096 identity="Me <me@example>"

# point a store at it — like `pass init <fingerprint>`, but with a Vault ref
passv init gpg/app

Alias it over pass if you like: alias pass=passv.

Commands

Everything mirrors pass:

passv                         # list the whole store as a tree
passv ls work                 # list a subfolder
passv show email/gmail        # decrypt to stdout
passv show -c email/gmail     # copy first line to the clipboard (auto-clears)
passv insert email/gmail      # prompt (twice, no echo) and store
passv insert -m note          # multiline entry (Ctrl-D to finish)
passv generate -n wifi 32     # 32-char symbol-free password, stored + printed
passv edit email/gmail        # decrypt into $EDITOR, re-encrypt on save
passv mv a b                  # move (re-encrypts if b is under a different .gpg-id)
passv cp a b                  # copy (likewise)
passv rm -r work              # remove an entry or subtree
passv find gmail              # list entries matching a term
passv grep -i 'aws_.*key'     # regexp-search decrypted content
passv git log                 # run git in the store

Per-subtree keys work exactly like pass — passv init -p work gpg/team gives everything under work/ a different Vault key; mv/cp across that boundary transparently decrypt with the old key and re-encrypt with the new one.

Environment

Variable Purpose
PASSWORD_STORE_DIR store location (default ~/.password-store)
VAULT_ADDR, VAULT_TOKEN standard Vault connection (falls back to ~/.vault-token)
PASSWORD_STORE_CLIP_TIME seconds before the clipboard is cleared (default 45)
PASSWORD_STORE_GENERATED_LENGTH default generate length (default 25)
EDITOR editor for passv edit (default vi)

Clipboard support uses wl-copy, xclip, xsel or pbcopy if present.

Interoperability

Because entries are standard OpenPGP messages encrypted to the engine key's public key, anyone holding that public key (e.g. gpg --import of vault read -field=public_key gpg/keys/app) can add entries with plain gpg; only decryption requires Vault. Conversely, a store created by real pass against the engine's public key is readable by passv unchanged.

Recipes

1. Create a brand-new store

# (admin, once) mint a key in the engine
vault write gpg/keys/personal algorithm=rsa-4096 identity="Me <me@unkin.net>"

# point a fresh store at it — writes ~/.password-store/.gpg-id = "gpg/personal"
export PASSWORD_STORE_DIR=~/.password-store
passv init gpg/personal

# optional: version the store with git (passv auto-commits every change)
passv git init
passv git remote add origin git@git.unkin.net:me/passwords.git

# start adding secrets
passv insert email/gmail
passv generate -n wifi/home 32
passv edit notes/recovery-codes
passv                                # browse the tree

Different subtrees can use different Vault keys — handy for shared vs personal secrets:

passv init -p work gpg/team          # everything under work/ uses gpg/team
passv generate work/ci/deploy-token 40

2. Migrate a traditional GPG pass store → passv

You have an existing ~/.password-store encrypted to a local GPG key. Two paths, depending on whether you want to keep that key or rotate to a Vault-native one.

Option A — import your GPG key into Vault (no re-encryption, instant). Every existing .gpg file already decrypts once Vault holds the matching private key; you only repoint .gpg-id.

# export the secret key that the store is encrypted to
gpg --export-secret-keys --armor you@example > /tmp/key.asc

# import it into the engine (stays sealed; not exportable)
vault write gpg/keys/mine/import private_key=@/tmp/key.asc exportable=false
shred -u /tmp/key.asc

# repoint the store: replace the fingerprint in .gpg-id with the Vault ref.
# (keep the original around until you've verified a few reads)
printf 'gpg/mine\n' > ~/.password-store/.gpg-id

passv show email/gmail               # decrypts via Vault, unchanged ciphertext

Option B — rotate onto a fresh Vault key (re-encrypts everything). Decrypt each entry with local gpg, re-encrypt to a new Vault key in a new store, then swap it in.

OLD=~/.password-store
export PASSWORD_STORE_DIR=~/.password-store-vault

vault write gpg/keys/personal algorithm=rsa-4096 identity="Me <me@unkin.net>"
passv init gpg/personal

find "$OLD" -name '*.gpg' | while read -r f; do
  name="${f#$OLD/}"; name="${name%.gpg}"
  gpg --quiet --decrypt "$f" | passv insert --multiline "$name"
done

# verify, then replace the old store
passv show email/gmail
mv "$OLD" "$OLD.bak" && mv ~/.password-store-vault ~/.password-store

3. Serve one store to both gpg and Vault at the same time

Encrypt each entry to two recipients — your local GPG key and the Vault key's public key — so it opens offline with plain pass/gpg and through Vault with passv. This is a real OpenPGP multi-recipient message; either private key decrypts it.

The two tools read the recipient list from different files:

File Read by Contents
.gpg-id pass, gpg GPG fingerprints (one per line)
.vault-id passv the Vault ref <mount>/<key>

Setup:

# 1. import the Vault key's PUBLIC half into your local gpg keyring
vault read -field=public_key gpg/keys/app | gpg --import
VAULT_FPR=$(vault read -field=fingerprint gpg/keys/app)
LOCAL_FPR=$(gpg --list-keys --with-colons you@example | awk -F: '/^fpr/{print $10; exit}')

# 2. list BOTH as recipients for pass, and the Vault ref for passv
cd ~/.password-store
printf '%s\n%s\n' "$LOCAL_FPR" "$VAULT_FPR" > .gpg-id      # pass encrypts to both
echo 'gpg/app'                                  > .vault-id # passv decrypts via Vault

Now write with pass (it encrypts to every id in .gpg-id), and read with either:

pass insert email/gmail        # multi-recipient: local key + Vault key
pass show   email/gmail        # offline, via your local gpg private key
passv show  email/gmail        # via Vault, private key never leaves the barrier

Note: passv insert/generate encrypt to the Vault key only (the engine is own-key). In a dual-mode store, add/edit entries with pass so both recipients are included; use passv for Vault-side reads (e.g. from CI or a host without the private key).

Build

make build   # -> dist/passv
make test    # go test -race (unit tests; no Vault needed)
make e2e     # full workflow against a Vault dev server + the gpg engine
make rpm     # passv RPM via nfpm

CI (Woodpecker) runs pre-commit/build/lint/test on PRs and builds+publishes the RPM to artifactapi rpm-internal on a v* tag.

S
Description
Vault-backed drop-in replacement for pass (password-store); routes GPG crypto through vault-plugin-secrets-gpg
Readme 60 KiB
Languages
Go 81%
Shell 14.6%
Makefile 4.4%