174 lines
7.3 KiB
Markdown
174 lines
7.3 KiB
Markdown
# Incorporating keep into an existing deploy setup
|
|
|
|
Concrete, by deploy *pattern* rather than by naming specific projects —
|
|
the shape of the fit depends on how a project currently gets its config
|
|
onto a target machine, not on what the project is.
|
|
|
|
## The general shape
|
|
|
|
Wherever a deploy script or a manually-maintained `.env` exists today, the
|
|
change is the same: replace "assume `.env` is already sitting there,
|
|
hand-copied at some point" with a `keep pull` at the top of the script.
|
|
|
|
```bash
|
|
# before — a deploy script assumes .env already exists on this machine
|
|
set -euo pipefail
|
|
source .env # or docker-compose reads it directly
|
|
|
|
# after
|
|
set -euo pipefail
|
|
keep pull <project>/<env> --out .env
|
|
```
|
|
|
|
The one-time setup cost per machine: `keep identity init` once, send the
|
|
printed public key to whoever runs `keep recipient add`, then
|
|
`keep identity set-id <id>` with what comes back. After that, every future
|
|
deploy from that machine just works — no more "wait, what's the DB
|
|
password again" when setting up a new box.
|
|
|
|
A deploy identity should almost always be granted **read-only** access
|
|
(`keep grant <vault> <deploy-recipient-id> --read-only`) — it only ever
|
|
needs to `pull`, and a read-only grant means a compromised deploy box
|
|
can't also rotate the vault or add itself more recipients.
|
|
|
|
When you do the first `keep push` for a project, push the *real*,
|
|
hand-maintained `.env` rather than a stripped-down one — any `# get this
|
|
from X` / `# rotate quarterly` comments in it get preserved (values
|
|
redacted) as a template alongside the secrets, retrievable later with
|
|
`keep pull <vault> --template`. That's the one moment this context is
|
|
available at all; a `.env` reconstructed later from memory won't have it.
|
|
|
|
## Getting the `keep` CLI onto a machine
|
|
|
|
Chicken-and-egg: every pattern below assumes `keep` is already a runnable
|
|
command on the machine doing the pulling. Two ways to get there, pick
|
|
based on whether you're fine with a git checkout existing on that
|
|
machine:
|
|
|
|
**A machine you're already developing on, or don't mind cloning onto**
|
|
(your laptop, a VPS you administer directly):
|
|
|
|
```bash
|
|
git clone <this repo>
|
|
cd keep
|
|
npm install
|
|
./scripts/install-cli.sh # builds, symlinks dist/cli/index.js onto PATH as `keep`
|
|
```
|
|
|
|
Symlinked rather than copied — a future `git pull && npm run build` is
|
|
the entire upgrade story, no need to re-run the install script.
|
|
|
|
**A deploy target you'd rather not clone a git repo onto** — bundle the
|
|
CLI and ship it over SSH, the same way flit/waste-go's `deploy-*.sh`
|
|
scripts ship a compiled binary:
|
|
|
|
```bash
|
|
HOST=user@your-vps ./deploy-cli.sh
|
|
```
|
|
|
|
Not quite a single file: the bundle itself (esbuild, ~18kb) covers
|
|
everything except `libsodium-wrappers`, which ships alongside it as the
|
|
real, unmodified package (~1.6MB total with its own dependency,
|
|
`libsodium`) rather than being force-inlined. That package's published
|
|
ESM build follows a broken relative import (see `src/shared/crypto.ts`'s
|
|
own comment on this), and statically inlining it via esbuild — technically
|
|
possible by routing through a literal `require()` call instead of
|
|
`import` — breaks its WASM engine's own Node-crypto feature detection at
|
|
runtime ("No secure random number generator found"), confirmed by
|
|
testing, not assumed. Shipping the real package is more reliable than
|
|
fighting that.
|
|
|
|
The script tars `dist-bundle/` (the esbuild output plus that one
|
|
dependency) and extracts it to `~/.keep-cli/` on the target over SSH,
|
|
then symlinks `~/bin/keep` to the entry point inside it — no git, no npm
|
|
install on that machine at all, just a `node` runtime (already there, if
|
|
it's running any of the other Node-based projects in this family). Re-run
|
|
it any time the CLI changes; safe to overwrite.
|
|
|
|
## Pattern: several docker-compose projects on one or more VPSes
|
|
|
|
The common shape once more than one or two projects are on `keep`:
|
|
each project lives in its own directory on the VPS
|
|
(`/opt/docker/<project>/docker-compose.yml`), and "deploy" means pulling
|
|
that project's env and running `docker compose up -d` in its directory.
|
|
`scripts/deploy.sh` in this repo is a small generic wrapper for exactly
|
|
that:
|
|
|
|
```bash
|
|
./deploy.sh myapp # pulls myapp/production, runs compose in /opt/docker/myapp
|
|
./deploy.sh myapp staging # pulls myapp/staging instead
|
|
DEPLOY_ROOT=/srv/apps ./deploy.sh myapp # different project root
|
|
```
|
|
|
|
One-time setup per VPS: `keep identity init` on that machine, register
|
|
its public key with `keep recipient add`, `keep identity set-id`, then
|
|
grant it **read-only** access to each project vault it deploys
|
|
(`keep grant <project>/production <vps-recipient-id> --read-only`). After
|
|
that, `./deploy.sh <project>` is the whole deploy step — no `.env`
|
|
hand-copied onto the box first.
|
|
|
|
For "which machine can deploy which project," `keep overview` (admin)
|
|
prints every vault and every recipient's access in one screen — the
|
|
thing to reach for instead of checking each vault's grants one at a
|
|
time.
|
|
|
|
## Pattern: docker-compose reading a local `.env`
|
|
|
|
A common shape — `docker compose up -d` reads `environment:` values from
|
|
a `.env` file sitting next to `docker-compose.yml` on the deploy machine,
|
|
hand-maintained and containing real secrets.
|
|
|
|
```bash
|
|
keep pull myapp/production --out .env
|
|
docker compose up -d
|
|
```
|
|
|
|
No code changes needed in the target project — `keep` only changes *how
|
|
the `.env` file gets there*, not what reads it. This is the easiest
|
|
category to retrofit.
|
|
|
|
## Pattern: cross-compile + scp + SSH-restart deploy scripts
|
|
|
|
A shape where a deploy script builds a binary locally, ships it to a VPS,
|
|
and restarts a service over SSH — reading a local `.env` next to the
|
|
script for things like shared secrets that shouldn't be hardcoded or
|
|
re-typed on every invocation.
|
|
|
|
```bash
|
|
keep pull myapp/production --out .env
|
|
set -a && source .env && set +a
|
|
# ...rest of the deploy script unchanged
|
|
```
|
|
|
|
Same retrofit as the docker-compose case, just applied before whatever
|
|
build/ship step already exists.
|
|
|
|
## Pattern: a plaintext config file served to a browser
|
|
|
|
Not every `config.js`/similar file that *looks* like environment config
|
|
actually holds secrets — some of it is public-by-design (an API base URL,
|
|
a feature flag) that's explicitly meant to be visible to anyone loading
|
|
the page. `keep` solves a confidentiality problem; a file with nothing
|
|
confidential in it doesn't have one. Worth checking this distinction
|
|
before wiring `keep` into anything — retrofitting a non-secret config
|
|
file would be solving a problem that file doesn't have.
|
|
|
|
## A vault server's own configuration
|
|
|
|
Whatever runs `keep` itself needs its own `ADMIN_PASSWORD` to be a real,
|
|
manually-set secret on its own host — it can't bootstrap its own trust
|
|
root by pulling itself from itself. This is the same "root of trust
|
|
starts somewhere unmanaged" fact every secrets system has (Vault's own
|
|
unseal keys aren't stored in Vault either). Not a gap, just worth being
|
|
explicit about: `keep`'s own deploy stays on a plain `.env`/
|
|
`docker-compose.yml` environment variables.
|
|
|
|
## New projects going forward
|
|
|
|
The actual best use case isn't retrofitting existing deploys — it's never
|
|
having the "hand-reconstruct a `.env` from memory" moment for a *new*
|
|
deploy target in the first place. For whatever gets built next: register
|
|
its production host as a `keep` recipient as part of first setup, write
|
|
the deploy script to `keep pull` from day one, and the annoyance this was
|
|
built to fix never has a chance to happen.
|