docs: multi-recipient age encryption (recipients/reencrypt) + bump 0.6.2.0 (EGB-283)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brian Majewski 2026-06-24 13:25:33 -07:00
parent b09f94c92f
commit f7576a3eae
5 changed files with 125 additions and 7 deletions

View file

@ -129,13 +129,15 @@ secrets pull
### Sharing with teammates
To share secrets with a teammate, they need:
**Simple approach (shared key):** To share secrets with a teammate, they need:
1. Access to your private `my-secrets` GitHub repo (add them as a collaborator)
2. A copy of `key.txt` (send it to them directly — AirDrop, USB, or in-person)
Everyone on the team uses the same key. When anyone runs `secrets push`, the encrypted files are updated and everyone else can `secrets pull` to get the latest version.
**Per-teammate keys (recommended for teams):** Use `secrets recipients add` so each person keeps their own private key — no key sharing needed. See [Onboarding and offboarding teammates](#onboarding-and-offboarding-teammates) below.
## Usage
### Daily workflow
@ -168,12 +170,16 @@ secrets clear
| `secrets run <command>` | Pull secrets, run a command, then clear secrets when it exits |
| `secrets list` | Show all projects that have stored secrets |
| `secrets rm <project>` | Delete a project's secrets from the store |
| `secrets rekey` | Generate a new encryption key and re-encrypt everything |
| `secrets rekey` | Generate a new encryption key and re-encrypt everything (single-key store) or re-encrypt to the current recipients without changing keys (multi-recipient store) |
| `secrets verify [project]` | Check the current project's `.secrets.json` against the store (missing/orphaned blobs) and decrypt every blob. `[project]` overrides the store directory name; the manifest is still read from the current directory |
| `secrets verify --all` | Decrypt-test every blob in every project — a store-wide integrity sweep |
| `secrets migrate [--dry-run]` | Copy-forward this project's encrypted blobs to store format v2 (non-destructive; manifest-free; `--dry-run` previews) |
| `secrets migrate --status` | Survey every project's v2 readiness; exits non-zero until the whole store is finalize-ready |
| `secrets migrate --finalize` | Drop the old v1 blobs and mark the store v2 — runs once, store-wide, after `verify` is green and every machine is upgraded |
| `secrets recipients list` | List the store's recipient public keys (and names if set) |
| `secrets recipients add <age1…> [--name N]` | Add a recipient key to the store and immediately re-encrypt every blob to the new set |
| `secrets recipients rm <key\|name> [--yes]` | Remove a recipient and re-encrypt the store; `--yes` required when removing your own key |
| `secrets reencrypt` | Re-encrypt every blob to the current recipients (idempotent — useful after a manual edit or partial failure) |
### Automatic project detection
@ -448,6 +454,64 @@ Some external secrets are whole binary files — an Android upload keystore, a c
On `secrets push` the file is encrypted into `<project>/external/`. On `secrets pull` it is restored to the same path with mode `600`; if a different version already exists there, it is backed up to `<name>.secrets-bak` first. The same path rules apply (inside `$HOME`, no `..`, no symlinks). Like merged Gradle keys, restored files are permanent plaintext on disk — `secrets clear` does not remove them.
### Onboarding and offboarding teammates
By default every team member uses the **same** `key.txt` (one shared private key). The multi-recipient feature lets each teammate have their **own** keypair while still sharing one store — so you never hand out a secret key to a new hire, and removing an ex-teammate's access is one command.
#### Onboarding a teammate
```bash
# 1. Teammate generates their own keypair on their machine (never shares the private key)
age-keygen -o ~/.secrets/key.txt # writes key.txt; prints the public key
# 2. Teammate sends you their PUBLIC key (printed by age-keygen, starts with age1…)
# — over Slack, email, whatever. Public keys are not secret.
# 3. An existing member adds the public key to the store
secrets recipients add age1theirpublickey --name alice
# => Adds alice to recipients.txt, re-encrypts every blob to the full set, pushes.
# 4. Teammate clones the store repo and drops their key.txt in place
git clone git@github.com:<you>/my-secrets.git ~/.secrets
# (key.txt already generated in step 1 — nothing to copy)
# 5. Teammate pulls into any project
cd ~/myapp
secrets pull
# => Their key matches one recipient stanza in every blob — it just works.
```
Run `secrets recipients list` to confirm who has access:
```
alice age1theirpublickey…
you age1yourpublickey…
```
#### Offboarding a teammate
```bash
# Remove the recipient by name (or public key) and re-encrypt the store
secrets recipients rm alice
# => Removes alice from recipients.txt, re-encrypts every blob, pushes.
# New blobs are no longer readable by alice's key.
```
> **Important:** git history can't be un-shared. If alice had access during a period when genuinely sensitive values were stored, rotate those values now (update them in the external system and run `secrets push`). The re-encrypt prevents future access; history is permanent.
#### Managing recipients
```bash
secrets recipients list # show all recipient keys and names
secrets recipients add age1… # add a key (bootstraps recipients.txt on a legacy store)
secrets recipients add age1… --name bob # attach a human-readable label
secrets recipients rm bob # remove by name
secrets recipients rm age1… # remove by public key
secrets reencrypt # re-encrypt to current recipients (idempotent heal)
```
`secrets which` shows a `recipients: N (alice, bob, …)` line so you can always confirm the active set from any project directory.
## Safety features
- **`secrets run` auto-clears** — plaintext files are deleted when the command exits, errors, or is interrupted with Ctrl-C
@ -501,7 +565,7 @@ For complete rotation with no historical exposure, create a fresh `~/.secrets/`
## Development
```bash
# Run the test suite (237 tests across three files)
# Run the test suite (272 tests across four files)
brew install bats-core
bats test/