# keep A self-hosted, end-to-end encrypted secrets store for deploy scripts. `keep pull wisp/production` gets you a `.env`-shaped bundle of secrets, decrypted locally — the server never sees plaintext values, and never sees the key that would let it. ## Why Across this project family — `goonk`, `wisp`, `npm-statuspage`, `flit`, `waste-go` — every repo has its own `.env`/`.env.example` pair, and every one of those `.env` files exists today by being hand-copied onto whatever machine needs it. That's a real, already-felt annoyance, not a hypothetical one: - Rotating a credential means finding and updating every machine that has a copy, by hand. - There's no record of which machine has which secret, or when it was last updated. - Onboarding a new deploy target means manually reconstructing a `.env` from memory, a password manager note, or asking "hey, what's the value for X." `keep` is a small always-on service that holds encrypted secret bundles and lets authorized machines/people pull them, decrypted locally, without ever exposing plaintext to the server in between. ## What it isn't - **Not Vault or Doppler.** Those are the established, heavier answers — full policy engines, dynamic secrets, enterprise everything. `keep` is the "already-solved problem, but self-hosted and scoped to what this actually needs" answer, closer in spirit to `age`/`sops` (multi-recipient encrypted files) than to a full secrets-management platform. - **Not a static encrypted file committed to git**, which is what `age`/`sops` typically produce. `keep` exists specifically for the two things a committed encrypted file can't give you without re-running a tool and re-committing: **live rotation** (push once, everyone with access can pull the update) and **an access log** (who/what actually fetched a secret bundle, and when — a git history shows content changes, not access). - **Not wisp.** wisp is ephemeral and single-retrieval by design — the opposite of what a secret needs to be. A deploy script needs to fetch the same bundle on every deploy, not once and then have it vanish. ## Core model Adapts the pattern `age`/`sops`/PGP already use for multi-recipient encrypted files — a symmetric key encrypts the actual payload, and that symmetric key is separately "wrapped" (encrypted) once per authorized recipient's public key — into a live service instead of a static file: 1. Each client (a developer's machine, a deploy script running on a VPS) has its own Ed25519/X25519 keypair — same identity model already built twice over in `flit` and `waste-go`. 2. A vault (`wisp/production`, `goonk/production`, whatever key) holds one encrypted blob — the actual secret bundle, encrypted under a random symmetric key. 3. That symmetric key is wrapped separately for every recipient granted access to that vault. The server stores N small wrapped-key blobs, one per recipient, alongside the one encrypted payload. 4. `keep pull ` fetches the payload plus this identity's wrapped key, unwraps locally with the private key, decrypts, and prints (or writes) the `.env`-shaped result. 5. Revoking a recipient's access is deleting their one wrapped-key row — the payload and every other recipient's access are untouched. 6. Rotating a secret is `keep push ` again — a new random symmetric key, re-wrapped for every currently-granted recipient. Cheap, since the recipient list rarely changes. Full design — data model, the atomic-grant/revoke details, the CLI surface, and what this explicitly does and doesn't protect against — is in [IMPLEMENTATION.md](./IMPLEMENTATION.md). ## Status Design only. No code yet.