docs(docker): document non-root runtime, data-volume migration, and image scan
This commit is contained in:
@@ -73,6 +73,32 @@ The server is run through `tsx` (no separate transpile step); TypeScript is exec
|
|||||||
|
|
||||||
Empty/unset → `config.commit` is `null` (local dev, tarball install, or git unavailable). The source URL itself is `config.sourceCodeUrl` (env `BACKSPACE_SOURCE_URL`, default upstream) — operators running a modified build MUST set it to their fork.
|
Empty/unset → `config.commit` is `null` (local dev, tarball install, or git unavailable). The source URL itself is `config.sourceCodeUrl` (env `BACKSPACE_SOURCE_URL`, default upstream) — operators running a modified build MUST set it to their fork.
|
||||||
|
|
||||||
|
### Container hardening (non-root)
|
||||||
|
|
||||||
|
The runtime image runs as the unprivileged `node` user (uid 1000), not root. On
|
||||||
|
container start, `docker-entrypoint.sh` runs as root only long enough to `chown`
|
||||||
|
the `./data` bind mount to `node` (only entries not already node-owned, so it is
|
||||||
|
near-instant after the first boot), then drops privileges via `gosu` and execs the
|
||||||
|
server. The build toolchain (`python3`/`make`/`g++`) is not installed in the
|
||||||
|
runtime stage — `better-sqlite3` and `sharp` load from prebuilt binaries — which
|
||||||
|
shrinks the runtime attack surface. `ffmpeg` remains (a real runtime dependency).
|
||||||
|
|
||||||
|
The published image carries an SBOM and SLSA provenance attestation, and the
|
||||||
|
amd64 image is scanned by Trivy before publish (report-only). Note: only the
|
||||||
|
amd64 image is scanned; the arm64 image is published unscanned.
|
||||||
|
|
||||||
|
**Minimum Docker version:** the attestation-bearing multi-arch image requires a
|
||||||
|
reasonably modern Docker to `pull` cleanly (Docker Engine 24+ recommended).
|
||||||
|
Very old daemons (≤ 20.10) may mishandle the `unknown/unknown` attestation
|
||||||
|
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
|
||||||
|
work — it swaps files inside a throwaway root container, and root can rewrite the
|
||||||
|
now uid-1000-owned files.
|
||||||
|
|
||||||
### Run: `docker compose up -d --build`
|
### Run: `docker compose up -d --build`
|
||||||
|
|
||||||
`docker-compose.yml` defines:
|
`docker-compose.yml` defines:
|
||||||
@@ -167,7 +193,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.
|
- **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.
|
- **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).
|
- **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`, root-owned 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`, 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`.**
|
||||||
|
|
||||||
> **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.
|
> **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.
|
||||||
|
|
||||||
@@ -250,7 +276,7 @@ They do **not** protect against **hardware loss** (disk failure, the box being d
|
|||||||
|
|
||||||
## 4. Restore
|
## 4. Restore
|
||||||
|
|
||||||
Restores are driven by `./restore.sh` from the host. Because `data/` (including `backspace.db` and `data/backups/`) is **container-owned (root)** via the bind-mount, the host user cannot rewrite those files directly — so the actual swap runs inside a throwaway root `alpine` container that mounts `data/`.
|
Restores are driven by `./restore.sh` from the host. Because `data/` (including `backspace.db` and `data/backups/`) is **container-owned (uid 1000)** via the bind-mount, the host user cannot rewrite those files directly — so the actual swap runs inside a throwaway root `alpine` container that mounts `data/`.
|
||||||
|
|
||||||
### List snapshots
|
### List snapshots
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ later change once the remediation pass has cleared the backlog.
|
|||||||
| `.github/workflows/codeql.yml` | CodeQL SAST (`javascript-typescript`, build-mode none) | PR + push main + weekly | Security tab |
|
| `.github/workflows/codeql.yml` | CodeQL SAST (`javascript-typescript`, build-mode none) | PR + push main + weekly | Security tab |
|
||||||
| `.github/workflows/security.yml` | gitleaks (secrets, full history), OSV-Scanner (deps), Trivy config (IaC), Trivy license | PR + push main + weekly | Security tab |
|
| `.github/workflows/security.yml` | gitleaks (secrets, full history), OSV-Scanner (deps), Trivy config (IaC), Trivy license | PR + push main + weekly | Security tab |
|
||||||
| `.github/workflows/scorecard.yml` | OpenSSF Scorecard (repo posture) | push main + weekly + on branch-protection change | Security tab + public badge |
|
| `.github/workflows/scorecard.yml` | OpenSSF Scorecard (repo posture) | push main + weekly + on branch-protection change | Security tab + public badge |
|
||||||
|
| `.github/workflows/docker-publish.yml` | Image scan (Trivy) + SBOM + provenance for the published container | tag push / manual | image scan (report-only) + SBOM + provenance |
|
||||||
|
|
||||||
> **gitleaks findings** surface in the workflow's job log and PR summary — the
|
> **gitleaks findings** surface in the workflow's job log and PR summary — the
|
||||||
> `gitleaks` job does not upload SARIF, so secret hits do **not** appear under
|
> `gitleaks` job does not upload SARIF, so secret hits do **not** appear under
|
||||||
@@ -36,8 +37,10 @@ merge-blocking, Dependabot alerts, and native secret-scanning are GitHub *settin
|
|||||||
tag-move attacks and satisfies Scorecard's Pinned-Dependencies check.
|
tag-move attacks and satisfies Scorecard's Pinned-Dependencies check.
|
||||||
- `step-security/harden-runner` (egress-policy `audit`) on Linux jobs.
|
- `step-security/harden-runner` (egress-policy `audit`) on Linux jobs.
|
||||||
- Least-privilege `permissions:` per workflow/job.
|
- Least-privilege `permissions:` per workflow/job.
|
||||||
- SBOM + SLSA provenance **will be** attached to the published container image
|
- SBOM + SLSA provenance are attached to the published container image at push
|
||||||
(added with the container-image-scan work in a later plan — not yet live).
|
(`.github/workflows/docker-publish.yml`), alongside a report-only Trivy scan
|
||||||
|
of the amd64 image (the arm64 image ships unscanned; enforcement is turned
|
||||||
|
on in a later plan).
|
||||||
|
|
||||||
## Maintainer checklist (one-time GitHub settings — NOT code)
|
## Maintainer checklist (one-time GitHub settings — NOT code)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user