Merge origin/main into EGB-283 (multi-recipient age encryption)
Reconcile the multi-recipient branch (cut from v0.6.1.0) with origin/main
at v0.7.4.0. The two feature lines are largely orthogonal; the one real
integration point is the external-blob encrypt path:
- EGB-712 added an additive-v2 dual-write loop (_external_blob_write_targets,
writing v2 + any v1 twin). EGB-283 routes every encrypt site through
RECIPIENT_ARGS for N-recipient encryption. Resolution keeps the dual-write
loop but encrypts each target to the full recipient set
(age "${RECIPIENT_ARGS[@]}" per write target), so dual-write and
multi-recipient compose. cmd_push loads recipients before both external
push sites; legacy single-key rekey keeps its fresh-keypair pubkey path.
Version: 0.6.2.0 + 0.7.4.0 -> 0.7.5.0. Docs (CLAUDE.md/README/CHANGELOG)
merged to carry both feature sets; subcommand list now includes
recipients/reencrypt and upgrade.
Tests: full `bats test/` green except 6 pre-existing host-environment
failures (4 chmod-600 restore assertions + 2 jq-PATH-shadow tests, all
macOS-authored), none touching merged code. recipients.bats 34/34 pass;
external/dual-write area passes except the same mode-600 host artifacts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
e29024bd63
16 changed files with 2723 additions and 142 deletions
142
README.md
142
README.md
|
|
@ -58,83 +58,95 @@ Beyond project files, `secrets` can also sync files that live *outside* the proj
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- **macOS** (uses Homebrew for installation)
|
||||
- **git** (already installed on most Macs — type `git --version` to check)
|
||||
- **age** (the encryption tool — installed in step 1 below)
|
||||
- **macOS or Linux**
|
||||
- **git** (`git --version` to check)
|
||||
- **age** and **jq** — `install.sh` checks for these and prints the exact install command for your platform (Homebrew on macOS, `apt`/`dnf` on Linux)
|
||||
|
||||
## Setup
|
||||
|
||||
### First machine (one-time setup)
|
||||
Clone the tool repo, then run `install.sh`. It checks dependencies and prints the
|
||||
two commands to finish setup. It never edits your shell config and never runs
|
||||
sudo — it prints the commands so you stay in control.
|
||||
|
||||
```bash
|
||||
# 1. Install the encryption tool
|
||||
brew install age
|
||||
|
||||
# 2. Download the secrets tool (this repo — contains only the CLI, no secret files)
|
||||
git clone https://codeberg.org/egbt/secrets.git ~/dev/secrets
|
||||
|
||||
# 3. Make the 'secrets' command available everywhere
|
||||
# Add this line to your shell config file (~/.zshrc on Mac):
|
||||
export PATH="$HOME/dev/secrets:$PATH"
|
||||
# Then restart your terminal, or run:
|
||||
source ~/.zshrc
|
||||
|
||||
# 4. Initialize your encrypted secrets store
|
||||
# This creates a folder at ~/.secrets/ with your encryption key
|
||||
secrets init
|
||||
|
||||
# 5. Create a PRIVATE repository on GitHub to store your encrypted secrets
|
||||
# Go to github.com/new, name it something like 'my-secrets', and make sure
|
||||
# "Private" is selected. Then connect it:
|
||||
cd ~/.secrets
|
||||
git remote add origin git@github.com:<you>/my-secrets.git
|
||||
git push -u origin main
|
||||
cd ~/dev/secrets
|
||||
./install.sh
|
||||
```
|
||||
|
||||
> **Important:** Step 5 creates a *separate* private repo for your encrypted secrets. This is different from the `secrets` tool repo you cloned in step 2. The tool repo can be public — it contains no secrets. The `~/.secrets/` repo must be private.
|
||||
`install.sh` prints a `export PATH="$HOME/dev/secrets:$PATH"` line — add it to your
|
||||
shell config (`~/.zshrc` or `~/.bashrc`) and restart your terminal. Then onboard
|
||||
this machine with one of the two flows below.
|
||||
|
||||
### Additional machines
|
||||
|
||||
On each new machine (your desktop, a teammate's laptop, etc.):
|
||||
### First machine (new vault)
|
||||
|
||||
```bash
|
||||
# 1. Install prerequisites and the tool (same as steps 1-3 above)
|
||||
brew install age
|
||||
git clone https://codeberg.org/egbt/secrets.git ~/dev/secrets
|
||||
export PATH="$HOME/dev/secrets:$PATH" # add to ~/.zshrc
|
||||
# 1. Create a PRIVATE repo for your encrypted secrets (github.com/new or a
|
||||
# Codeberg/GitLab private repo). It holds only ciphertext — never your key.
|
||||
# Then wire it up and push the store in one command:
|
||||
secrets init --remote git@github.com:<you>/my-secrets.git
|
||||
|
||||
# 2. Clone the encrypted secrets repo
|
||||
git clone git@github.com:<you>/my-secrets.git ~/.secrets
|
||||
|
||||
# 3. Copy the encryption key from your first machine
|
||||
# This is the only step that requires direct machine-to-machine transfer.
|
||||
# Choose one method:
|
||||
#
|
||||
# Option A: AirDrop (Mac to Mac)
|
||||
# On your first machine, right-click ~/.secrets/key.txt → Share → AirDrop
|
||||
# Save it to ~/.secrets/key.txt on the new machine
|
||||
#
|
||||
# Option B: Secure copy over SSH
|
||||
# scp first-machine:~/.secrets/key.txt ~/.secrets/key.txt
|
||||
#
|
||||
# Option C: USB drive
|
||||
# Copy key.txt to a USB drive, transfer it, delete from USB after
|
||||
|
||||
# 4. Pull your secrets into any project
|
||||
# 2. (optional) Add a project's secrets. From a project directory:
|
||||
cd ~/myapp
|
||||
secrets pull
|
||||
secrets push
|
||||
# The first push asks once whether to auto-track new env files and records
|
||||
# your choice in the project's .secrets.json.
|
||||
```
|
||||
|
||||
`secrets init --remote` generates your key (`~/.secrets/key.txt`), wires the
|
||||
remote, and pushes the initial store so the upstream branch exists. The private
|
||||
secrets repo is separate from this tool repo — the tool repo is public and holds
|
||||
no secrets; the `~/.secrets/` repo must be private.
|
||||
|
||||
> Running `secrets init` interactively (in a terminal) also offers to add your
|
||||
> first project's secrets right away. Run it with `--yes` (or in any non-tty
|
||||
> context like CI) to skip that prompt and just create the vault.
|
||||
|
||||
### Other machines (join an existing vault)
|
||||
|
||||
On a second machine, a desktop, or a teammate's laptop:
|
||||
|
||||
```bash
|
||||
# 1. Clone the tool and run the bootstrap (as in Setup above)
|
||||
git clone https://codeberg.org/egbt/secrets.git ~/dev/secrets
|
||||
cd ~/dev/secrets && ./install.sh # add the printed PATH line to your shell config
|
||||
|
||||
# 2. Get key.txt onto this machine (the one manual, out-of-band step):
|
||||
# AirDrop (Mac→Mac), or
|
||||
# scp first-machine:~/.secrets/key.txt ~/Downloads/key.txt, or
|
||||
# a USB drive (delete from the drive afterward)
|
||||
|
||||
# 3. Join the vault in one command:
|
||||
secrets join --remote git@github.com:<you>/my-secrets.git --key ~/Downloads/key.txt
|
||||
```
|
||||
|
||||
`secrets join` clones the vault, installs the key at mode 600, and **verifies the
|
||||
key actually decrypts the store before declaring success** — a mis-copied key
|
||||
fails loudly here, not silently on a later `secrets pull`. On success it tells you
|
||||
to run `secrets pull` in any project.
|
||||
|
||||
> **The key file (`~/.secrets/key.txt`) is the only thing that needs to be transferred manually.** It never leaves your machines — it's excluded from git, never uploaded, never transmitted over the internet. Anyone with this file can decrypt all your secrets, so treat it like a password.
|
||||
|
||||
### Sharing with teammates
|
||||
|
||||
**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)
|
||||
1. Access to your private secrets repo (add them as a collaborator)
|
||||
2. A copy of `key.txt` (send it 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.
|
||||
Everyone on the team uses the same key. A teammate joins with
|
||||
`secrets join --remote <repo-url> --key <path-to-key.txt>`. When anyone runs
|
||||
`secrets push`, the encrypted files update and everyone else runs `secrets pull`
|
||||
to get the latest.
|
||||
|
||||
### Updating the tool
|
||||
|
||||
```bash
|
||||
git -C ~/dev/secrets pull
|
||||
```
|
||||
|
||||
If your store was last written by a newer client than yours, `secrets` prints a
|
||||
one-line version-skew nudge — that's your cue to run the command above.
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -169,17 +181,35 @@ secrets clear
|
|||
| `secrets clear` | Delete plaintext secret files from the current directory |
|
||||
| `secrets run <command>` | Pull secrets, run a command, then clear secrets when it exits |
|
||||
| `secrets list` | Show all projects that have stored secrets |
|
||||
| `secrets list --json` | Same listing as a machine-readable JSON object (`{store, projects[].entries[]}`, each entry `dotenv`/`external`) for tooling and CI. JSON goes to stdout; notices to stderr |
|
||||
| `secrets rm <project>` | Delete a project's secrets from the store |
|
||||
| `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 migrate --finalize` | **Optional GC** — drop the old v1 blobs and mark the store pure v2. Never required: upgraded clients dual-write and read-fall-back, so not finalizing never cuts anyone off |
|
||||
| `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) |
|
||||
| `secrets upgrade` | Self-update the tool: `git pull --ff-only` on the `secrets` checkout, report old → new version, then re-check store version-skew. No auto-update, no background checks |
|
||||
| `secrets upgrade --check` | Report whether an update is available (without pulling); changes nothing |
|
||||
|
||||
### Upgrading: do teammates on an older `secrets` get new secrets?
|
||||
|
||||
Store-format v2 is **additive** — an upgraded client reads either blob suffix and keeps the old (v1) suffix alive for externals that already existed, so you almost never have to coordinate an upgrade:
|
||||
|
||||
| Secret type | Old client gets it? |
|
||||
|---|---|
|
||||
| `.env` / `.env.*` / `.dev.vars` | **Yes, always** (blob name is identical across formats) |
|
||||
| whole-file external (`file`) | **Yes, always** |
|
||||
| `properties` external that already existed | **Yes** (dual-written so old clients stay fresh) |
|
||||
| brand-new `properties` external | **No — must upgrade `secrets`** (the gentle forcing function) |
|
||||
|
||||
"Upgrade your secrets" = `git pull` the tool clone (binary ≥ 0.6.0.0) and/or `secrets migrate` the store. A read-only teammate only needs the tool `git pull`.
|
||||
|
||||
And you'll be told when you're behind: if a store was last written by a newer `secrets` than the one you're running, any command prints a one-line nudge to stderr (non-fatal) — and `secrets which` shows the store's `written-by:` version. Stores written by older builds (no version stamp) stay silent.
|
||||
|
||||
### Automatic project detection
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue