Skip to content

feat(packaging): add Debian (.deb) build via nfpm with systemd unit (v2)#7583

Open
JohnMcLear wants to merge 1 commit intoether:developfrom
JohnMcLear:chore/packaging-apt-v2
Open

feat(packaging): add Debian (.deb) build via nfpm with systemd unit (v2)#7583
JohnMcLear wants to merge 1 commit intoether:developfrom
JohnMcLear:chore/packaging-apt-v2

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Reintroduces the Debian packaging from #7559 (reverted in #7582) with two targeted fixes flagged during review:

  1. Rename etherpad-liteetherpad everywhere — package name, installed paths (/opt/etherpad, /etc/etherpad, /var/lib/etherpad, /var/log/etherpad), systemd unit (etherpad.service), CLI wrapper (/usr/bin/etherpad), Documentation= URL, and the .deb filename (etherpad_<ver>_<arch>.deb). Matches the repo rename from ether/etherpad-lite to ether/etherpad. Kept replaces/conflicts/provides: etherpad-lite so any dev builds of the reverted 7559 artefact upgrade cleanly.
  2. Default DB → sqlite (was dirty). The shipped settings.json.template has always warned "You shouldn't use 'dirty' for anything else than testing", and a Debian package is exactly the "not testing" case. postinstall rewrites the seeded /etc/etherpad/settings.json to dbType: "sqlite" pointed at /var/lib/etherpad/etherpad.db. sqlite is already a transitive dep via ueberdb2rusty-store-kv, so no new apt dependencies.

Everything else matches the post-review state of #7559 (smoke-test exits non-zero on /health timeout, /etc/default/etherpad is 0640 root:etherpad, 2-space indent).

Installing the package gives users:

  • /opt/etherpad with a prebuilt, self-contained node_modules/ — no pnpm required at runtime, only nodejs (>= 20).
  • etherpad system user/group, created via adduser in preinst.
  • /etc/etherpad/settings.json (seeded from the template on first install with sqlite default; preserved across upgrades; removed on purge).
  • /var/lib/etherpad owned by etherpad:etherpad, writable under ProtectSystem=strict.
  • /lib/systemd/system/etherpad.service — hardened unit (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies) with Restart=on-failure.
  • /usr/bin/etherpad CLI wrapper running node --import tsx/esm.

Part of #7529 — top-3 deployment targets (Snap #7558, Apt, Home Assistant).

CI

.github/workflows/deb-package.yml triggers on v* tags, builds both arches via native runners (ubuntu-latest + ubuntu-24.04-arm), and smoke-tests the amd64 package end-to-end:

  1. dpkg -i installs
  2. Asserts /etc/etherpad/settings.json contains "dbType": "sqlite" (guards against accidental dirty-default regressions)
  3. systemctl start etherpad
  4. curl /health returns 200 (exits non-zero + dumps journalctl if it never becomes healthy)
  5. dpkg --purge removes config + user

Artefacts are attached to the GitHub Release.

Not included (follow-up)

Publishing to an APT repo (Cloudsmith / Launchpad PPA / self-hosted reprepro) is out of scope — needs a governance decision on who holds the signing key. Recipes are in packaging/README.md to be wired in once that's decided.

Legacy

bin/buildDebian.sh and bin/deb-src/ are stale (Etherpad v1.3, init-based, unmaintained). Flagged for removal in a follow-up PR so this one stays mechanical.

Test plan

  • pnpm install --frozen-lockfile && pnpm run build:etherpad succeeds
  • Local nfpm package --packager deb produces a well-formed .deb (dpkg-deb -I / -c)
  • Fresh install on Ubuntu 24.04: service starts, /health returns OK, /etc/etherpad/settings.json shows "dbType": "sqlite", /var/lib/etherpad/etherpad.db is created by the etherpad user
  • Upgrade install: /etc/etherpad/settings.json untouched; service restarted
  • apt remove keeps /etc and /var/lib; apt purge removes them plus the etherpad user
  • CI workflow succeeds on amd64 and arm64 (the workflow itself runs the smoke test)

Refs #7529, #7559, #7582

🤖 Generated with Claude Code

First-class Debian packaging for Etherpad, producing
etherpad_<version>_<arch>.deb artefacts for amd64 and arm64 from a
single nfpm manifest. Installing the package gives users:

- /opt/etherpad with a prebuilt, self-contained node_modules/ — no
  pnpm required at runtime, just `nodejs (>= 20)`.
- etherpad system user/group, created via `adduser` in preinst.
- /etc/etherpad/settings.json seeded from the template on first
  install, preserved across upgrades, removed on `purge`. Seed rewrites
  dbType from the template's dev-only `dirty` default to `sqlite`,
  pointed at /var/lib/etherpad/etherpad.db so fresh installs get an
  ACID-safe DB without manual config. sqlite is shipped by ueberdb2
  (rusty-store-kv), so no additional apt deps are needed.
- /var/lib/etherpad owned by etherpad:etherpad, writable under the
  hardened unit's ProtectSystem=strict.
- /lib/systemd/system/etherpad.service — hardened unit
  (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
  RestrictAddressFamilies) with Restart=on-failure.
- /usr/bin/etherpad CLI wrapper running `node --import tsx/esm`.

CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both
arches via native runners (ubuntu-latest + ubuntu-24.04-arm),
smoke-tests the amd64 package end-to-end (install → verify sqlite
default → systemctl start → curl /health → purge → confirm user
removed), and attaches the artefacts to the GitHub Release.

Re-introduces the work from ether#7559 (reverted in ether#7582) with two
corrections:

1. Package name and all installed paths use `etherpad`, not
   `etherpad-lite` — matches the repo rename. Kept replaces/conflicts
   on `etherpad-lite` so any dev builds of the reverted PR upgrade
   cleanly.
2. Default dbType is `sqlite`, not `dirty`. The template's own comment
   says dirty is for testing only; shipping it by default to everyone
   who runs `apt install etherpad` is the wrong tradeoff for a
   production package.

Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted
reprepro) is intentionally out of scope — needs a governance decision
on who holds the signing key. Recipes are documented in
packaging/README.md.

Refs ether#7529, ether#7559, ether#7582

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnMcLear
Copy link
Copy Markdown
Member Author

/review

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 22, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Plugin migration startup failure 🐞 Bug ☼ Reliability ⭐ New
Description
On fresh installs, Etherpad startup runs checkForMigration(), which falls back to running pnpm ls
and then writing /opt/etherpad/var/installed_plugins.json if the file is missing; the package does
not ship/create that file, and the systemd unit disallows writes under /opt/etherpad. This can
cause the service to crash during startup and never reach /health.
Code

.github/workflows/deb-package.yml[R73-85]

+      - name: Stage tree for packaging
+        run: |
+          set -eux
+          STAGE=staging/opt/etherpad
+          mkdir -p "${STAGE}"
+
+          # Production footprint = src/ + bin/ + node_modules/ + metadata.
+          cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \
+                node_modules "${STAGE}/"
+
+          # Make pnpm-workspace.yaml production-only (same trick Dockerfile uses).
+          printf 'packages:\n  - src\n  - bin\n' > "${STAGE}/pnpm-workspace.yaml"
+
Evidence
The packaging workflow stages only src/, bin/, root metadata, and node_modules/ into the
/opt/etherpad tree, so /opt/etherpad/var/installed_plugins.json is not present in the resulting
package. At runtime, Etherpad’s server startup always calls checkForMigration(), which runs `pnpm
ls if installed_plugins.json is missing and then writes the file under settings.root/var`; with
the packaged layout settings.root resolves to /opt/etherpad. The systemd unit uses
ProtectSystem=strict and only whitelists /var/lib/etherpad, /var/log/etherpad, and
/etc/etherpad as writable, so writing to /opt/etherpad/var is not permitted even if the
directory existed.

.github/workflows/deb-package.yml[73-85]
packaging/nfpm.yaml[47-59]
packaging/scripts/postinstall.sh[14-40]
packaging/systemd/etherpad.service[22-44]
packaging/bin/etherpad[13-18]
src/node/server.ts[151-176]
src/static/js/pluginfw/installer.ts[25-90]
src/static/js/pluginfw/installer.ts[122-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Fresh installs can fail because Etherpad calls `checkForMigration()` during startup, and if `/opt/etherpad/var/installed_plugins.json` is missing it tries to execute `pnpm ls` and write that file under `/opt/etherpad/var`. The .deb packaging currently doesn’t ship/create that file (or even `/opt/etherpad/var`), and the systemd unit doesn’t permit writes under `/opt/etherpad`.

### Issue Context
- Packaging stages `/opt/etherpad` from a curated file list.
- Etherpad’s plugin migration logic treats absence of `var/installed_plugins.json` as a trigger to run `pnpm` and then persist plugin state under `settings.root/var`.
- The systemd sandbox only allows writes to `/var/lib/etherpad`, `/var/log/etherpad`, and `/etc/etherpad`.

### Fix Focus Areas
- .github/workflows/deb-package.yml[73-85]
- packaging/nfpm.yaml[47-59]
- packaging/scripts/postinstall.sh[14-40]
- packaging/systemd/etherpad.service[22-44]

### Suggested fix approach
1. Ensure `/opt/etherpad/var/` exists in the packaged tree (or create it in `postinstall.sh`).
2. Seed `/opt/etherpad/var/installed_plugins.json` at install time with minimal valid content (for example, only `ep_etherpad-lite`), so `checkForMigration()` does not attempt to run `pnpm` on first boot.
3. If you want runtime plugin install/uninstall to work under the hardened unit, relocate plugin state to `/var/lib/etherpad` (and symlink `/opt/etherpad/var` to it), and/or extend `ReadWritePaths` to include the required writable plugin directories.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Readonly /opt breaks startup 🐞 Bug ☼ Reliability
Description
The unit sets ProtectSystem=strict but Etherpad writes runtime files under settings.root/var during
startup (for example var/js and var/installed_plugins.json). Because /opt/etherpad/var is neither
created/redirected in postinstall nor included in ReadWritePaths, a fresh install can fail to start
with mkdir/write errors under /opt/etherpad/var.
Code

packaging/systemd/etherpad.service[R22-44]

+# --- Sandboxing ---------------------------------------------------------
+NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=true
+PrivateDevices=true
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectKernelLogs=true
+ProtectControlGroups=true
+ProtectHostname=true
+ProtectClock=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+RestrictNamespaces=true
+LockPersonality=true
+MemoryDenyWriteExecute=false     # Node's JIT needs W+X mappings
+SystemCallArchitectures=native
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
+UMask=0027
+
+ReadWritePaths=/var/lib/etherpad /var/log/etherpad /etc/etherpad
+
Evidence
The service sandbox makes the filesystem read-only except for explicit exceptions, but Etherpad’s
startup path writes to <root>/var. With the Debian layout, Etherpad’s computed root is the directory
above src (so /opt/etherpad), making those writes target /opt/etherpad/var which is not writable per
the unit and not redirected by the packaging scripts.

packaging/systemd/etherpad.service[22-44]
packaging/scripts/postinstall.sh[34-40]
src/node/utils/AbsolutePaths.ts[77-93]
src/node/server.ts[173-175]
src/static/js/pluginfw/installer.ts[25-29]
src/static/js/pluginfw/installer.ts[136-148]
src/node/hooks/express/specialpages.ts[310-314]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The systemd unit makes `/opt/etherpad` effectively read-only (`ProtectSystem=strict` + `ReadWritePaths` omits `/opt/etherpad/...`), but Etherpad writes to `path.join(settings.root, 'var', ...)` during startup, where `settings.root` resolves to `/opt/etherpad` in this package layout. This can prevent `etherpad.service` from starting.
### Issue Context
Etherpad startup awaits `checkForMigration()` which can write `${root}/var/installed_plugins.json`, and the express hooks create `${root}/var/js`. The package currently does not create or redirect `/opt/etherpad/var` to a writable location.
### Fix Focus Areas
- packaging/scripts/postinstall.sh[34-40]
- packaging/systemd/etherpad.service[22-44]
### Suggested fix
Implement one of the following (prefer the symlink approach to keep writes out of `/opt`):
1) **Symlink `/opt/etherpad/var` to a writable location**:
 - In `postinstall.sh` (configure):
   - `mkdir -p /var/lib/etherpad/var`
   - `ln -sfn /var/lib/etherpad/var /opt/etherpad/var`
   - `chown -R etherpad:etherpad /var/lib/etherpad/var`
 - This keeps `ReadWritePaths=/var/lib/etherpad` sufficient.
2) **Allow `/opt/etherpad/var` writes explicitly**:
 - Create `/opt/etherpad/var` in `postinstall.sh` and `chown` it to `etherpad:etherpad`.
 - Add `/opt/etherpad/var` to `ReadWritePaths` in the unit.
Either way, ensure the directory exists before service start so `fs.mkdirSync(.../var/js)` and plugin migration writes don’t throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Unverified nfpm download 🐞 Bug ⛨ Security ⭐ New
Description
The CI workflow downloads an external nfpm .deb via curl and installs it with dpkg -i without
verifying a checksum or signature. If the download is tampered with, arbitrary code can run in the
CI job with sudo privileges.
Code

.github/workflows/deb-package.yml[R64-72]

+      - name: Install nfpm
+        run: |
+          set -e
+          NFPM_ARCH=amd64
+          [ "${{ matrix.arch }}" = "arm64" ] && NFPM_ARCH=arm64
+          curl -fsSL -o /tmp/nfpm.deb \
+            "https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}/nfpm_${NFPM_VERSION#v}_${NFPM_ARCH}.deb"
+          sudo dpkg -i /tmp/nfpm.deb
+
Evidence
The workflow fetches the nfpm installer package directly from a URL into /tmp and installs it
immediately; there is no checksum verification step between download and dpkg -i.

.github/workflows/deb-package.yml[64-72]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The workflow installs nfpm from a remote `.deb` without validating integrity.

### Issue Context
This is a supply-chain risk because the job runs `dpkg -i` with sudo immediately after download.

### Fix Focus Areas
- .github/workflows/deb-package.yml[64-72]

### Suggested fix approach
- Download the official checksum file from the same GitHub release and verify the `.deb` with `sha256sum -c` before installing.
- Alternatively, use a trusted installer mechanism that performs verification (or pin to a vetted artifact and validate its digest).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. nfpm download not verified 🐞 Bug ⛨ Security
Description
The release workflow downloads an nfpm .deb via curl and installs it with dpkg without verifying a
checksum or signature. If the upstream artifact is tampered with, the workflow could produce
compromised release packages.
Code

.github/workflows/deb-package.yml[R64-71]

+      - name: Install nfpm
+        run: |
+          set -e
+          NFPM_ARCH=amd64
+          [ "${{ matrix.arch }}" = "arm64" ] && NFPM_ARCH=arm64
+          curl -fsSL -o /tmp/nfpm.deb \
+            "https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}/nfpm_${NFPM_VERSION#v}_${NFPM_ARCH}.deb"
+          sudo dpkg -i /tmp/nfpm.deb
Evidence
The workflow directly installs a remotely fetched .deb into the runner without any integrity
verification step (checksum/GPG).

.github/workflows/deb-package.yml[64-71]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The workflow installs nfpm from a downloaded `.deb` without any integrity verification.
### Issue Context
Even with HTTPS, this lacks defense-in-depth against compromised upstream release artifacts.
### Fix Focus Areas
- .github/workflows/deb-package.yml[64-71]
### Suggested fix
Add an integrity verification step before `dpkg -i`, for example:
- Download nfpm’s published `checksums.txt` (or equivalent) for `${NFPM_VERSION}` and verify `/tmp/nfpm.deb` with `sha256sum -c`.
- Alternatively, verify a published signature (GPG/cosign) if available for nfpm release artifacts.
- Only proceed to `sudo dpkg -i` if verification passes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add Debian (.deb) packaging with systemd service and nfpm manifest

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add native Debian (.deb) packaging via nfpm for amd64 and arm64
• Configure systemd service with hardened security settings and auto-restart
• Implement pre/post install/remove scripts for user/group and config management
• Default database to sqlite instead of dev-only dirty mode for production safety
• Add CI workflow to build, smoke-test, and attach packages to GitHub releases
Diagram
flowchart LR
  A["Source code<br/>+ node_modules"] -->|Stage| B["staging/opt/etherpad"]
  B -->|nfpm manifest| C["nfpm.yaml"]
  C -->|Build| D["etherpad_VERSION_ARCH.deb"]
  E["preinstall.sh"] -->|Create user| F["etherpad system user"]
  G["postinstall.sh"] -->|Configure| H["Settings + Symlinks"]
  I["etherpad.service"] -->|Hardened unit| J["systemd service"]
  K["CI workflow"] -->|Tag trigger| L["Build + Test + Release"]
Loading

Grey Divider

File Changes

1. packaging/scripts/preinstall.sh ⚙️ Configuration changes +28/-0

Create etherpad system user and group

packaging/scripts/preinstall.sh


2. packaging/scripts/postinstall.sh ⚙️ Configuration changes +71/-0

Configure directories, rewrite DB default to sqlite

packaging/scripts/postinstall.sh


3. packaging/scripts/preremove.sh ⚙️ Configuration changes +20/-0

Stop systemd service before package removal

packaging/scripts/preremove.sh


View more (7)
4. packaging/scripts/postremove.sh ⚙️ Configuration changes +43/-0

Clean up config, data, and system user on purge

packaging/scripts/postremove.sh


5. packaging/nfpm.yaml ⚙️ Configuration changes +120/-0

Define nfpm manifest for .deb package structure

packaging/nfpm.yaml


6. packaging/bin/etherpad ✨ Enhancement +18/-0

CLI wrapper launching Etherpad via tsx ESM loader

packaging/bin/etherpad


7. packaging/systemd/etherpad.service ⚙️ Configuration changes +48/-0

Hardened systemd unit with security restrictions

packaging/systemd/etherpad.service


8. packaging/systemd/etherpad.default ⚙️ Configuration changes +7/-0

Environment variable defaults for systemd service

packaging/systemd/etherpad.default


9. packaging/README.md 📝 Documentation +95/-0

Document packaging layout, build, install, and upgrade

packaging/README.md


10. .github/workflows/deb-package.yml ⚙️ Configuration changes +164/-0

CI workflow to build, test, and release .deb packages

.github/workflows/deb-package.yml


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 22, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Readonly /opt breaks startup 🐞 Bug ☼ Reliability
Description
The unit sets ProtectSystem=strict but Etherpad writes runtime files under settings.root/var during
startup (for example var/js and var/installed_plugins.json). Because /opt/etherpad/var is neither
created/redirected in postinstall nor included in ReadWritePaths, a fresh install can fail to start
with mkdir/write errors under /opt/etherpad/var.
Code

packaging/systemd/etherpad.service[R22-44]

+# --- Sandboxing ---------------------------------------------------------
+NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=true
+PrivateDevices=true
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectKernelLogs=true
+ProtectControlGroups=true
+ProtectHostname=true
+ProtectClock=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+RestrictNamespaces=true
+LockPersonality=true
+MemoryDenyWriteExecute=false     # Node's JIT needs W+X mappings
+SystemCallArchitectures=native
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
+UMask=0027
+
+ReadWritePaths=/var/lib/etherpad /var/log/etherpad /etc/etherpad
+
Evidence
The service sandbox makes the filesystem read-only except for explicit exceptions, but Etherpad’s
startup path writes to <root>/var. With the Debian layout, Etherpad’s computed root is the directory
above src (so /opt/etherpad), making those writes target /opt/etherpad/var which is not writable per
the unit and not redirected by the packaging scripts.

packaging/systemd/etherpad.service[22-44]
packaging/scripts/postinstall.sh[34-40]
src/node/utils/AbsolutePaths.ts[77-93]
src/node/server.ts[173-175]
src/static/js/pluginfw/installer.ts[25-29]
src/static/js/pluginfw/installer.ts[136-148]
src/node/hooks/express/specialpages.ts[310-314]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The systemd unit makes `/opt/etherpad` effectively read-only (`ProtectSystem=strict` + `ReadWritePaths` omits `/opt/etherpad/...`), but Etherpad writes to `path.join(settings.root, 'var', ...)` during startup, where `settings.root` resolves to `/opt/etherpad` in this package layout. This can prevent `etherpad.service` from starting.

### Issue Context
Etherpad startup awaits `checkForMigration()` which can write `${root}/var/installed_plugins.json`, and the express hooks create `${root}/var/js`. The package currently does not create or redirect `/opt/etherpad/var` to a writable location.

### Fix Focus Areas
- packaging/scripts/postinstall.sh[34-40]
- packaging/systemd/etherpad.service[22-44]

### Suggested fix
Implement one of the following (prefer the symlink approach to keep writes out of `/opt`):
1) **Symlink `/opt/etherpad/var` to a writable location**:
  - In `postinstall.sh` (configure):
    - `mkdir -p /var/lib/etherpad/var`
    - `ln -sfn /var/lib/etherpad/var /opt/etherpad/var`
    - `chown -R etherpad:etherpad /var/lib/etherpad/var`
  - This keeps `ReadWritePaths=/var/lib/etherpad` sufficient.

2) **Allow `/opt/etherpad/var` writes explicitly**:
  - Create `/opt/etherpad/var` in `postinstall.sh` and `chown` it to `etherpad:etherpad`.
  - Add `/opt/etherpad/var` to `ReadWritePaths` in the unit.

Either way, ensure the directory exists before service start so `fs.mkdirSync(.../var/js)` and plugin migration writes don’t throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. nfpm download not verified 🐞 Bug ⛨ Security
Description
The release workflow downloads an nfpm .deb via curl and installs it with dpkg without verifying a
checksum or signature. If the upstream artifact is tampered with, the workflow could produce
compromised release packages.
Code

.github/workflows/deb-package.yml[R64-71]

+      - name: Install nfpm
+        run: |
+          set -e
+          NFPM_ARCH=amd64
+          [ "${{ matrix.arch }}" = "arm64" ] && NFPM_ARCH=arm64
+          curl -fsSL -o /tmp/nfpm.deb \
+            "https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}/nfpm_${NFPM_VERSION#v}_${NFPM_ARCH}.deb"
+          sudo dpkg -i /tmp/nfpm.deb
Evidence
The workflow directly installs a remotely fetched .deb into the runner without any integrity
verification step (checksum/GPG).

.github/workflows/deb-package.yml[64-71]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The workflow installs nfpm from a downloaded `.deb` without any integrity verification.

### Issue Context
Even with HTTPS, this lacks defense-in-depth against compromised upstream release artifacts.

### Fix Focus Areas
- .github/workflows/deb-package.yml[64-71]

### Suggested fix
Add an integrity verification step before `dpkg -i`, for example:
- Download nfpm’s published `checksums.txt` (or equivalent) for `${NFPM_VERSION}` and verify `/tmp/nfpm.deb` with `sha256sum -c`.
- Alternatively, verify a published signature (GPG/cosign) if available for nfpm release artifacts.
- Only proceed to `sudo dpkg -i` if verification passes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +22 to +44
# --- Sandboxing ---------------------------------------------------------
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectHostname=true
ProtectClock=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=false # Node's JIT needs W+X mappings
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
UMask=0027

ReadWritePaths=/var/lib/etherpad /var/log/etherpad /etc/etherpad

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Readonly /opt breaks startup 🐞 Bug ☼ Reliability

The unit sets ProtectSystem=strict but Etherpad writes runtime files under settings.root/var during
startup (for example var/js and var/installed_plugins.json). Because /opt/etherpad/var is neither
created/redirected in postinstall nor included in ReadWritePaths, a fresh install can fail to start
with mkdir/write errors under /opt/etherpad/var.
Agent Prompt
### Issue description
The systemd unit makes `/opt/etherpad` effectively read-only (`ProtectSystem=strict` + `ReadWritePaths` omits `/opt/etherpad/...`), but Etherpad writes to `path.join(settings.root, 'var', ...)` during startup, where `settings.root` resolves to `/opt/etherpad` in this package layout. This can prevent `etherpad.service` from starting.

### Issue Context
Etherpad startup awaits `checkForMigration()` which can write `${root}/var/installed_plugins.json`, and the express hooks create `${root}/var/js`. The package currently does not create or redirect `/opt/etherpad/var` to a writable location.

### Fix Focus Areas
- packaging/scripts/postinstall.sh[34-40]
- packaging/systemd/etherpad.service[22-44]

### Suggested fix
Implement one of the following (prefer the symlink approach to keep writes out of `/opt`):
1) **Symlink `/opt/etherpad/var` to a writable location**:
   - In `postinstall.sh` (configure):
     - `mkdir -p /var/lib/etherpad/var`
     - `ln -sfn /var/lib/etherpad/var /opt/etherpad/var`
     - `chown -R etherpad:etherpad /var/lib/etherpad/var`
   - This keeps `ReadWritePaths=/var/lib/etherpad` sufficient.

2) **Allow `/opt/etherpad/var` writes explicitly**:
   - Create `/opt/etherpad/var` in `postinstall.sh` and `chown` it to `etherpad:etherpad`.
   - Add `/opt/etherpad/var` to `ReadWritePaths` in the unit.

Either way, ensure the directory exists before service start so `fs.mkdirSync(.../var/js)` and plugin migration writes don’t throw.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +73 to +85
- name: Stage tree for packaging
run: |
set -eux
STAGE=staging/opt/etherpad
mkdir -p "${STAGE}"

# Production footprint = src/ + bin/ + node_modules/ + metadata.
cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \
node_modules "${STAGE}/"

# Make pnpm-workspace.yaml production-only (same trick Dockerfile uses).
printf 'packages:\n - src\n - bin\n' > "${STAGE}/pnpm-workspace.yaml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Plugin migration startup failure 🐞 Bug ☼ Reliability

On fresh installs, Etherpad startup runs checkForMigration(), which falls back to running pnpm ls
and then writing /opt/etherpad/var/installed_plugins.json if the file is missing; the package does
not ship/create that file, and the systemd unit disallows writes under /opt/etherpad. This can
cause the service to crash during startup and never reach /health.
Agent Prompt
### Issue description
Fresh installs can fail because Etherpad calls `checkForMigration()` during startup, and if `/opt/etherpad/var/installed_plugins.json` is missing it tries to execute `pnpm ls` and write that file under `/opt/etherpad/var`. The .deb packaging currently doesn’t ship/create that file (or even `/opt/etherpad/var`), and the systemd unit doesn’t permit writes under `/opt/etherpad`.

### Issue Context
- Packaging stages `/opt/etherpad` from a curated file list.
- Etherpad’s plugin migration logic treats absence of `var/installed_plugins.json` as a trigger to run `pnpm` and then persist plugin state under `settings.root/var`.
- The systemd sandbox only allows writes to `/var/lib/etherpad`, `/var/log/etherpad`, and `/etc/etherpad`.

### Fix Focus Areas
- .github/workflows/deb-package.yml[73-85]
- packaging/nfpm.yaml[47-59]
- packaging/scripts/postinstall.sh[14-40]
- packaging/systemd/etherpad.service[22-44]

### Suggested fix approach
1. Ensure `/opt/etherpad/var/` exists in the packaged tree (or create it in `postinstall.sh`).
2. Seed `/opt/etherpad/var/installed_plugins.json` at install time with minimal valid content (for example, only `ep_etherpad-lite`), so `checkForMigration()` does not attempt to run `pnpm` on first boot.
3. If you want runtime plugin install/uninstall to work under the hardened unit, relocate plugin state to `/var/lib/etherpad` (and symlink `/opt/etherpad/var` to it), and/or extend `ReadWritePaths` to include the required writable plugin directories.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant