fix(mastodon): make tag annotations substitutable

The tag manager reads `#LIBREPORTAL|<TAG>|<VALUE>` and takes <VALUE> 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, `<KIND>_DATA_<n>`. 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 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-18 05:36:28 +01:00
parent 9919eea138
commit c11052b753
3 changed files with 154 additions and 11 deletions

View File

@ -15,18 +15,18 @@ services:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- LOCAL_DOMAIN=DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA - LOCAL_DOMAIN=DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
- DB_HOST=mastodon-postgres - DB_HOST=mastodon-postgres
- DB_USER=RANDOM_TAG_1_DATA #LIBREPORTAL|RANDOM_TAG_1|unconfigured - DB_USER=RANDOM_DATA_1 #LIBREPORTAL|RANDOM_TAG_1|RANDOM_DATA_1
- DB_PASS=PASSWORD_TAG_1_DATA #LIBREPORTAL|PASSWORD_TAG_1|unconfigured - DB_PASS=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
- DB_NAME=RANDOM_TAG_2_DATA #LIBREPORTAL|RANDOM_TAG_2|unconfigured - DB_NAME=RANDOM_DATA_2 #LIBREPORTAL|RANDOM_TAG_2|RANDOM_DATA_2
- REDIS_HOST=mastodon-redis - REDIS_HOST=mastodon-redis
- SECRET_KEY_BASE=HEX_TAG_1_DATA #LIBREPORTAL|HEX_TAG_1|unconfigured - SECRET_KEY_BASE=HEX_DATA_1 #LIBREPORTAL|HEX_TAG_1|HEX_DATA_1
- OTP_SECRET=HEX_TAG_2_DATA #LIBREPORTAL|HEX_TAG_2|unconfigured - OTP_SECRET=HEX_DATA_2 #LIBREPORTAL|HEX_TAG_2|HEX_DATA_2
- VAPID_PRIVATE_KEY=VAPID_TAG_1_DATA #LIBREPORTAL|VAPID_TAG_1|unconfigured - VAPID_PRIVATE_KEY=VAPID_DATA_1 #LIBREPORTAL|VAPID_TAG_1|VAPID_DATA_1
- VAPID_PUBLIC_KEY=VAPID_TAG_2_DATA #LIBREPORTAL|VAPID_TAG_2|unconfigured - VAPID_PUBLIC_KEY=VAPID_DATA_2 #LIBREPORTAL|VAPID_TAG_2|VAPID_DATA_2
- SMTP_SERVER= - SMTP_SERVER=
- SMTP_PORT=587 - SMTP_PORT=587
- SMTP_LOGIN= - 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= - SMTP_FROM_ADDRESS=
- EMAIL_DELIVERY_METHOD=none - EMAIL_DELIVERY_METHOD=none
- SMTP_AUTH_METHOD=none - SMTP_AUTH_METHOD=none
@ -59,9 +59,9 @@ services:
image: postgres:15 image: postgres:15
container_name: mastodon-postgres container_name: mastodon-postgres
environment: environment:
- POSTGRES_DB=RANDOM_TAG_2_DATA #LIBREPORTAL|RANDOM_TAG_2|unconfigured - POSTGRES_DB=RANDOM_DATA_2 #LIBREPORTAL|RANDOM_TAG_2|RANDOM_DATA_2
- POSTGRES_USER=RANDOM_TAG_1_DATA #LIBREPORTAL|RANDOM_TAG_1|unconfigured - POSTGRES_USER=RANDOM_DATA_1 #LIBREPORTAL|RANDOM_TAG_1|RANDOM_DATA_1
- POSTGRES_PASSWORD=PASSWORD_TAG_1_DATA #LIBREPORTAL|PASSWORD_TAG_1|unconfigured - POSTGRES_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
volumes: volumes:
- ./postgres:/var/lib/postgresql/data - ./postgres:/var/lib/postgresql/data
networks: networks:

View File

@ -73,6 +73,10 @@ libreportal update check # just re-check the channel
Updates download + verify the new release tarball and redeploy. Your data, Updates download + verify the new release tarball and redeploy. Your data,
configs, and backups are untouched (they live outside the replaced install tree). 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 ## Backups on an external / removable drive
Point a backup location at the drive's mount path. For a removable disk, set Point a backup location at the drive's mount path. For a removable disk, set

139
docs/guide/upgrade-notes.md Normal file
View File

@ -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
`<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.
### 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 `<app>.config` as `RANDOMIZEDPASSWORD<n>` / `RANDOMIZEDUSERNAME<n>`, which are
generated once and persisted.