LibrePortal/docs/guide/upgrade-notes.md
librelad 4685320353 feat(secrets): real VAPID keypair for mastodon, slot-numbered DB passwords
VAPID: the two values are the halves of one P-256 keypair, not independent
secrets — the browser verifies that a push is signed by the private key matching
the public key it subscribed with. The RANDOMIZED* generators mint each
placeholder on its own, so they produced two unrelated strings and web push could
never have worked. Generate the pair in mastodon_install_post_setup the way stoat
already does, encoded as Mastodon's webpush gem expects: unpadded URL-safe base64
of the 32-byte private scalar and the 65-byte uncompressed public point, sliced
out of the SEC1 DER. Verified by rebuilding the key from the emitted private half
and re-deriving the public point — openssl accepts it and the point matches.

Generated once and never rotated (rotation would invalidate every subscription),
but a pair of the wrong shape is replaced, so an install carrying the old
unrelated strings heals itself on next install — their public half is 42 chars
where a real point is 87.

Slots: CFG_<APP>_DB_PASSWORD -> CFG_<APP>_DB_PASSWORD_1 and likewise for
DB_ROOT_PASSWORD, across mastodon, owncloud, mattermost, matrix, nextcloud and
bookstack, so a database credential is always a numbered slot and a second one is
just _2. Renaming a key means reconciliation drops the old and adds the new
holding its placeholder, so an existing install regenerates unless the value is
carried over first — documented, including that the old file survives as
.<app>.config.bak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:42:50 +01:00

205 lines
8.2 KiB
Markdown

# LibrePortal — Upgrade Notes
Version-specific notes for **existing installs**. `libreportal update apply` replaces
the install tree only — it never rewrites an app's deployed
`<containers-dir>/<app>/docker-compose.yml`. So a fix to an app template reaches a
running app on its next `libreportal app install <app>` (also the path taken by
restore, peer pull, and migrate), not on update. Anything below that needs a manual
step says so.
## 0.2.0 — Mastodon shipped with unsubstituted database credentials
**Affects:** anyone who installed Mastodon on 0.1.0. No other app is affected.
### What went wrong
Compose templates carry tag annotations of the form
`#LIBREPORTAL|<TAG>|<VALUE>`, where `<VALUE>` is the *current literal* the tag
manager searches for on that line. Mastodon's annotations used `unconfigured`
while the line bodies said `PASSWORD_TAG_1_DATA` and friends, so the two never
matched and nothing was ever substituted:
```yaml
- DB_PASS=PASSWORD_TAG_1_DATA #LIBREPORTAL|PASSWORD_TAG_1|unconfigured
```
The literal placeholder strings shipped as the real values. Because `unconfigured`
does not look like a placeholder, neither the tag state check nor the pre-start
stale-tag gate in `dockerComposeUp` flagged it, and restarts did not heal it.
An affected instance is running with these publicly known constants:
| Setting | Value in an affected install |
| --- | --- |
| `DB_USER` / `POSTGRES_USER` | `RANDOM_TAG_1_DATA` |
| `DB_PASS` / `POSTGRES_PASSWORD` | `PASSWORD_TAG_1_DATA` |
| `DB_NAME` / `POSTGRES_DB` | `RANDOM_TAG_2_DATA` |
| `SECRET_KEY_BASE` | `HEX_TAG_1_DATA` |
| `OTP_SECRET` | `HEX_TAG_2_DATA` |
| `VAPID_PRIVATE_KEY` / `VAPID_PUBLIC_KEY` | `VAPID_TAG_1_DATA` / `VAPID_TAG_2_DATA` |
`mastodon-postgres` publishes no host port, so the database is reachable only from
other containers on the LibrePortal network — but `SECRET_KEY_BASE` is what Rails
uses to sign and encrypt session cookies, and the web service *is* internet-facing
through Traefik. Treat a public instance as needing rotation, not just cleanup.
### Why you cannot just re-install
`libreportal app install mastodon` copies the corrected template over the deployed
compose and generates real credentials. The Postgres volume
(`<containers-dir>/mastodon/postgres`) was initialised with the old literals and
keeps them — `POSTGRES_*` is only read by `initdb` on an empty data directory. The
new credentials will not match, and the app will fail to reach its database.
Pick one of the two paths below.
### Path A — discard the instance (no data worth keeping)
```bash
libreportal app uninstall mastodon
libreportal app install mastodon
```
Everything is regenerated correctly. Federation identity is lost; the instance is
new to the network.
### Path B — keep the data, rotate the credentials
Take a cold copy first — with the app stopped, the app folder contains the whole
database:
```bash
libreportal app stop mastodon
sudo cp -a <containers-dir>/mastodon <containers-dir>/mastodon.bak-0.1.0
```
Re-template to get real credentials into the compose file:
```bash
libreportal app install mastodon
```
Read the values it generated:
```bash
grep -E 'POSTGRES_(DB|USER|PASSWORD)=' <containers-dir>/mastodon/docker-compose.yml
```
Bring up only the database and connect as the old superuser (the container's unix
socket trusts local connections, so no password is needed):
```bash
docker start mastodon-postgres
docker exec -it mastodon-postgres psql -U RANDOM_TAG_1_DATA -d postgres
```
Rename the database and create the new role, substituting the three values you
just read:
```sql
ALTER DATABASE "RANDOM_TAG_2_DATA" RENAME TO "<new POSTGRES_DB>";
CREATE ROLE "<new POSTGRES_USER>" LOGIN SUPERUSER PASSWORD '<new POSTGRES_PASSWORD>';
\c "<new POSTGRES_DB>"
REASSIGN OWNED BY "RANDOM_TAG_1_DATA" TO "<new POSTGRES_USER>";
\c postgres "<new POSTGRES_USER>"
DROP ROLE "RANDOM_TAG_1_DATA";
```
The rename needs no active sessions on that database, which is why Mastodon is
stopped. The role is created and the objects reassigned rather than renamed —
PostgreSQL refuses to rename the session role you are connected as.
Start the app:
```bash
libreportal app start mastodon
```
**Expect user-visible fallout.** `SECRET_KEY_BASE` changed, so every session is
invalidated and everyone logs in again. `OTP_SECRET` changed, which affects stored
two-factor enrolments — be ready to clear 2FA for users who are locked out:
```bash
docker exec -it mastodon-service bin/tootctl accounts modify <username> --disable-2fa
```
The VAPID keypair changed too; browsers re-subscribe to push on next login.
Once the instance is healthy, remove the backup copy.
## 0.2.0 — App credentials moved into `<app>.config`
**Affects:** existing installs of **mastodon, owncloud, mattermost, matrix** and
**stoat**. Nothing to do until you next reinstall one of them.
### What changed
Those five apps took their generated secrets from the compose-side generator tags
(`PASSWORD_TAG_<n>`, `RANDOM_TAG_<n>`, `HEX_TAG_<n>`, `VAPID_TAG_<n>`). Those mint
a fresh secret on **every** templating run, so a reinstall handed the app a brand
new database password while its data volume kept the one `initdb` was given.
Their secrets now live in `<app>.config` as `RANDOMIZED*` placeholders and reach
the compose through the same `#LIBREPORTAL|<APP>_<KEY>_TAG|` mechanism every other
config value uses — generated once on first install, then preserved. Passwords are
still randomly generated; nothing here asks you to choose one.
### What an existing install sees
Config reconciliation adds the new keys on update, still holding their
placeholders. The deployed compose is untouched until you reinstall, so the app
keeps running on its current credentials.
On the next `libreportal app install <app>`, the placeholders are filled with
freshly generated secrets — which will not match what the app's data volume was
initialised with. That is the same desync the old mechanism caused on every
reinstall; the difference is that it now happens at most once, because the values
are preserved from then on.
### Database password keys are now numbered slots
`CFG_<APP>_DB_PASSWORD` became `CFG_<APP>_DB_PASSWORD_1`, and
`CFG_<APP>_DB_ROOT_PASSWORD` became `CFG_<APP>_DB_ROOT_PASSWORD_1`, so a database
credential is always a numbered slot and an app that needs a second one just adds
`_2`. This also touches **nextcloud** and **bookstack**, which were otherwise
unaffected by the change above.
A rename is a delete plus an add as far as config reconciliation is concerned: the
old key is dropped and the new one arrives holding its `RANDOMIZED*` placeholder,
so the next install generates a fresh password rather than keeping the current
one. To carry the existing password over, copy it into the new key before
reinstalling — reconciliation leaves the previous file as a hidden
`.<app>.config.bak` sibling, so the old value is still there:
```bash
grep DB_PASSWORD <containers-dir>/<app>/.<app>.config.bak
```
Paste each value into the matching `_1` key in `<containers-dir>/<app>/<app>.config`.
### Mastodon web push
`VAPID_PRIVATE_KEY` and `VAPID_PUBLIC_KEY` were generated as two independent
random strings, which is not a keypair — web push could never have worked. They
are now generated together as a P-256 keypair by the install hook, in the format
Mastodon's webpush gem expects. Any install still carrying the old values
regenerates the pair once on its next install (the old public half is the wrong
length to be a valid point, which is what triggers it) and is correct from then
on. Existing push subscriptions were not working anyway, so nothing is lost;
clients re-subscribe on next login.
### Carrying credentials across, generally
If you have one of these installed and want to avoid regeneration, capture the
credentials **before** reinstalling. With the app stopped:
```bash
grep -E 'POSTGRES_|MYSQL_|SECRET_KEY_BASE|OTP_SECRET|VAPID_|RABBITMQ_DEFAULT_PASS' <containers-dir>/<app>/docker-compose.yml
```
then paste each value into the matching `CFG_<APP>_<KEY>` in
`<containers-dir>/<app>/<app>.config`, replacing the `RANDOMIZED*` placeholder.
The install will adopt what it finds rather than generating over it. If you skip
this, follow the database steps in the Mastodon section above — they apply to any
of the five, with that app's own database and role names.