Configuration

1 Recipient resolution

nimvault resolves the GPG recipient through a 3-tier chain. The first non-empty value wins:

  1. CLI flag: --recipient KEY (or -r KEY)

  2. Environment variable: NIMVAULT_GPG_RECIPIENT

  3. Config file: .vault/config in the git repository root

If none are set, nimvault exits with an error.

../_images/recipient_resolution.svg

2 Config file format

.vault/config uses a simple key-value format:

# Lines starting with # are comments
recipient = 9CCCE36402CB49A6
root = repo

Whitespace around the = is ignored. Recognized keys:

recipient

GPG key ID for encryption (required).

root

Base directory for path resolution. When set to repo, paths are stored relative to the repository root. When set to a relative path, it is resolved against the repository root. When absent, paths use ~/ prefix for portability.

wrap

Repeatable. <glob>:<recipient>[,<recipient>...] – which recipients may open the entries matching a path pattern. See below.

backend

gpg (default) or age. Also NIMVAULT_BACKEND.

identity

Age identity file. Also NIMVAULT_AGE_IDENTITY.

signer

gpg (default for the GPG backend), ssh (default for age), or none.

sign_key

SSH private key used to sign the manifest when signer = ssh.

allowed_signers

SSH allowed-signers file used to verify that signature.

signer_identity

Principal to match inside allowed_signers.

3 Per-path recipients

Not every machine should read every secret. A wrap rule maps a path pattern to the recipients allowed to open those entries:

recipient = LAPTOPKEY

# Specific rules first: the first match decides.
wrap = ~/.ssh/**:LAPTOPKEY
wrap = ~/.gnupg/**:LAPTOPKEY
wrap = **:LAPTOPKEY,BUILDBOXKEY

Order in the file is precedence, so the narrow rules go above the general one. An entry matching no rule uses recipient, which is why a config with no wrap lines behaves exactly as it did before rules existed.

3.1 How it works

Each entry’s payload is encrypted under its own data key (see the manifest reference). The data keys are grouped by recipient set, and each group is written to its own .vault/keys.<group>.gpg encrypted to that group alone. A machine decrypts the group files its key opens and no others, so it can read exactly the entries meant for it.

unseal on such a machine restores what it can read. check needs no key at all, so vault integrity is verifiable from anywhere.

Patterns: \* matches within one path segment, \*\* spans separators, and a pattern ending in / covers everything beneath it. Patterns are anchored at both ends, so ~/x/* does not match ~/x/deep/one.

3.2 Changing the rules

Editing wrap rules does not move anything by itself. Run nimvault rotate: it recomputes each entry’s group and rewrites the key files, leaving payloads untouched. An entry that moved group has its key written to the new group’s file and dropped from the old one.

3.3 A prerequisite worth stating

Groups are only worth anything when the machines hold different keys. If every machine shares one private key, every machine can open every group and the split is decoration. Give each machine its own key and exchange public keys before relying on this.

3.4 Break-glass recovery

Because a group takes several recipients, an offline key – generated once, printed or kept on removable media, never on any machine – can be added to the general rule. It then opens the vault if every machine is lost, without a threshold scheme or any additional machinery.

4 GPG key ID

The recipient value is any string GPG accepts as a recipient identifier:

  • Long key ID: 9CCCE36402CB49A6

  • Fingerprint: ABCD1234...

  • Email: user@example.com

Long key IDs are recommended for unambiguous matching.

4.1 Finding your key ID

gpg --list-keys --keyid-format long
# pub   rsa4096/9CCCE36402CB49A6 2024-01-01 [SC]
#                ^^^^^^^^^^^^^^^^ this is the key ID