From 7d1895308da39222c864b21376eed47419011f13 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:21:20 +0200 Subject: [PATCH] fix(docker): make SARIF upload non-blocking; correct seed-admin/build-stage/restore ownership docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final whole-branch review (opus) fixes: - docker-publish.yml: upload-sarif was if:always() but not continue-on-error, so a Trivy SARIF-emit flake would fail the job and SKIP the multi-arch publish. Made it non-blocking so a scanner hiccup never blocks a release. - deployment.md: seed-admin-rotated.txt is root-owned (written via docker exec, which bypasses the gosu drop) — reverted an over-correction. Corrected the canonical runtime-stage build description (no toolchain; non-root gosu). First-boot chown note. - restore.sh: comment ownership root -> uid 1000. --- .github/workflows/docker-publish.yml | 1 + docs/systems/deployment.md | 11 +++++++---- restore.sh | 5 +++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4c5f5faa..b964f8b8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -108,6 +108,7 @@ jobs: - name: Upload Trivy image SARIF if: always() + continue-on-error: true # a scanner/SARIF-emit flake must never skip the publish below uses: github/codeql-action/upload-sarif@02c5e83432fe5497fd85b873b6c9f16a8578e1d9 # v3.37.0 with: sarif_file: trivy-image.sarif diff --git a/docs/systems/deployment.md b/docs/systems/deployment.md index 8abce703..f1536ad3 100644 --- a/docs/systems/deployment.md +++ b/docs/systems/deployment.md @@ -62,7 +62,7 @@ One installer, three modes, recorded as `DEPLOY_MODE` in `.env`. `install.sh` au `Dockerfile` has two stages: 1. **`builder`** (`node:20-slim`) — enables pnpm via corepack, installs the full workspace with `pnpm install --frozen-lockfile`, copies `shared`/`server`/`web` source, and runs `pnpm --filter @backspace/web build` to produce the static frontend (`packages/web/dist`). -2. **`runtime`** (`node:20-slim`) — installs the native toolchain for `better-sqlite3` plus `ffmpeg` (`python3 make g++ ffmpeg`), installs production-only deps with `pnpm install --prod --frozen-lockfile` (`tsx` is a server runtime dependency), copies `shared` + `server` source and the prebuilt `web/dist`, creates `/app/data/uploads`, and starts the server with `node --import tsx/esm src/index.ts` from `/app/packages/server`. +2. **`runtime`** (`node:20-slim`) — installs `ffmpeg` (media) + `gosu` (privilege drop) only — **no C toolchain**, since `better-sqlite3`/`sharp` load prebuilt binaries — installs production-only deps with `pnpm install --prod --frozen-lockfile` (`tsx` is a server runtime dependency), copies `shared` + `server` source and the prebuilt `web/dist`, creates `/app/data/uploads`, and runs the server **as the non-root `node` user** via `docker-entrypoint.sh` (which chowns `/app/data` as root, then `exec gosu node`) with `node --import tsx/esm src/index.ts` from `/app/packages/server`. The server is run through `tsx` (no separate transpile step); TypeScript is executed directly at runtime. @@ -94,8 +94,11 @@ manifests. New installs via `install.sh` (get.docker.com) are fine. **Upgrade note for existing self-hosters:** on the first start of the hardened image, the contents of your host `./data` directory are chowned to uid 1000. This -is expected and idempotent. If you previously accessed `./data` on the host as a -different user, adjust host-side access accordingly. `./restore.sh` continues to +is expected and idempotent. On an instance with a large `uploads/` tree on slow +storage (e.g. a Pi on SD), the **first** restart after upgrade may take noticeably +longer as this one-time chown runs before the server starts; subsequent boots only +touch not-yet-node-owned entries and are near-instant. If you previously accessed +`./data` on the host as a different user, adjust host-side access accordingly. `./restore.sh` continues to work — it swaps files inside a throwaway root container, and root can rewrite the now uid-1000-owned files. @@ -200,7 +203,7 @@ Behavior (`packages/server/src/scripts/remediate-seed-admin.ts`): - **Targets only the local seed admin** — `username = 'admin'` with `home_instance IS NULL` and `is_admin = 1`. Replicated/federated users are never touched. - **Rotates only `admin123`.** It verifies the current hash against `admin123`; if the password has already been changed, it is a **no-op** ("nothing to do"). It is fully idempotent — safe to run repeatedly. - **Never deletes** the account (the default-space ownership constraint above). -- On rotation it generates a 24-character random password, updates the hash, prints the new password to stdout, **and** writes it to `data/seed-admin-rotated.txt` (mode `0600`, owned by the container's runtime user uid 1000 via the bind-mount). **Store the password somewhere safe, then delete `data/seed-admin-rotated.txt`.** +- On rotation it generates a 24-character random password, updates the hash, prints the new password to stdout, **and** writes it to `data/seed-admin-rotated.txt` (mode `0600`, **root-owned** — the script runs via `docker exec`, which bypasses the entrypoint's gosu drop and runs as root, so this file is uid 0 until the next container restart re-chowns it). **Store the password somewhere safe, then delete `data/seed-admin-rotated.txt`** (a non-root host user may need `sudo`). > **Note — sessions are not invalidated.** Rotation changes the stored password hash only; it does **not** revoke existing JWTs. An already-logged-in admin session survives until the token expires (`JWT_EXPIRES_IN`, default 30 days). Rotation closes off *future* logins with the old password; it does not eject a currently active session. If you must terminate live sessions immediately, rotate `JWT_SECRET` (which invalidates **all** tokens instance-wide) and restart. diff --git a/restore.sh b/restore.sh index 5fcd976f..a32e5f94 100755 --- a/restore.sh +++ b/restore.sh @@ -47,8 +47,9 @@ read -rp "Continue? [y/N] " yn echo "[1/3] Stopping backspace container..." docker compose stop backspace -# data/backspace.db and data/backups/ are container-owned (root). The host user cannot -# cp/rm them directly, so do the swap inside a throwaway root container that mounts data/. +# data/backspace.db and data/backups/ are container-owned (uid 1000, the non-root runtime +# user). The host user cannot cp/rm them directly, so do the swap inside a throwaway root +# container that mounts data/ (root can rewrite the uid-1000-owned files). # (youruser is in the docker group on both boxes — no sudo prompt.) TS="$(date -u +%Y%m%dT%H%M%S)" echo "[2/3] Swapping DB inside a root container (pre-restore copy + WAL clear + install)..."