From c11052b7537c09660f5b67f8a5dd618b69897c1c Mon Sep 17 00:00:00 2001 From: librelad Date: Tue, 18 Aug 2026 05:36:28 +0100 Subject: [PATCH] fix(mastodon): make tag annotations substitutable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tag manager reads `#LIBREPORTAL||` and takes as the current literal to search for on that line, so it must equal the string in the line body. Mastodon used `unconfigured` as the annotation value against bodies like `PASSWORD_TAG_1_DATA` — nothing matched, nothing was ever substituted, and the placeholder shipped as the live database password, SECRET_KEY_BASE, OTP secret and VAPID keypair. Nothing caught it either: `unconfigured` doesn't match `_DATA`, so tagsManagerGetTagState reported the tags as configured, and the stale-tag gate in dockerComposeUp (which tests the annotation value against `^[A-Z][A-Z0-9_]*_DATA(_[0-9]+)?$`) let the app start. Adopt the convention every other app already uses — body placeholder identical to the annotation value, `_DATA_`. All 11 tags now substitute, the app and postgres services agree on the same generated credentials, and an unfilled tag is visible to the pre-start gate. Existing 0.1.0 installs keep their literal credentials until re-installed, and their Postgres was initialised with them, so a plain re-install desynchronises the compose from the volume. Document both recovery paths in upgrade notes. Co-Authored-By: Claude Opus 5 --- containers/mastodon/docker-compose.yml | 22 ++-- docs/guide/install-and-use.md | 4 + docs/guide/upgrade-notes.md | 139 +++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 docs/guide/upgrade-notes.md diff --git a/containers/mastodon/docker-compose.yml b/containers/mastodon/docker-compose.yml index 1972152..01cb99e 100755 --- a/containers/mastodon/docker-compose.yml +++ b/containers/mastodon/docker-compose.yml @@ -15,18 +15,18 @@ services: - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA - LOCAL_DOMAIN=DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA - DB_HOST=mastodon-postgres - - DB_USER=RANDOM_TAG_1_DATA #LIBREPORTAL|RANDOM_TAG_1|unconfigured - - DB_PASS=PASSWORD_TAG_1_DATA #LIBREPORTAL|PASSWORD_TAG_1|unconfigured - - DB_NAME=RANDOM_TAG_2_DATA #LIBREPORTAL|RANDOM_TAG_2|unconfigured + - DB_USER=RANDOM_DATA_1 #LIBREPORTAL|RANDOM_TAG_1|RANDOM_DATA_1 + - DB_PASS=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1 + - DB_NAME=RANDOM_DATA_2 #LIBREPORTAL|RANDOM_TAG_2|RANDOM_DATA_2 - REDIS_HOST=mastodon-redis - - SECRET_KEY_BASE=HEX_TAG_1_DATA #LIBREPORTAL|HEX_TAG_1|unconfigured - - OTP_SECRET=HEX_TAG_2_DATA #LIBREPORTAL|HEX_TAG_2|unconfigured - - VAPID_PRIVATE_KEY=VAPID_TAG_1_DATA #LIBREPORTAL|VAPID_TAG_1|unconfigured - - VAPID_PUBLIC_KEY=VAPID_TAG_2_DATA #LIBREPORTAL|VAPID_TAG_2|unconfigured + - SECRET_KEY_BASE=HEX_DATA_1 #LIBREPORTAL|HEX_TAG_1|HEX_DATA_1 + - OTP_SECRET=HEX_DATA_2 #LIBREPORTAL|HEX_TAG_2|HEX_DATA_2 + - VAPID_PRIVATE_KEY=VAPID_DATA_1 #LIBREPORTAL|VAPID_TAG_1|VAPID_DATA_1 + - VAPID_PUBLIC_KEY=VAPID_DATA_2 #LIBREPORTAL|VAPID_TAG_2|VAPID_DATA_2 - SMTP_SERVER= - SMTP_PORT=587 - SMTP_LOGIN= - - SMTP_PASSWORD=PASSWORD_TAG_2_DATA #LIBREPORTAL|PASSWORD_TAG_2|unconfigured + - SMTP_PASSWORD=PASSWORD_DATA_2 #LIBREPORTAL|PASSWORD_TAG_2|PASSWORD_DATA_2 - SMTP_FROM_ADDRESS= - EMAIL_DELIVERY_METHOD=none - SMTP_AUTH_METHOD=none @@ -59,9 +59,9 @@ services: image: postgres:15 container_name: mastodon-postgres environment: - - POSTGRES_DB=RANDOM_TAG_2_DATA #LIBREPORTAL|RANDOM_TAG_2|unconfigured - - POSTGRES_USER=RANDOM_TAG_1_DATA #LIBREPORTAL|RANDOM_TAG_1|unconfigured - - POSTGRES_PASSWORD=PASSWORD_TAG_1_DATA #LIBREPORTAL|PASSWORD_TAG_1|unconfigured + - POSTGRES_DB=RANDOM_DATA_2 #LIBREPORTAL|RANDOM_TAG_2|RANDOM_DATA_2 + - POSTGRES_USER=RANDOM_DATA_1 #LIBREPORTAL|RANDOM_TAG_1|RANDOM_DATA_1 + - POSTGRES_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1 volumes: - ./postgres:/var/lib/postgresql/data networks: diff --git a/docs/guide/install-and-use.md b/docs/guide/install-and-use.md index 4483474..52b8794 100644 --- a/docs/guide/install-and-use.md +++ b/docs/guide/install-and-use.md @@ -73,6 +73,10 @@ libreportal update check # just re-check the channel Updates download + verify the new release tarball and redeploy. Your data, configs, and backups are untouched (they live outside the replaced install tree). +Some releases carry a fix that an already-installed app only picks up on its next +install — see [upgrade notes](upgrade-notes.md) for the versions that need a +manual step. + ## Backups on an external / removable drive Point a backup location at the drive's mount path. For a removable disk, set diff --git a/docs/guide/upgrade-notes.md b/docs/guide/upgrade-notes.md new file mode 100644 index 0000000..c06867c --- /dev/null +++ b/docs/guide/upgrade-notes.md @@ -0,0 +1,139 @@ +# LibrePortal — Upgrade Notes + +Version-specific notes for **existing installs**. `libreportal update apply` replaces +the install tree only — it never rewrites an app's deployed +`//docker-compose.yml`. So a fix to an app template reaches a +running app on its next `libreportal app install ` (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||`, where `` 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 +(`/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 /mastodon /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)=' /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 ""; +CREATE ROLE "" LOGIN SUPERUSER PASSWORD ''; +\c "" +REASSIGN OWNED BY "RANDOM_TAG_1_DATA" TO ""; +\c postgres "" +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 --disable-2fa +``` + +The VAPID keypair changed too; browsers re-subscribe to push on next login. + +Once the instance is healthy, remove the backup copy. + +### Known limitation, both paths + +Re-templating an app regenerates every `PASSWORD_TAG_*` / `RANDOM_TAG_*` / +`HEX_TAG_*` / `VAPID_TAG_*` value — the generators mint a fresh secret on each run +and the tag manager writes it in. For any app whose database lives in a persistent +volume, that means a re-template can desynchronise the compose file from the +initialised database exactly as described above. This is not specific to Mastodon +or to this fix; app credentials that must survive re-templating are the ones held +in `.config` as `RANDOMIZEDPASSWORD` / `RANDOMIZEDUSERNAME`, which are +generated once and persisted.