Files
keep/README.md
Fredrik Johansson 2c34562c7f Initial design: self-hosted E2E encrypted secrets sync
README covers the pitch and how this differs from Vault/Doppler (too
heavy) and a static age/sops-encrypted file in git (no live rotation,
no access log). IMPLEMENTATION covers the multi-recipient key-wrapping
scheme (age/sops-style, adapted to libsodium primitives already used
in flit/waste-go), the flat vault/recipient/grant/access-log schema,
rotation and revocation semantics (including the standard "revoke
doesn't retroactively unread already-decrypted secrets" caveat), CLI
surface, and deploy-script integration.

Motivated by a real, already-felt annoyance: every repo in this
project family (goonk, wisp, npm-statuspage, flit, waste-go) has its
own hand-copied .env today, with no rotation story and no record of
which machine has which secret.

No code yet — design stage. Admin auth mechanism flagged as needing a
decision before implementation starts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:02:08 +02:00

3.6 KiB

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.

Status

Design only. No code yet.