# passv A Vault-backed, drop-in replacement for [`pass`](https://www.passwordstore.org/) (the standard unix password-store). `passv` keeps the exact same on-disk layout — a tree of `.gpg` binary OpenPGP files under `$PASSWORD_STORE_DIR` — but performs **all encryption and decryption through a [`vault-plugin-secrets-gpg`](https://git.unkin.net/unkin/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 `"/"` (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 ```sh 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 " # point a store at it — like `pass init `, but with a Vault ref passv init gpg/app ``` Alias it over `pass` if you like: `alias pass=passv`. ## Commands Everything mirrors `pass`: ```sh 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 ```sh # (admin, once) mint a key in the engine vault write gpg/keys/personal algorithm=rsa-4096 identity="Me " # 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: ```sh 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`. ```sh # 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. ```sh OLD=~/.password-store export PASSWORD_STORE_DIR=~/.password-store-vault vault write gpg/keys/personal algorithm=rsa-4096 identity="Me " 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 `/` | Setup: ```sh # 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**: ```sh 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 ```sh 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.