From a758c9d78b396467d038f81a308d073a13278612 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Fri, 31 Jul 2026 14:39:35 +0200 Subject: [PATCH] #219: verified reproducible build without network access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADR 0024 §4 decision, taken explicitly and both ways: customers OPERATE prebuilt digest-pinned images (no customer-side build), and ADDITIONALLY the workspace build is verified to work with networking disabled - so site-local patching stays possible without internet. Evidence (docs/vs-nfd/96-offline-build-protokoll.md): pnpm install --offline --frozen-lockfile plus pnpm build under docker run --network none (node:22.15.1-alpine + pnpm 11.9.0, the pinned toolchain), reproduced twice from clean checkouts with identical results. The offline kit is the pnpm store (~870 MB) plus the build user's ~/.cache (~460 MB - the prisma engines live there; without the cache the prisma postinstall fails offline). The one network dependency found and bounded: the drawio plugin's installable ZIP fetches its pinned vendor tarball on first build. Deploy images contain no plugin ZIPs, so the delivery-relevant build is fully offline (CI=1 skips the fetch, as in CI); an offline ZIP build pre-seeds the tarball into packages/plugins/drawio/vendor/. Also catches up the operations manual's scheduler-job table to 10 (read-trail-maintenance was added in #224 without the row here). Refs #219. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 --- .../0024-reproducible-offline-deployment.md | 21 +++++ docs/vs-nfd/20-massnahmenplan.md | 2 +- docs/vs-nfd/70-betriebshandbuch.md | 39 ++++++---- docs/vs-nfd/96-offline-build-protokoll.md | 78 +++++++++++++++++++ 4 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 docs/vs-nfd/96-offline-build-protokoll.md diff --git a/docs/architecture/adr/0024-reproducible-offline-deployment.md b/docs/architecture/adr/0024-reproducible-offline-deployment.md index 516cc0d..aaf9d54 100644 --- a/docs/architecture/adr/0024-reproducible-offline-deployment.md +++ b/docs/architecture/adr/0024-reproducible-offline-deployment.md @@ -41,6 +41,27 @@ documented mirror or update path. bundle, verify by digest, back up, apply, verify, roll back — with the irreversibility of migrations stated explicitly. +## Decisions taken in #219 + +- **Both paths, with stated roles**: customers OPERATE prebuilt, + digest-pinned images (the CI release build; mirrored per #218) — no + customer-side build for operation. ADDITIONALLY the workspace build is + verified to work with networking disabled, so site-local patching + remains possible (open source as an audit advantage) — the "no local + patching" consequence therefore does not apply. +- **The offline kit** is the pnpm store plus the build user's `~/.cache` + (the prisma postinstall pulls its engines from there instead of the + network) — both filled by one online `pnpm install --frozen-lockfile`. +- **Evidence**: `pnpm install --offline --frozen-lockfile` and + `pnpm build` under `docker run --network none` (node:22.15.1-alpine, + pnpm 11.9.0 — the pinned toolchain), reproduced twice from clean + checkouts (`docs/vs-nfd/96-offline-build-protokoll.md`). +- **The one network dependency found**: the drawio plugin's installable + ZIP fetches its pinned vendor tarball on first build. Deploy images + contain no plugin ZIPs, so the delivery-relevant build is fully + offline (`CI=1` skips the fetch, as in CI); for an offline ZIP build + the tarball is pre-seeded into `packages/plugins/drawio/vendor/`. + ## Consequences - Digest pinning creates recurring maintenance: security updates now diff --git a/docs/vs-nfd/20-massnahmenplan.md b/docs/vs-nfd/20-massnahmenplan.md index 36bcb44..03c6d9f 100644 --- a/docs/vs-nfd/20-massnahmenplan.md +++ b/docs/vs-nfd/20-massnahmenplan.md @@ -71,7 +71,7 @@ gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung - [x] Alle Images auf Digest pinnen (schließt den `gotenberg:8`-Punkt ein) · 1 AT · #203 - [x] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218 -- [ ] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine +- [x] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine Prebuilt-Images) · 2–3 AT · #219 - [ ] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220 - [ ] Offline-Update-Pfad inkl. Migrationen · 2–3 AT · #221 diff --git a/docs/vs-nfd/70-betriebshandbuch.md b/docs/vs-nfd/70-betriebshandbuch.md index ea19299..5506352 100644 --- a/docs/vs-nfd/70-betriebshandbuch.md +++ b/docs/vs-nfd/70-betriebshandbuch.md @@ -57,10 +57,16 @@ dokumentiert die realen Instanzen). auf den Spiegel zeigen. Vollständige Prozedur: `deploy/stages.md` §5b; Ausführungsnachweis: `95-mirror-protokoll.md`. -- ⏳ offen: netzloser Build (#219), Testlauf in isolierter Umgebung - (#220), Offline-Update-Pfad (#221); Meilenstein M28. Bereits vorhanden - als Grundlage: alle Dritt-Images digest-gepinnt (#203), ein - authoritativer Node-Pin (#236), SBOMs je Release (#202). +- **Netzloser Build (#219): ✅ verifiziert und entschieden.** Betrieb + läuft aus Prebuilt-Images; zusätzlich ist der Workspace-Build ohne + Netz zweifach reproduziert (Offline-Kit = pnpm-Store + Build-Cache; + einzige Grenze: das installierbare drawio-Plugin-ZIP braucht sein + vorab abgelegtes Vendor-Tarball). Entscheidung und Prozedur: + ADR 0024 §Decisions, Nachweis: `96-offline-build-protokoll.md`. +- ⏳ offen: Testlauf in isolierter Umgebung (#220), Offline-Update-Pfad + (#221); Meilenstein M28. Bereits vorhanden als Grundlage: alle + Dritt-Images digest-gepinnt (#203), ein authoritativer Node-Pin + (#236), SBOMs je Release (#202). ## 2 Update und Rollback @@ -120,19 +126,20 @@ Referenz: `docs/operations/restore-runbook.md` (maßgeblich), Alle Jobs laufen in der api (in-app Scheduler, `jobs`-Tabelle), sind im Site-Admin-Systempanel sichtbar und dort manuell auslösbar (auditiert -als `job.triggered`). Aktueller Bestand (9): +als `job.triggered`). Aktueller Bestand (10): -| Job | Rhythmus | Wirkung | -| -------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `trash-purge` | täglich | endgültiges Löschen abgelaufener Papierkorb-Seiten **und** -Teiche (`trash.retentionDays`, #31/#193) | -| `orphan-file-sweep` | täglich | verwaiste Uploads entfernen (#194); trägt nachts auch den SHA-256-Backfill (#199) | -| `page-compaction` | stündlich | `page_updates`-Log in den Zustand mergen | -| `version-thinning` | täglich | automatische Versions-Snapshots ausdünnen | -| `audit-retention` | täglich | `audit_log` nach `audit.retentionDays` beschneiden (#196), Lücke selbst auditiert (`audit.pruned`) | -| `conversion-payload-prune` | täglich | Import-/Export-Payloads fertiger Jobs nullen (#233) | -| `mail-outbox-retention` | täglich | SENT/endgültig FAILED Outbox-Zeilen löschen (#234) | -| `data-export-purge` | stündlich | abgelaufene DSGVO-Datenexporte entfernen (#68) | -| `notification-digest` | alle 15 min (Versand nach Nutzer-Präferenz) | Benachrichtigungs-Digests versenden | +| Job | Rhythmus | Wirkung | +| -------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `trash-purge` | täglich | endgültiges Löschen abgelaufener Papierkorb-Seiten **und** -Teiche (`trash.retentionDays`, #31/#193) | +| `orphan-file-sweep` | täglich | verwaiste Uploads entfernen (#194); trägt nachts auch den SHA-256-Backfill (#199) | +| `page-compaction` | stündlich | `page_updates`-Log in den Zustand mergen | +| `version-thinning` | täglich | automatische Versions-Snapshots ausdünnen | +| `audit-retention` | täglich | `audit_log` nach `audit.retentionDays` beschneiden (#196), Lücke selbst auditiert (`audit.pruned`) | +| `conversion-payload-prune` | täglich | Import-/Export-Payloads fertiger Jobs nullen (#233) | +| `mail-outbox-retention` | täglich | SENT/endgültig FAILED Outbox-Zeilen löschen (#234) | +| `data-export-purge` | stündlich | abgelaufene DSGVO-Datenexporte entfernen (#68) | +| `notification-digest` | alle 15 min (Versand nach Nutzer-Präferenz) | Benachrichtigungs-Digests versenden | +| `read-trail-maintenance` | täglich | Lesetrail-Partitionen vorausanlegen + `read_events` nach `readTrail.retentionDays` beschneiden (#224, auditiert `read_trail.pruned`) | Der e2e-Zaun `apps/web/e2e/system.spec.ts` pinnt diese Zahl — ein neuer Job ohne Handbuch-/Zaun-Anpassung wird rot. diff --git a/docs/vs-nfd/96-offline-build-protokoll.md b/docs/vs-nfd/96-offline-build-protokoll.md new file mode 100644 index 0000000..c7dc416 --- /dev/null +++ b/docs/vs-nfd/96-offline-build-protokoll.md @@ -0,0 +1,78 @@ +# Offline-Build-Protokoll — Build ohne Netzzugang (Issue #219) + +Nachweis für die in ADR 0024 §4 geforderte, ausdrücklich getroffene +Entscheidung zur Reproduzierbarkeit ohne Netz. Belegstufe: **live +verifiziert** — zweifach aus sauberem Checkout reproduziert. + +- **Datum:** 2026-07-31 +- **Toolchain (gepinnt):** Container `node:22.15.1-alpine` (identisch + zum Basis-Image der Deploy-Images, Node-Pin #236), pnpm `11.9.0` + (`packageManager`-Feld in `package.json`, via corepack aktiviert). +- **Netzisolation:** `docker run --network none` — der Build-Container + hat keinerlei Egress; jede Netzabhängigkeit schlägt hart fehl. + +## Entscheidung (ADR 0024 §4) + +**Beides, mit klarer Rollenverteilung:** + +1. **Ausgelieferte Instanzen laufen aus Prebuilt-Images** (Release-Build + der CI, digest-gepinnt, gespiegelt nach `95-mirror-protokoll.md`) — + der Kunde baut für den Betrieb nichts selbst. +2. **Zusätzlich ist der Workspace-Build ohne Netz verifiziert** — die + Behörde KANN lokal patchen (Quelloffenheit als Prüfvorteil), ohne + dass dafür Internetzugang nötig wäre. + +## Offline-Kit + +Einmalig auf einem Rechner **mit** Netz erzeugt: + +| Bestandteil | Erzeugung | Inhalt | +| ------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------- | +| pnpm-Store (~870 MB) | `pnpm install --frozen-lockfile` mit `store-dir`-Mount | alle Pakete exakt nach `pnpm-lock.yaml` | +| `~/.cache` des Build-Users (~460 MB) | fällt beim selben Install an | u. a. die Prisma-Engines, die das prisma-Postinstall sonst nachlädt | +| optional: `drawio-30.3.6.tar.gz` | einmalig von der gepinnten GitHub-Release-URL | nur für das installierbare drawio-Plugin-ZIP nötig (s. u.) | + +## Prozedur (offline) + +```sh +# sauberer Checkout + Offline-Kit gemountet, KEIN Netz: +docker run --rm --network none -e CI=1 \ + -v :/work -v :/pnpm-store -v :/root/.cache \ + -w /work node:22.15.1-alpine sh -c ' + corepack enable && corepack prepare pnpm@11.9.0 --activate + pnpm config set store-dir /pnpm-store + pnpm install --offline --frozen-lockfile + pnpm build' +``` + +## Ergebnis + +| Lauf | Checkout | `pnpm install --offline` | `pnpm build` | Artefakte geprüft | +| ---- | -------------------- | ------------------------ | ------------ | --------------------------------------------------- | +| 1 | frisch (git archive) | OK (11,8 s) | Exit 0 | `apps/api/dist/main.js`, `apps/web/dist/index.html` | +| 2 | frisch (git archive) | OK | Exit 0 | identisch vorhanden | + +## Befund und bewusste Grenze + +- **Einzige Netzabhängigkeit im Build:** das drawio-Plugin holt beim + ersten Bau seines **installierbaren ZIPs** das gepinnte + draw.io-Release-Tarball (v30.3.6) von GitHub nach `vendor/`. Ohne Netz + und ohne `CI=1` schlägt genau dieser Schritt fehl (beobachtet, Lauf 0 + dieses Protokolls). +- **Einordnung:** Die Deploy-Images enthalten keine Plugin-ZIPs — + Plugins werden zur Laufzeit über die Dropzone installiert. Der + auslieferungsrelevante Build ist damit vollständig offline; `CI=1` + (der auch in der CI gilt) überspringt den Vendor-Fetch und baut das + Controller-Bundle. Wer das drawio-ZIP offline bauen will, legt das + Tarball aus dem Offline-Kit vorab als + `packages/plugins/drawio/vendor/drawio-30.3.6.tar.gz` ab — der Build + lädt nur, was nicht schon dort liegt. +- Prisma-Engines kommen offline aus `~/.cache` — der Cache gehört + deshalb fest zum Kit (ohne ihn schlägt das prisma-Postinstall fehl). + +## Konsequenz „kein lokales Patchen"? + +Entfällt — mit dem Offline-Kit ist lokales Patchen möglich. Wer es +organisatorisch ausschließen will, betreibt nur die Prebuilt-Images; +beides ist dokumentiert und die Wahl liegt beim Betreiber +(Betriebshandbuch §1). -- 2.45.2