78 lines
3.6 KiB
Markdown
78 lines
3.6 KiB
Markdown
|
|
# 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 <vault>` 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 <vault>` 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.
|