LibrePortal/docs/guide/upgrade-notes.md
librelad 5706498565 fix(secrets): move app credentials into <app>.config, fix slot collision
Five apps (mastodon, owncloud, mattermost, matrix, stoat) 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 new database password while its data volume kept the
one initdb was given, and the app came back up unable to open its own database.

Move them to <app>.config as RANDOMIZED* placeholders, reaching the compose via
the #LIBREPORTAL|<APP>_<KEY>_TAG| mechanism tags_processor_app_config_values
already provides. No new handler: the tag name is derived from the config key, so
this is a config line plus a tag per secret. Generation is unchanged — still
random on first install; the value is now remembered instead of re-rolled.

Also fixes two things this exposed:

- The RANDOMIZED* replacers matched unanchored. `sort -u` orders slots lexically
  (1, 10, 11, 2), so slot 1's pattern rewrote the prefix inside slot 10's
  placeholder and slots 10+ ended up holding slot 1's secret with a digit glued
  on — derivable, and invisible because the values weren't byte-identical.
  Anchoring with \b makes match order irrelevant. Verified at 20 slots across
  all four placeholder types: 64 keys, 64 distinct values, no prefix collisions.

- generateRandomPassword drew from base64 without constraining the mix; measured
  over 2000 draws, 1 in 40 contained no digit at all. Retry until the result has
  both a digit and a letter, bounded so a pathological length can't spin.

owncloud gains a fix in passing: its compose seeded the admin account from
PASSWORD_TAG_2 while the WebUI displayed CFG_OWNCLOUD_ADMIN_PASSWORD, which was
generated separately and never used. Both now read the same value.

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

6.6 KiB

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:

- 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)

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:

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:

libreportal app install mastodon

Read the values it generated:

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):

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:

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:

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:

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.

If you have one of these installed and want to avoid it, capture the credentials before reinstalling. With the app stopped:

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.