Commit Graph

7 Commits

Author SHA1 Message Date
Fredrik Johansson
8dcb675ad2 Save a redacted .env template alongside pushed secrets
All checks were successful
Docker / build-and-push (push) Successful in 1m51s
keep push now saves a comment/structure-preserving, value-redacted copy
of the source .env next to the encrypted secrets, so context like "why
this exists" or "get this from X" survives ingest instead of being
silently dropped. keep pull --template retrieves it. Old vaults pushed
before this existed keep working (flat payload, no template) and fail
with a clear error rather than crashing if --template is requested.
2026-07-15 20:41:45 +02:00
Fredrik Johansson
bb7f890a68 keep overview: print which server it's talking to
All checks were successful
Docker / build-and-push (push) Successful in 1m53s
Surfaced by a real mix-up this session: KEEP_SERVER_URL was set to a
"dev"-named hostname, and it was unclear without checking DNS/proxy
config whether that was actually the same server as the one running
on the deploy VPS or a separate instance -- nothing in `overview`'s
output (vault names, recipient labels) says which server answered.
Defaults silently to localhost:3050 if the env var isn't set at all,
which is its own way to end up looking at the wrong thing.

Printed before the fetch, not after, so it still shows up on a failed
request ("error: fetch failed") -- the exact case where knowing the
target mattered most and, before this, showed nothing at all.

Verified against a local test server: shows the right URL on success,
and shows the (default, unset) URL immediately before a deliberately
failed fetch to a port nothing's listening on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 16:25:40 +02:00
Fredrik Johansson
58d5d787dd Implement announce/watch: a deploy signal that reverses CI's reach
All checks were successful
Docker / build-and-push (push) Successful in 1m58s
CI announces a new image tag to keep (any grant is enough, read-only
included); each deploy target polls for it locally instead of CI
holding standing SSH access to production. keep stays a pure relay —
the tag lives outside the encrypted vault payload in its own table,
and keep never executes anything itself.

Adds vault_announcements, POST/GET /vaults/:key/announce, and the
`keep announce`/`keep watch` CLI commands, per
PROPOSAL-announce-and-agent.md (now marked implemented). Verified live:
a read-only identity announcing successfully and being logged, watch
picking up the tag on its first poll, and watch exiting nonzero for an
unknown/ungranted vault.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 21:12:55 +02:00
Fredrik Johansson
5f31eb76c7 overview: show when each vault was last updated, and by whom
Was silently dropping updatedAt/updatedBy even though the server
already returned them — the CLI just never printed them. Also
resolves updatedBy to the recipient's label server-side (matching how
grants already show a label alongside the recipient id) instead of
printing a bare recipient_id.

Verified against a live server: overview now prints "demo/production
(updated <iso timestamp> by test-box)" instead of omitting that line
entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:30:02 +02:00
Fredrik Johansson
f6a1ac13f2 Add keep overview command and a docker-compose deploy wrapper
All checks were successful
Docker / build-and-push (push) Successful in 1m57s
keep overview gives an admin a cross-vault view of every recipient and
grant in one screen, instead of walking vaults one at a time via
/vaults/:key/recipients. scripts/deploy.sh pulls a project's env from
keep before running docker compose up, for VPS deploys of several
docker-compose projects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:16:29 +02:00
Fredrik Johansson
001623c8e2 Resolve open design questions: write-scoped grants, rollback, purge
Per-question resolution, per best practice rather than deferral:

- Push authorization: closed the least-privilege gap where read implied
  write. vault_grants gained can_write (default true, so nothing
  existing changes); enforced on push and grant (granting others is
  itself a mutation of vault membership, so it needs write too, not
  just read). 'keep grant --read-only' creates a read-only grant.

- Version history: not full history (conflates "undo a typo'd push"
  with "this leaked, stop retaining it" into one mechanism). Retains
  exactly one previous version as a rollback safety net
  (vaults.prev_ciphertext/prev_nonce + a vault_grants_previous mirror
  table so recipients can unwrap it), plus 'keep push --purge' for
  compromise-driven rotations that explicitly skips retention and
  wipes any existing previous version too.

- Per-secret-key granularity: resolved by NOT building it — documented
  the escape hatch (split into more vaults) instead of adding partial-
  decrypt complexity for a problem the existing primitive solves.

One more real bug caught during verification: the CLI's --previous flag
initially signed a path including its query string, but the server
verifies against req.originalUrl with the query stripped — a mismatch
that would have made every --previous request fail signature
verification. Fixed by splitting the signed path from the request URL
in signedFetch, signing only the former.

Verified end-to-end with three independent identities: read-only grant
correctly blocked from push and from granting others, write access and
read-only status both preserved correctly across a rotation, previous-
version pull working for a routine push and correctly unavailable to
every recipient after a purge push. Also re-verified against a fresh
Docker build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:50:08 +02:00
Fredrik Johansson
67000b66ee Implement keep: self-hosted E2E encrypted secrets sync
Server: Express + better-sqlite3 (WAL), multi-recipient key-wrapping
per IMPLEMENTATION.md's design — vaults/recipients/vault_grants/
access_log. Two auth paths: ADMIN_PASSWORD header for recipient
management and revoke (pure metadata operations), signed-request
auth (Ed25519 signature over method+path+timestamp+body-hash) for
push/pull/grant, mirroring the spirit of this project family's other
signed-handshake patterns without naming them.

CLI: identity init/show/set-id, push/pull/grant/log, admin recipient
add/list/remove and revoke. Grant is a client-side crypto operation
(the granter unwraps the vault's current key locally and reseals it
for the new recipient) rather than a server-side operation, since the
server never holds an unwrapped key to grant with.

Verified end-to-end with two independent local identities against a
live server and separately against the built Docker image: register,
push, pull (granted and ungranted), grant without re-pushing, admin
revoke, a subsequent rotation confirming the revoked recipient stays
excluded, and rejection of missing/malformed signed-request auth.

Two real bugs caught during verification, not just written up:
- libsodium-wrappers' published ESM build does a relative import only
  resolvable under bundler-style resolution — broken under plain Node
  ESM. Fixed via createRequire to force the CJS build.
- Express's req.path inside a sub-router is relative to the mount
  point, which would have silently mismatched a client signing the
  full request path. Fixed by verifying against req.originalUrl.

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