Reject keep-executes-locally, switch to a .keep-vault marker file
Two changes based on further review: - Explicitly considers and rejects letting keep's server execute the redeploy directly on announce, even though co-location removes the credential/network objection that sank the CI-SSHes-in alternative. The reason it's still wrong: a fully compromised keep server today can't do anything worse than leak ciphertext, since it never holds a decryption key. Giving it exec capability breaks that property — a routine web app bug becomes "triggers arbitrary redeploys," not "leaks encrypted blobs." Also notes the concrete cost: doing this from inside a container needs the Docker socket mounted in, which is broadly equivalent to root on the host. - Replaces the "vault key = deploy directory name" assumption with an explicit .keep-vault marker file per project directory. Repo name, deploy directory name, and vault key are three independent strings in practice (confirmed by a real example already in this fleet) — no naming convention should assume any two of them match. Bootstrap is now: choose a vault key, keep push it, drop a one-line .keep-vault file. CI's announce step uses the same deliberately hardcoded key, never derived from the repo name automatically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,38 @@ happens today (a plain `docker compose up -d`, relying on that project's
|
|||||||
own `pull_policy: always`) for anything not yet bootstrapped, rather than
|
own `pull_policy: always`) for anything not yet bootstrapped, rather than
|
||||||
either blocking deploys entirely or hard-failing on missing vaults.
|
either blocking deploys entirely or hard-failing on missing vaults.
|
||||||
|
|
||||||
|
### Considered and rejected: `keep` executes the redeploy itself
|
||||||
|
|
||||||
|
Since `keep`'s own server will run on the same VPS as the deploy targets,
|
||||||
|
it's tempting to skip the polling indirection entirely and have `keep`
|
||||||
|
shell out to the deploy script directly when it receives an `announce` —
|
||||||
|
no new SSH credential, no new network-reachable pathway, since nothing
|
||||||
|
is reaching *into* the box that wasn't already running on it.
|
||||||
|
|
||||||
|
Rejected anyway, because it trades away a 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`, that stops being true — a bug in the HTTP layer
|
||||||
|
(not even a cryptographic break; an ordinary web app bug, an auth check
|
||||||
|
wrong on some edge case) becomes "attacker triggers arbitrary
|
||||||
|
redeploys," not "attacker sees encrypted blobs they can't read." That's
|
||||||
|
a new class of bad outcome, not a bigger version of an existing one, and
|
||||||
|
it's not worth trading for skipping a polling loop.
|
||||||
|
|
||||||
|
There's also a concrete operational cost, not just a principle: actually
|
||||||
|
running `docker compose up -d` from inside `keep`'s own process would
|
||||||
|
need either the Docker socket mounted into `keep`'s container
|
||||||
|
(`/var/run/docker.sock` — widely treated as equivalent to handing that
|
||||||
|
container root on the host) or running `keep` outside a container
|
||||||
|
entirely, throwing away the isolation its own `Dockerfile` already
|
||||||
|
provides today.
|
||||||
|
|
||||||
|
`keep` stays a pure relay regardless of where it's deployed. Something
|
||||||
|
*outside* `keep`'s own process boundary does the polling and the
|
||||||
|
executing — see §4.
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
### 1. `announce` — a distinct verb from `push`/`pull`
|
### 1. `announce` — a distinct verb from `push`/`pull`
|
||||||
@@ -172,17 +204,24 @@ for project in "${PROJECTS[@]}"; do
|
|||||||
echo ""
|
echo ""
|
||||||
echo "▶ $project"
|
echo "▶ $project"
|
||||||
|
|
||||||
# Best-effort: does this machine have a working keep grant for
|
# A vault key is NOT assumed to match the deploy-directory name — repo
|
||||||
# "$project/production"? If not — not bootstrapped onto keep yet, or
|
# names, deploy directory names, and vault keys are three independent
|
||||||
# never will be (a static site with no secrets) — fall through to
|
# strings in practice (a repo can be named one thing, its deploy
|
||||||
# exactly what this script already did before keep existed. Every
|
# directory another, entirely by history/accident). .keep-vault is an
|
||||||
# keep call here is allowed to fail; failure means "keep doesn't
|
# explicit, one-time-per-project marker: if it's missing, this project
|
||||||
# apply to this project," never "stop the deploy."
|
# was never bootstrapped onto keep (or never will be — a static site
|
||||||
if keep pull "$project/production" --out "$dir/.env.new" 2>/dev/null; then
|
# with no secrets), and the loop falls through to exactly what it did
|
||||||
|
# before keep existed. Every keep call here is allowed to fail;
|
||||||
|
# failure means "keep doesn't apply to this project," never "stop the
|
||||||
|
# deploy."
|
||||||
|
if [ -f "$dir/.keep-vault" ]; then
|
||||||
|
vault="$(cat "$dir/.keep-vault")"
|
||||||
|
if keep pull "$vault" --out "$dir/.env.new" 2>/dev/null; then
|
||||||
mv "$dir/.env.new" "$dir/.env"
|
mv "$dir/.env.new" "$dir/.env"
|
||||||
else
|
else
|
||||||
rm -f "$dir/.env.new"
|
rm -f "$dir/.env.new"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if (cd "$dir" && docker compose up -d 2>&1); then
|
if (cd "$dir" && docker compose up -d 2>&1); then
|
||||||
echo " ✓ $project up"
|
echo " ✓ $project up"
|
||||||
@@ -193,8 +232,12 @@ for project in "${PROJECTS[@]}"; do
|
|||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
Vault key convention: `<project>/production`, using the exact directory
|
Bootstrapping a project onto `keep` becomes: choose a vault key (no
|
||||||
name already in `PROJECTS` — no separate mapping table to keep in sync.
|
naming constraint — doesn't need to match the repo or the directory),
|
||||||
|
`keep push` it, grant whichever recipients need it, and
|
||||||
|
`echo "<the vault key>" > "$dir/.keep-vault"`. One file, human-readable,
|
||||||
|
`cat`-able to check what's configured, no second array in this script to
|
||||||
|
keep in sync by hand as directories get renamed or reorganized.
|
||||||
|
|
||||||
Notice `announce`/`watch` aren't actually load-bearing in this version —
|
Notice `announce`/`watch` aren't actually load-bearing in this version —
|
||||||
whatever already triggers this script (cron, systemd timer, or still
|
whatever already triggers this script (cron, systemd timer, or still
|
||||||
@@ -249,6 +292,17 @@ project hasn't been bootstrapped onto `keep` yet):
|
|||||||
run: keep announce myapp/production --tag ${{ steps.meta.outputs.short_sha }}
|
run: keep announce myapp/production --tag ${{ steps.meta.outputs.short_sha }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The vault key here (`myapp/production`) is a **literal, hardcoded string
|
||||||
|
in this specific repo's own workflow file** — never derived from
|
||||||
|
`${{ github.repository }}` or any other automatic source. Same reasoning
|
||||||
|
as §4's `.keep-vault` file: the repo's own name, its deploy directory's
|
||||||
|
name, and its vault key are three independent strings that happen to
|
||||||
|
often look similar and are not guaranteed to match (see Context — a real
|
||||||
|
example already exists in this fleet). Whoever bootstraps a project onto
|
||||||
|
`keep` should paste the same deliberately-chosen vault key into both this
|
||||||
|
workflow step and that project's `.keep-vault` file, not rely on either
|
||||||
|
side inferring it from something else.
|
||||||
|
|
||||||
`continue-on-error: true` is load-bearing, not incidental — a project
|
`continue-on-error: true` is load-bearing, not incidental — a project
|
||||||
without a `keep` identity registered yet must still build and push its
|
without a `keep` identity registered yet must still build and push its
|
||||||
image successfully. This step failing should be silent/expected for any
|
image successfully. This step failing should be silent/expected for any
|
||||||
@@ -287,6 +341,13 @@ not-yet-bootstrapped project, not a red X on the workflow.
|
|||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
|
- **`.keep-vault` format** — a bare vault-key string is enough for
|
||||||
|
today's one-environment-per-directory reality. If a directory ever
|
||||||
|
needs to deploy more than one environment from the same
|
||||||
|
`docker-compose.yml` (unlikely given the current fleet shape, but not
|
||||||
|
impossible), a bare string stops being enough and it'd need to become
|
||||||
|
a small key=value file or similar. Not designing that ahead of an
|
||||||
|
actual need — ship the bare-string version.
|
||||||
- **Fleet-wide `pull_policy: always` sweep** — a prerequisite this
|
- **Fleet-wide `pull_policy: always` sweep** — a prerequisite this
|
||||||
proposal depends on but doesn't itself fix (see Context). Should
|
proposal depends on but doesn't itself fix (see Context). Should
|
||||||
happen before or alongside this work, as its own small pass, not
|
happen before or alongside this work, as its own small pass, not
|
||||||
|
|||||||
Reference in New Issue
Block a user