Implement announce/watch: a deploy signal that reverses CI's reach
All checks were successful
Docker / build-and-push (push) Successful in 1m58s
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>
This commit is contained in:
@@ -183,6 +183,8 @@ keep push <vault> [--file .env] [--purge] -- encrypt + upload; requires a WRITE
|
||||
keep pull <vault> [--format env|json] [--out <path>] [--previous] -- fetch + decrypt; requires a grant
|
||||
keep grant <vault> <recipient-id> [--read-only] -- add a recipient to a vault you have WRITE access to
|
||||
keep log <vault> -- tail the access log for one vault
|
||||
keep announce <vault> --tag <tag> -- record a deploy tag; requires ANY grant on the vault (read-only is enough)
|
||||
keep watch <vault> [--interval 60] -- poll for a new announced tag; exits nonzero if the vault doesn't exist / no grant
|
||||
|
||||
# admin operations — gated by ADMIN_PASSWORD, see below
|
||||
keep recipient add --label "..." --pubkey <hex>
|
||||
@@ -215,6 +217,75 @@ Went with (2) — a global admin password is an acceptable trust model for
|
||||
"one person or small team running their own homelab," which is the
|
||||
actual scale this is built for.
|
||||
|
||||
## Announce/watch — a deploy signal, not a secret
|
||||
|
||||
CI producing a fresh image on every push still left deployment as a
|
||||
manual step, and the obvious fix — CI SSHes into the VPS and redeploys —
|
||||
was rejected: it means a standing SSH credential with production-deploy
|
||||
rights sits in CI secrets, reachable by whatever's executing on a shared
|
||||
runner across every project it builds. A compromised build step in one
|
||||
project could then reach every other project's deploy target, which is
|
||||
a real regression from `keep`'s own "read no longer implies write"
|
||||
posture (see above).
|
||||
|
||||
The fix that doesn't reintroduce that problem: reverse the direction.
|
||||
CI never reaches into a VPS — it tells `keep` "there's a new image for
|
||||
this project" (`keep announce <vault> --tag <tag>`), and each VPS, which
|
||||
already has to run *something* locally to redeploy itself, polls `keep`
|
||||
for that signal (`keep watch <vault>`) and acts on it locally, using an
|
||||
identity and grant it already legitimately holds. `keep`'s server never
|
||||
executes anything remotely — it stays a pure relay, same posture as
|
||||
everything else it does.
|
||||
|
||||
This was deliberately not built as "`keep` executes the redeploy
|
||||
itself," even though `keep` typically runs on the same box as its
|
||||
deploy targets and that would skip the polling indirection entirely.
|
||||
Doing so would trade away the property that's been the throughline of
|
||||
`keep`'s whole design: today, a fully compromised `keep` server — not
|
||||
just a compromised client identity — can't do anything worse than leak
|
||||
ciphertext and metadata, because the server never holds a decryption
|
||||
key. The moment `keep` can also execute a command on `announce`, an
|
||||
ordinary web app bug (not even a cryptographic break) becomes "attacker
|
||||
triggers arbitrary redeploys" — a new class of bad outcome, not a bigger
|
||||
version of an existing one.
|
||||
|
||||
An announced tag is stored outside the encrypted vault payload
|
||||
(`vault_announcements`, one row per vault — latest known state, not a
|
||||
history, same shape as `vaults.prev_ciphertext`) since an image tag
|
||||
isn't a secret and doesn't belong inside a secrets blob alongside real
|
||||
credentials. `POST /vaults/:key/announce` only requires *a* grant, not a
|
||||
write grant — `hasGrant`, not `hasWriteGrant` — since announcing isn't a
|
||||
mutation of a vault's secret content, just a signal; a CI identity for a
|
||||
project can stay read-only even with this feature in play. `keep watch`
|
||||
is deliberately dumb polling, not a websocket or long-poll — matches the
|
||||
pattern already used elsewhere in this project family, and a 60-second
|
||||
detection lag for a deploy is an acceptable tradeoff against the
|
||||
complexity of push-based notification.
|
||||
|
||||
Two things worth stating plainly about what this does and doesn't
|
||||
change:
|
||||
|
||||
- The tag itself is now server-visible, where previously not even the
|
||||
fact that a new image existed was something `keep` needed to know.
|
||||
Accepted tradeoff — a git short-SHA isn't sensitive, and the feature
|
||||
only works if `keep` can see it.
|
||||
- A compromised read-only CI identity could announce a *false* tag,
|
||||
pointing a deploy target at an attacker-chosen image. `keep` never
|
||||
pulls or runs that image itself, only relays the string — the actual
|
||||
`docker compose pull` on the deploy target still pulls from whatever
|
||||
registry it's configured for, using the tag it was told. This is
|
||||
equivalent in severity to that CI identity's build step being
|
||||
compromised in the first place (it could already push a bad image
|
||||
under a legitimate tag), not a new capability.
|
||||
|
||||
A bulk multi-project deploy script (looping over a fixed project list,
|
||||
`docker compose up -d` per project) adopts this by checking for a
|
||||
per-project `.keep-vault` marker file before calling `keep pull`; a
|
||||
missing marker falls through to exactly what the script did before
|
||||
`keep` existed — never a hard failure, since not every project will be
|
||||
bootstrapped onto `keep` at the same time. `scripts/deploy.sh` in this
|
||||
repo is the single-project version of the same pattern.
|
||||
|
||||
## Deploy integration
|
||||
|
||||
The actual motivating use case — a deploy script pulling secrets instead
|
||||
|
||||
Reference in New Issue
Block a user