diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 0bfa38d..106ef92 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -189,6 +189,7 @@ keep recipient add --label "..." --pubkey keep recipient list keep recipient remove keep revoke +keep overview -- every vault + every grant in one screen (GET /api/admin/overview) ``` ### Admin operations need their own authorization story diff --git a/INTEGRATION.md b/INTEGRATION.md index 5df7066..cbabf3f 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -31,6 +31,33 @@ A deploy identity should almost always be granted **read-only** access needs to `pull`, and a read-only grant means a compromised deploy box can't also rotate the vault or add itself more recipients. +## 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//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 /production --read-only`). After +that, `./deploy.sh ` 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 diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..dc7bb18 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Pulls a project's env from keep, then brings its docker compose stack up. +# +# Usage: +# ./deploy.sh [env] +# +# Expects, per project, a directory at $DEPLOY_ROOT// containing +# a docker-compose.yml. Vault key pulled is "/" (env +# defaults to "production"). Requires a keep identity already registered +# and granted read access to that vault on this machine +# (see INTEGRATION.md). +set -euo pipefail + +project="${1:?usage: deploy.sh [env]}" +env="${2:-production}" +root="${DEPLOY_ROOT:-/opt/docker}" +dir="$root/$project" + +if [ ! -d "$dir" ]; then + echo "error: $dir does not exist" >&2 + exit 1 +fi + +keep pull "$project/$env" --out "$dir/.env" +(cd "$dir" && docker compose up -d) diff --git a/src/cli/commands/recipient.ts b/src/cli/commands/recipient.ts index f16901c..c2cf30f 100644 --- a/src/cli/commands/recipient.ts +++ b/src/cli/commands/recipient.ts @@ -18,3 +18,22 @@ export async function recipientRemove(recipientId: string): Promise { await expectOk(await adminFetch('DELETE', `/api/admin/recipients/${encodeURIComponent(recipientId)}`)); console.log(`recipient removed: ${recipientId} (and all their vault grants)`); } + +export async function adminOverview(): Promise { + const data = await expectOk(await adminFetch('GET', '/api/admin/overview')); + + console.log('recipients:'); + if (!data.recipients.length) console.log(' none yet.'); + for (const r of data.recipients) { + console.log(` ${r.recipientId} ${r.label}`); + } + + console.log('\nvaults:'); + if (!data.vaults.length) console.log(' none yet.'); + for (const v of data.vaults) { + console.log(` ${v.vaultKey}`); + for (const g of v.grants) { + console.log(` ${g.canWrite ? 'rw' : 'r-'} ${g.recipientId} ${g.label ?? '(unknown recipient)'}`); + } + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts index cb3a1d2..a7af986 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import { identityInit, identityShow, identitySetId } from './commands/identity.js'; -import { recipientAdd, recipientList, recipientRemove } from './commands/recipient.js'; +import { recipientAdd, recipientList, recipientRemove, adminOverview } from './commands/recipient.js'; import { vaultPush, vaultPull, vaultGrant, vaultRevoke, vaultLog } from './commands/vault.js'; function flag(args: string[], name: string, fallback?: string): string | undefined { @@ -28,6 +28,8 @@ async function main(): Promise { if (sub === 'remove') return await recipientRemove(rest[0]); } + if (cmd === 'overview') return await adminOverview(); + if (cmd === 'push') return await vaultPush(sub, flag(rest, 'file', '.env')!, boolFlag(rest, 'purge')); if (cmd === 'pull') return await vaultPull(sub, (flag(rest, 'format', 'env') as 'env' | 'json'), flag(rest, 'out'), boolFlag(rest, 'previous')); if (cmd === 'grant') return await vaultGrant(sub, rest[0], boolFlag(rest, 'read-only')); @@ -58,6 +60,7 @@ function printUsage(): void { keep recipient add --label