The sha256 only proves a download is intact; a compromised host could swap the tarball + its checksum. Add minisign signatures, which prove authenticity (the host can't forge them without the offline secret key). Ships INACTIVE behind a REPLACE_ME placeholder, so installs work until a real key is generated; then it's REQUIRED. - make_release.sh: signs the tarball when LP_MINISIGN_SECKEY is set -> <tarball>.minisig. - libreportal.pub: the public key (placeholder), ships in the tarball and is installed to the ROOT-OWNED footprint (/usr/local/lib/libreportal/libreportal.pub) by init.sh -> the manager can't swap it to accept forged updates. footprint_version -> 2. - install.sh: LP_MINISIGN_PUBKEY constant; once non-placeholder, downloads + verifies the .minisig (minisign -P) and REFUSES on invalid/missing (auto-installs minisign if needed). --no-verify-signature is a dev-only escape hatch. - fetch.sh (update path): verifies against the footprint .pub (minisign -p), refuses on invalid/missing. - docs/DEVELOPMENT.md: keygen (minisign -G), paste pubkey into libreportal.pub + install.sh, keep the secret key offline, sign builds via LP_MINISIGN_SECKEY, bump footprint_version on key rotation. Verified end-to-end with a real throwaway key: good signature accepted; tampered, wrong-key, and missing-signature all refused; placeholder skips (sha256 still enforced). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
177 lines
8.4 KiB
Markdown
177 lines
8.4 KiB
Markdown
# LibrePortal — Development & Releases
|
||
|
||
How to run a development copy, cut stable/edge releases, and test them before they
|
||
go out. For installing/using LibrePortal, see [USER.md](USER.md).
|
||
|
||
## Mental model (read this first)
|
||
|
||
**Install modes** — `CFG_INSTALL_MODE` decides where the code comes from:
|
||
|
||
| Mode | Source | Use |
|
||
|---|---|---|
|
||
| `release` (default) | a checksum-verified `.tar.gz` over HTTPS | end users / stable |
|
||
| `git` | `git clone` of the repo | contributors tracking a branch |
|
||
| `local` | a copy of a local folder | hacking on the code on the box |
|
||
|
||
**Three roots** (each relocatable at install, then fixed):
|
||
`<system>` (manager-owned: configs/db/logs/install) · `<containers>` (container
|
||
user: app data) · `<backups>` (container user: repos). Defaults `/libreportal-*`.
|
||
|
||
**Two users:** the **manager** (`sudo_user_name`, default `libreportal`) owns the
|
||
control plane and runs the runtime; the **container user** (`CFG_DOCKER_INSTALL_USER`,
|
||
default `dockerinstall`) owns app data + runs rootless Docker. Genuine-root actions
|
||
go through fixed, root-owned helpers in `/usr/local/lib/libreportal/` (paths +
|
||
manager name are *baked* into them at install — never read from runtime config).
|
||
|
||
**Key files:**
|
||
- `init.sh` — the installer (self-contained; creates users/folders/helpers, bakes things).
|
||
- `install.sh` — the thin bootstrap (download+verify+extract a release, then run init.sh).
|
||
- `scripts/source/paths.sh` — resolves the three roots + manager user.
|
||
- `scripts/source/fetch.sh` — `lpFetchRelease` / `lpVersionGt` (runtime fetch + version compare).
|
||
- `scripts/release/make_release.sh` — builds release artifacts.
|
||
- `VERSION` — the single source of the version number.
|
||
|
||
## Run a development copy
|
||
|
||
From a clone of the repo, on a **throwaway Debian/Ubuntu host** (install is
|
||
destructive — it creates system users and dirs):
|
||
|
||
**Local mode** (install from the working tree — best for hacking):
|
||
```bash
|
||
sudo ./init.sh --random-password --local init
|
||
# custom locations work in dev too:
|
||
sudo ./init.sh --random-password --local --system-dir=/srv/lp --manager-user=lpadmin init
|
||
```
|
||
|
||
**Git mode** (track a branch):
|
||
```bash
|
||
# via the bootstrap:
|
||
sudo ./install.sh --git-url=https://example.com/you/LibrePortal.git \
|
||
--git-user=USER --git-token=TOKEN
|
||
# or directly: ./init.sh init <password> <git_user> <git_token> <git_url> true git
|
||
```
|
||
|
||
**Iterating:** re-run the installer to redeploy after changes (local mode re-copies
|
||
the tree). To wipe and start over:
|
||
```bash
|
||
sudo ./init.sh uninstall # removes the three roots + users + footprint
|
||
sudo ./init.sh --skip-docker-images uninstall # keep the Docker layer for a fast reinstall
|
||
```
|
||
|
||
> This repo's CI/hook setup may auto-deploy on commit (commit on a branch →
|
||
> auto-merge → redeploy). That's environment-specific; the commands above are the
|
||
> portable way to stand up and refresh a dev box.
|
||
|
||
## Cut a release (stable or edge)
|
||
|
||
1. **Bump the version** in `VERSION` (semver, e.g. `0.2.0` → `0.3.0`). Commit it.
|
||
2. **Build the artifact** (uses `git archive`, so it ships only committed files and
|
||
honours `.gitattributes export-ignore` — `scripts/unused`, `site`, `docs`,
|
||
`.claude`, the release tooling, etc. never ship):
|
||
```bash
|
||
scripts/release/make_release.sh stable # or: edge [git-ref]
|
||
```
|
||
Produces, under `dist/<channel>/`:
|
||
- `libreportal-<version>.tar.gz` — the release
|
||
- `libreportal-<version>.tar.gz.sha256` — its checksum
|
||
- `latest.json` — `{ version, channel, url, sha256, notes }` (the channel pointer)
|
||
3. **Publish** by copying `dist/<channel>/*` to the host so they're served at
|
||
`https://get.libreportal.org/<channel>/…` (Phase E — the `getlibreportal`
|
||
container; not built yet). `latest.json` is what makes a version "the latest".
|
||
|
||
**Channels:** `stable` is the default users get; `edge` is for early/testing
|
||
builds. Same tooling, different `<channel>`. To promote an edge build to stable,
|
||
rebuild with `make_release.sh stable` at that ref (or copy its artifacts into the
|
||
`stable/` path and update `stable/latest.json`).
|
||
|
||
## Test a release locally before publishing
|
||
|
||
No hosting needed — serve `dist/` and point an install at it:
|
||
|
||
```bash
|
||
# build, then serve the artifacts
|
||
scripts/release/make_release.sh stable
|
||
( cd dist && python3 -m http.server 8000 )
|
||
|
||
# on a throwaway host, install from your local server:
|
||
LP_RELEASE_BASE_URL=http://<your-ip>:8000 \
|
||
sudo ./install.sh --channel=stable --system-dir=/libreportal-system
|
||
```
|
||
|
||
`LP_RELEASE_BASE_URL` overrides the release host everywhere (installer, updater,
|
||
recovery). Quick non-destructive checks:
|
||
|
||
```bash
|
||
# fetch + verify + stage only (no install):
|
||
LP_RELEASE_BASE_URL=http://<ip>:8000 ./install.sh --dry-run
|
||
# tamper with the tarball and confirm it's refused:
|
||
echo x >> dist/stable/libreportal-*.tar.gz
|
||
LP_RELEASE_BASE_URL=http://<ip>:8000 ./install.sh --dry-run # => CHECKSUM MISMATCH
|
||
```
|
||
|
||
## How updates work (so you can reason about them)
|
||
|
||
In `release` mode the WebUI badge + `libreportal update apply` compare the local
|
||
`VERSION` against the channel's `latest.json` (`lpVersionGt`); if newer, they
|
||
`lpFetchRelease` the new tarball (verified) and redeploy. Because the install tree
|
||
is *code only* (configs/logs/backups live in the other roots), the update just
|
||
replaces it — no backup/restore dance. `git`/`local` modes keep their existing
|
||
git-based update path.
|
||
|
||
**The footprint exception (important).** `update apply` runs as the *manager*, and
|
||
the manager is deliberately forbidden from rewriting the **root-owned footprint**
|
||
(the helpers in `/usr/local/lib/libreportal/`, the CLI wrapper, the uninstall
|
||
launcher, the systemd unit, the sudoers) — that immutability *is* the de-sudo
|
||
security boundary. So a manager-run update can refresh **code/apps/WebUI**, but not
|
||
those. To track when an update touches them, `init.sh` carries a `footprint_version`
|
||
integer, baked at install into `/usr/local/lib/libreportal/.footprint_version` and
|
||
published in `latest.json`. When the channel's `footprint_version` exceeds the
|
||
installed one, the updater **refuses the WebUI apply** and the badge flags
|
||
`footprint_update_needed` — the user re-runs the installer as root
|
||
(`curl … install.sh | sudo bash`), which fetches *and* re-bakes the footprint
|
||
atomically. (Re-running the installer is idempotent.)
|
||
|
||
➡️ **BUMP `footprint_version` in `init.sh` whenever you change anything in
|
||
`scripts/system/*`, the CLI wrapper, the uninstall launcher, the systemd unit, or
|
||
the sudoers.** Forgetting it means those root components silently stay stale until
|
||
the next full reinstall.
|
||
|
||
## Signing releases (minisign)
|
||
|
||
The sha256 only proves a download is intact — a *compromised host* could swap the
|
||
tarball **and** its checksum. A minisign signature proves the release is genuinely
|
||
ours: the host can't forge it without the offline secret key. It ships **inactive**
|
||
(a `REPLACE_ME` placeholder), so installs work today; once you set a real key,
|
||
verification becomes **required** for release installs + updates.
|
||
|
||
**One-time setup:**
|
||
```bash
|
||
minisign -G -p libreportal.pub -s ~/.minisign/libreportal.key # generate the keypair
|
||
# 1. keep ~/.minisign/libreportal.key OFFLINE (this is the thing to protect)
|
||
# 2. paste the PUBLIC key (the RW… line) into BOTH:
|
||
# - libreportal.pub (ships + installed to the root footprint, used by updates)
|
||
# - install.sh LP_MINISIGN_PUBKEY=… (the standalone bootstrap)
|
||
# 3. bump footprint_version in init.sh (the footprint's public key changed)
|
||
```
|
||
|
||
**Signing a build:** point `make_release` at the secret key on the release machine:
|
||
```bash
|
||
LP_MINISIGN_SECKEY=~/.minisign/libreportal.key scripts/release/make_release.sh stable
|
||
```
|
||
It emits `libreportal-<ver>.tar.gz.minisig` alongside the tarball. `install.sh` and
|
||
the updater (`lpFetchRelease`) download `.minisig`, verify it against the public key,
|
||
and **refuse** on a bad/missing signature. `--no-verify-signature` on `install.sh`
|
||
is a dev-only escape hatch.
|
||
|
||
> Rotating the key later = repeat steps 2–3 and re-bump `footprint_version` (the
|
||
> root-owned public key is part of the footprint).
|
||
|
||
## Conventions
|
||
|
||
- **Versioning:** semver in `VERSION`. Bump before building; `latest.json` carries it.
|
||
- **New runtime script?** Add it under `scripts/<area>/…` and run
|
||
`scripts/source/files/generate_arrays.sh run` so it's sourced (build/standalone
|
||
tooling under `scripts/release` and `scripts/system` is intentionally excluded).
|
||
- **Don't** make the OS footprint (`/etc/*`, `/usr/local/*`) relocatable — it's
|
||
fixed by design for the privilege model.
|