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>
This commit is contained in:
parent
c11052b753
commit
5706498565
@ -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_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
|
||||
- DB_USER=MASTODON_DB_USER_DATA #LIBREPORTAL|MASTODON_DB_USER_TAG|MASTODON_DB_USER_DATA
|
||||
- DB_PASS=MASTODON_DB_PASSWORD_DATA #LIBREPORTAL|MASTODON_DB_PASSWORD_TAG|MASTODON_DB_PASSWORD_DATA
|
||||
- DB_NAME=MASTODON_DB_NAME_DATA #LIBREPORTAL|MASTODON_DB_NAME_TAG|MASTODON_DB_NAME_DATA
|
||||
- REDIS_HOST=mastodon-redis
|
||||
- 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
|
||||
- SECRET_KEY_BASE=MASTODON_SECRET_KEY_BASE_DATA #LIBREPORTAL|MASTODON_SECRET_KEY_BASE_TAG|MASTODON_SECRET_KEY_BASE_DATA
|
||||
- OTP_SECRET=MASTODON_OTP_SECRET_DATA #LIBREPORTAL|MASTODON_OTP_SECRET_TAG|MASTODON_OTP_SECRET_DATA
|
||||
- VAPID_PRIVATE_KEY=MASTODON_VAPID_PRIVATE_KEY_DATA #LIBREPORTAL|MASTODON_VAPID_PRIVATE_KEY_TAG|MASTODON_VAPID_PRIVATE_KEY_DATA
|
||||
- VAPID_PUBLIC_KEY=MASTODON_VAPID_PUBLIC_KEY_DATA #LIBREPORTAL|MASTODON_VAPID_PUBLIC_KEY_TAG|MASTODON_VAPID_PUBLIC_KEY_DATA
|
||||
- SMTP_SERVER=
|
||||
- SMTP_PORT=587
|
||||
- SMTP_LOGIN=
|
||||
- SMTP_PASSWORD=PASSWORD_DATA_2 #LIBREPORTAL|PASSWORD_TAG_2|PASSWORD_DATA_2
|
||||
- SMTP_PASSWORD=
|
||||
- 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_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
|
||||
- POSTGRES_DB=MASTODON_DB_NAME_DATA #LIBREPORTAL|MASTODON_DB_NAME_TAG|MASTODON_DB_NAME_DATA
|
||||
- POSTGRES_USER=MASTODON_DB_USER_DATA #LIBREPORTAL|MASTODON_DB_USER_TAG|MASTODON_DB_USER_DATA
|
||||
- POSTGRES_PASSWORD=MASTODON_DB_PASSWORD_DATA #LIBREPORTAL|MASTODON_DB_PASSWORD_TAG|MASTODON_DB_PASSWORD_DATA
|
||||
volumes:
|
||||
- ./postgres:/var/lib/postgresql/data
|
||||
networks:
|
||||
|
||||
@ -20,6 +20,31 @@ CFG_MASTODON_AUTHELIA=false
|
||||
CFG_MASTODON_HEADSCALE=false
|
||||
#
|
||||
# =============================================================================
|
||||
# SECRETS
|
||||
# =============================================================================
|
||||
# These feed the compose via #LIBREPORTAL|MASTODON_<KEY>_TAG| tags. They are
|
||||
# auto-generated on first install and — unlike a generator tag in the compose —
|
||||
# preserved across reinstalls, which is what keeps them in step with the
|
||||
# Postgres volume and with every logged-in session.
|
||||
#
|
||||
# DB_NAME / DB_USER / DB_PASSWORD = Postgres database, role and password. Set
|
||||
# once by initdb when the volume is created; changing them afterwards without
|
||||
# also changing them in Postgres locks Mastodon out of its own database.
|
||||
# SECRET_KEY_BASE = signs and encrypts Rails session cookies. Rotating it logs
|
||||
# every user out.
|
||||
# OTP_SECRET = protects stored two-factor enrolments. Rotating it invalidates
|
||||
# them, and anyone with 2FA on needs it reset before they can log in.
|
||||
# VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY = Web Push identity.
|
||||
#
|
||||
CFG_MASTODON_DB_NAME=RANDOMIZEDUSERNAME1
|
||||
CFG_MASTODON_DB_USER=RANDOMIZEDUSERNAME2
|
||||
CFG_MASTODON_DB_PASSWORD=RANDOMIZEDPASSWORD1
|
||||
CFG_MASTODON_SECRET_KEY_BASE=RANDOMIZEDHEX1
|
||||
CFG_MASTODON_OTP_SECRET=RANDOMIZEDHEX2
|
||||
CFG_MASTODON_VAPID_PRIVATE_KEY=RANDOMIZEDVAPID1
|
||||
CFG_MASTODON_VAPID_PUBLIC_KEY=RANDOMIZEDVAPID2
|
||||
#
|
||||
# =============================================================================
|
||||
# METADATA
|
||||
# =============================================================================
|
||||
# CATEGORY = application category for grouping
|
||||
|
||||
@ -103,7 +103,7 @@ services:
|
||||
environment:
|
||||
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
|
||||
- POSTGRES_USER=synapse
|
||||
- POSTGRES_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
|
||||
- POSTGRES_PASSWORD=MATRIX_DB_PASSWORD_DATA #LIBREPORTAL|MATRIX_DB_PASSWORD_TAG|MATRIX_DB_PASSWORD_DATA
|
||||
- POSTGRES_DB=synapse
|
||||
# Not optional. Synapse refuses to start against a database with any
|
||||
# other collation or ctype — it needs deterministic byte ordering for
|
||||
|
||||
@ -38,6 +38,12 @@ CFG_MATRIX_ENABLE_REGISTRATION=false
|
||||
CFG_MATRIX_ADMIN_USERNAME=admin
|
||||
CFG_MATRIX_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1
|
||||
CFG_MATRIX_MONITORING=false
|
||||
# Postgres password for the `synapse` role, fed to the compose via
|
||||
# #LIBREPORTAL|MATRIX_DB_PASSWORD_TAG| and written into homeserver.yaml by the
|
||||
# install hook. Generated on first install and preserved across reinstalls —
|
||||
# initdb sets it once when the volume is created, so a regenerated value would
|
||||
# leave Synapse unable to open its own database.
|
||||
CFG_MATRIX_DB_PASSWORD=RANDOMIZEDPASSWORD2
|
||||
#
|
||||
# =============================================================================
|
||||
# METADATA
|
||||
|
||||
@ -46,12 +46,13 @@ matrix_install_post_compose()
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Must match the password the compose handed to Postgres. Reading it back
|
||||
# from the deployed compose is the only way to stay in step: the value is
|
||||
# generated per-install by the password tag processor.
|
||||
# Must match the password the compose handed to Postgres. Read it back from
|
||||
# the deployed compose rather than from CFG_MATRIX_DB_PASSWORD: this hook
|
||||
# runs after templating, so the compose is the settled value, and it stays
|
||||
# correct even on an install whose config still holds the placeholder.
|
||||
local db_password
|
||||
db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "PASSWORD_TAG_1")
|
||||
if [[ -z "$db_password" || "$db_password" == "PASSWORD_DATA_1" ]]; then
|
||||
db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_DB_PASSWORD_TAG")
|
||||
if [[ -z "$db_password" || "$db_password" == "MATRIX_DB_PASSWORD_DATA" ]]; then
|
||||
isError "Database password was not generated in the compose file — aborting Synapse configuration."
|
||||
return 1
|
||||
fi
|
||||
|
||||
@ -34,7 +34,7 @@ services:
|
||||
# Fixed role and database name, random password. The database is
|
||||
# only reachable on the internal docker network, and a generated
|
||||
# username buys nothing while making manual psql recovery painful.
|
||||
- MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:PASSWORD_DATA_1@mattermost-postgres:5432/mattermost?sslmode=disable&connect_timeout=10 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
|
||||
- MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:MATTERMOST_DB_PASSWORD_DATA@mattermost-postgres:5432/mattermost?sslmode=disable&connect_timeout=10 #LIBREPORTAL|MATTERMOST_DB_PASSWORD_TAG|MATTERMOST_DB_PASSWORD_DATA
|
||||
# Mattermost builds every absolute link (invites, password resets,
|
||||
# CORS and websocket origin checks) from this. Wrong value and the
|
||||
# web client connects but the websocket is rejected, which shows up
|
||||
@ -83,7 +83,7 @@ services:
|
||||
environment:
|
||||
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
|
||||
- POSTGRES_USER=mattermost
|
||||
- POSTGRES_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
|
||||
- POSTGRES_PASSWORD=MATTERMOST_DB_PASSWORD_DATA #LIBREPORTAL|MATTERMOST_DB_PASSWORD_TAG|MATTERMOST_DB_PASSWORD_DATA
|
||||
- POSTGRES_DB=mattermost
|
||||
volumes:
|
||||
- ./postgres:/var/lib/postgresql/data
|
||||
|
||||
@ -26,6 +26,11 @@ CFG_MATTERMOST_HEALTHCHECK=true
|
||||
CFG_MATTERMOST_AUTHELIA=false
|
||||
CFG_MATTERMOST_HEADSCALE=false
|
||||
CFG_MATTERMOST_MONITORING=false
|
||||
# Postgres password for the `mattermost` role, fed to the compose via
|
||||
# #LIBREPORTAL|MATTERMOST_DB_PASSWORD_TAG| (both the server's datasource URL and
|
||||
# the database's own env). Generated on first install and preserved across
|
||||
# reinstalls — initdb sets it once when the volume is created.
|
||||
CFG_MATTERMOST_DB_PASSWORD=RANDOMIZEDPASSWORD1
|
||||
#
|
||||
# =============================================================================
|
||||
# METADATA
|
||||
|
||||
@ -20,12 +20,12 @@ services:
|
||||
- OWNCLOUD_DOMAIN=DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
|
||||
- OWNCLOUD_TRUSTED_DOMAINS=TRUSTED_DOMAINS_DATA #LIBREPORTAL|TRUSTED_DOMAINS_TAG|TRUSTED_DOMAINS_DATA
|
||||
- OWNCLOUD_DB_TYPE=mysql
|
||||
- OWNCLOUD_DB_NAME=RANDOM_DATA_3 #LIBREPORTAL|RANDOM_TAG_3|RANDOM_DATA_3
|
||||
- OWNCLOUD_DB_USERNAME=RANDOM_DATA_1 #LIBREPORTAL|RANDOM_TAG_1|RANDOM_DATA_1
|
||||
- OWNCLOUD_DB_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
|
||||
- OWNCLOUD_DB_NAME=OWNCLOUD_DB_NAME_DATA #LIBREPORTAL|OWNCLOUD_DB_NAME_TAG|OWNCLOUD_DB_NAME_DATA
|
||||
- OWNCLOUD_DB_USERNAME=OWNCLOUD_DB_USER_DATA #LIBREPORTAL|OWNCLOUD_DB_USER_TAG|OWNCLOUD_DB_USER_DATA
|
||||
- OWNCLOUD_DB_PASSWORD=OWNCLOUD_DB_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_DB_PASSWORD_TAG|OWNCLOUD_DB_PASSWORD_DATA
|
||||
- OWNCLOUD_DB_HOST=owncloud-mariadb
|
||||
- OWNCLOUD_ADMIN_USERNAME=RANDOM_DATA_2 #LIBREPORTAL|RANDOM_TAG_2|RANDOM_DATA_2
|
||||
- OWNCLOUD_ADMIN_PASSWORD=PASSWORD_DATA_2 #LIBREPORTAL|PASSWORD_TAG_2|PASSWORD_DATA_2
|
||||
- OWNCLOUD_ADMIN_USERNAME=OWNCLOUD_ADMIN_USERNAME_DATA #LIBREPORTAL|OWNCLOUD_ADMIN_USERNAME_TAG|OWNCLOUD_ADMIN_USERNAME_DATA
|
||||
- OWNCLOUD_ADMIN_PASSWORD=OWNCLOUD_ADMIN_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_ADMIN_PASSWORD_TAG|OWNCLOUD_ADMIN_PASSWORD_DATA
|
||||
- OWNCLOUD_MYSQL_UTF8MB4=true
|
||||
- OWNCLOUD_REDIS_ENABLED=true
|
||||
- OWNCLOUD_REDIS_HOST=owncloud-redis
|
||||
@ -73,13 +73,13 @@ services:
|
||||
restart: unless-stopped
|
||||
hostname: mariadb
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=PASSWORD_DATA_3 #LIBREPORTAL|PASSWORD_TAG_3|PASSWORD_DATA_3
|
||||
- MYSQL_USER=RANDOM_DATA_1 #LIBREPORTAL|RANDOM_TAG_1|RANDOM_DATA_1
|
||||
- MYSQL_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
|
||||
- MYSQL_DATABASE=RANDOM_DATA_3 #LIBREPORTAL|RANDOM_TAG_3|RANDOM_DATA_3
|
||||
- MYSQL_ROOT_PASSWORD=OWNCLOUD_DB_ROOT_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_DB_ROOT_PASSWORD_TAG|OWNCLOUD_DB_ROOT_PASSWORD_DATA
|
||||
- MYSQL_USER=OWNCLOUD_DB_USER_DATA #LIBREPORTAL|OWNCLOUD_DB_USER_TAG|OWNCLOUD_DB_USER_DATA
|
||||
- MYSQL_PASSWORD=OWNCLOUD_DB_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_DB_PASSWORD_TAG|OWNCLOUD_DB_PASSWORD_DATA
|
||||
- MYSQL_DATABASE=OWNCLOUD_DB_NAME_DATA #LIBREPORTAL|OWNCLOUD_DB_NAME_TAG|OWNCLOUD_DB_NAME_DATA
|
||||
command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"]
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=PASSWORD_DATA_3"] #LIBREPORTAL|PASSWORD_TAG_3|PASSWORD_DATA_3
|
||||
test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=OWNCLOUD_DB_ROOT_PASSWORD_DATA"] #LIBREPORTAL|OWNCLOUD_DB_ROOT_PASSWORD_TAG|OWNCLOUD_DB_ROOT_PASSWORD_DATA
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@ -25,9 +25,20 @@ CFG_OWNCLOUD_HEADSCALE=false
|
||||
# VERSION = specific version of the application to use
|
||||
# ADMIN_USERNAME = default admin username for the application
|
||||
# ADMIN_PASSWORD = default admin password (will be generated if set to RANDOMIZEDPASSWORD)
|
||||
# DB_NAME / DB_USER / DB_PASSWORD = MariaDB schema + app account used by ownCloud
|
||||
# DB_ROOT_PASSWORD = MariaDB root account; kept separate from DB_PASSWORD so the app user can be rotated without touching root
|
||||
#
|
||||
# Every value below feeds the compose via #LIBREPORTAL|OWNCLOUD_<KEY>_TAG| tags.
|
||||
# They are generated on first install and preserved across reinstalls — which is
|
||||
# what keeps them in step with the MariaDB volume, and what makes the admin
|
||||
# credentials shown in the WebUI the ones ownCloud was actually seeded with.
|
||||
#
|
||||
CFG_OWNCLOUD_ADMIN_USERNAME=RANDOMIZEDUSERNAME1
|
||||
CFG_OWNCLOUD_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1
|
||||
CFG_OWNCLOUD_DB_NAME=RANDOMIZEDUSERNAME2
|
||||
CFG_OWNCLOUD_DB_USER=RANDOMIZEDUSERNAME3
|
||||
CFG_OWNCLOUD_DB_PASSWORD=RANDOMIZEDPASSWORD2
|
||||
CFG_OWNCLOUD_DB_ROOT_PASSWORD=RANDOMIZEDPASSWORD3
|
||||
#
|
||||
# =============================================================================
|
||||
# METADATA
|
||||
|
||||
@ -64,7 +64,7 @@ services:
|
||||
environment:
|
||||
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
|
||||
- RABBITMQ_DEFAULT_USER=stoat
|
||||
- RABBITMQ_DEFAULT_PASS=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
|
||||
- RABBITMQ_DEFAULT_PASS=STOAT_RABBITMQ_PASSWORD_DATA #LIBREPORTAL|STOAT_RABBITMQ_PASSWORD_TAG|STOAT_RABBITMQ_PASSWORD_DATA
|
||||
volumes:
|
||||
- ./data/rabbit:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
@ -92,7 +92,7 @@ services:
|
||||
environment:
|
||||
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
|
||||
- MINIO_ROOT_USER=stoatminio
|
||||
- MINIO_ROOT_PASSWORD=PASSWORD_DATA_2 #LIBREPORTAL|PASSWORD_TAG_2|PASSWORD_DATA_2
|
||||
- MINIO_ROOT_PASSWORD=STOAT_MINIO_PASSWORD_DATA #LIBREPORTAL|STOAT_MINIO_PASSWORD_TAG|STOAT_MINIO_PASSWORD_DATA
|
||||
- MINIO_DOMAIN=minio
|
||||
networks:
|
||||
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
||||
@ -123,7 +123,7 @@ services:
|
||||
# leaves it for the container's shell instead of interpolating it here.
|
||||
environment:
|
||||
- MC_USER=stoatminio
|
||||
- MC_PASS=PASSWORD_DATA_2 #LIBREPORTAL|PASSWORD_TAG_2|PASSWORD_DATA_2
|
||||
- MC_PASS=STOAT_MINIO_PASSWORD_DATA #LIBREPORTAL|STOAT_MINIO_PASSWORD_TAG|STOAT_MINIO_PASSWORD_DATA
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
while ! /usr/bin/mc ready minio; do
|
||||
|
||||
@ -33,6 +33,13 @@ CFG_STOAT_AUTHELIA=false
|
||||
CFG_STOAT_HEADSCALE=false
|
||||
CFG_STOAT_ENABLE_VIDEO=true
|
||||
CFG_STOAT_MONITORING=false
|
||||
# Service credentials fed to the compose via #LIBREPORTAL|STOAT_<KEY>_TAG|.
|
||||
# Generated on first install and preserved across reinstalls: RabbitMQ writes
|
||||
# its user into ./data/rabbit on first boot and ignores the env afterwards, so a
|
||||
# regenerated password would leave the broker unreachable. MinIO's root password
|
||||
# is shared by the object store and the bucket-creation job, which have to agree.
|
||||
CFG_STOAT_RABBITMQ_PASSWORD=RANDOMIZEDPASSWORD1
|
||||
CFG_STOAT_MINIO_PASSWORD=RANDOMIZEDPASSWORD2
|
||||
#
|
||||
# =============================================================================
|
||||
# METADATA
|
||||
|
||||
@ -127,13 +127,44 @@ 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
|
||||
## 0.2.0 — App credentials moved into `<app>.config`
|
||||
|
||||
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.
|
||||
**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:
|
||||
|
||||
```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.
|
||||
|
||||
@ -35,7 +35,7 @@ processBcryptPassword()
|
||||
# Remove any single quotes from the bcrypt hash
|
||||
bcrypt_password=$(echo "$bcrypt_password" | tr -d "'")
|
||||
|
||||
local result; result=$(runCfgOp sed -i -E "s#$placeholder#$bcrypt_password#g" "$file")
|
||||
local result; result=$(runCfgOp sed -i -E "s#$placeholder\\b#$bcrypt_password#g" "$file")
|
||||
checkSuccess "Use sed to replace placeholder with bcrypt hash"
|
||||
|
||||
# Verify replacement
|
||||
|
||||
@ -1,11 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
generateRandomPassword()
|
||||
generateRandomPassword()
|
||||
{
|
||||
local password=""
|
||||
local length=${CFG_GENERATED_PASS_LENGTH:-20} # Default to 20 if not set
|
||||
|
||||
# Generate password with letters and numbers only (no special chars)
|
||||
password=$(dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 | tr -d '+/=' | head -c $length)
|
||||
local attempt
|
||||
|
||||
# Letters and numbers only, no special characters: these values are embedded
|
||||
# in connection URLs (Mattermost's Postgres DSN), interpolated into container
|
||||
# entrypoints and written into YAML, and a symbol would need different
|
||||
# escaping in each.
|
||||
#
|
||||
# Retry until the result carries at least one digit AND one letter. base64 of
|
||||
# urandom is alphanumeric but says nothing about the mix — measured over 2000
|
||||
# draws at the default length, 1 in 40 came back with no digit at all, which
|
||||
# trips any policy that requires one. At that hit rate the retry is free.
|
||||
# Bounded rather than `while true` so a pathological length (or an empty
|
||||
# /dev/urandom read) can't spin forever; a sub-2 length can't hold both
|
||||
# classes at all, so it is accepted as-is.
|
||||
for attempt in {1..50}; do
|
||||
password=$(dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 | tr -d '+/=' | head -c $length)
|
||||
[[ ${#password} -eq $length ]] || continue
|
||||
[[ $length -lt 2 ]] && break
|
||||
[[ "$password" == *[0-9]* && "$password" == *[A-Za-z]* ]] && break
|
||||
done
|
||||
|
||||
echo "$password"
|
||||
}
|
||||
|
||||
@ -7,13 +7,17 @@ replaceHexKeys()
|
||||
# Only scan for hex placeholders that actually exist in the file
|
||||
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDHEX[0-9]*' "$file" 2>/dev/null | sort -u)
|
||||
|
||||
# \b on the substitution below: `sort -u` orders slots lexically (1, 10, 11,
|
||||
# 2), so without it slot 1's pattern matches inside slot 10's placeholder and
|
||||
# slots 10+ end up holding slot 1's secret with a digit appended. See
|
||||
# password_replace.sh for the full description.
|
||||
if [[ -n "$existing_placeholders" ]]; then
|
||||
while IFS= read -r placeholder; do
|
||||
if [[ -n "$placeholder" ]]; then
|
||||
local hex_key
|
||||
hex_key=$(openssl rand -hex 32)
|
||||
|
||||
runCfgOp sed -i "s/${placeholder}/${hex_key}/g" "$file"
|
||||
runCfgOp sed -i "s/${placeholder}\\b/${hex_key}/g" "$file"
|
||||
checkSuccess "Updated ${placeholder} in $(basename "$file") with a new hex key."
|
||||
fi
|
||||
done <<< "$existing_placeholders"
|
||||
|
||||
@ -7,13 +7,17 @@ replaceVAPIDKeys()
|
||||
# Only scan for VAPID placeholders that actually exist in the file
|
||||
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDVAPID[0-9]*' "$file" 2>/dev/null | sort -u)
|
||||
|
||||
# \b on the substitution below: `sort -u` orders slots lexically (1, 10, 11,
|
||||
# 2), so without it slot 1's pattern matches inside slot 10's placeholder and
|
||||
# slots 10+ end up holding slot 1's secret with a digit appended. See
|
||||
# password_replace.sh for the full description.
|
||||
if [[ -n "$existing_placeholders" ]]; then
|
||||
while IFS= read -r placeholder; do
|
||||
if [[ -n "$placeholder" ]]; then
|
||||
local vapid_key
|
||||
vapid_key=$(openssl rand -base64 32 | tr -d '+/=' | tr -cd '[:alnum:]')
|
||||
|
||||
runCfgOp sed -i "s/${placeholder}/${vapid_key}/g" "$file"
|
||||
runCfgOp sed -i "s/${placeholder}\\b/${vapid_key}/g" "$file"
|
||||
checkSuccess "Updated ${placeholder} in $(basename "$file") with a new VAPID key."
|
||||
fi
|
||||
done <<< "$existing_placeholders"
|
||||
|
||||
@ -4,14 +4,22 @@ replacePlainPasswords()
|
||||
{
|
||||
local file="$1"
|
||||
|
||||
# Only scan for placeholders that actually exist in the file
|
||||
# Only scan for placeholders that actually exist in the file.
|
||||
#
|
||||
# The \b on the substitution below is load-bearing once a file uses ten or
|
||||
# more slots. `sort -u` orders these lexically — 1, 10, 11, 2 — so an
|
||||
# unanchored `s/RANDOMIZEDPASSWORD1/<secret>/g` runs first and rewrites the
|
||||
# RANDOMIZEDPASSWORD1 *inside* RANDOMIZEDPASSWORD10, leaving slot 10 holding
|
||||
# slot 1's secret with a stray "0" on the end. Slots 10+ then share a secret
|
||||
# derivable from slot 1, and nothing downstream notices because the values
|
||||
# aren't byte-identical. The word boundary makes the match order irrelevant.
|
||||
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDPASSWORD[0-9]+' "$file" 2>/dev/null | sort -u)
|
||||
|
||||
if [[ -n "$existing_placeholders" ]]; then
|
||||
while IFS= read -r password_placeholder; do
|
||||
if [[ -n "$password_placeholder" ]]; then
|
||||
local random_password=$(generateRandomPassword)
|
||||
runCfgOp sed -i 's/'"${password_placeholder}"'/'"${random_password}"'/g' "$file"
|
||||
runCfgOp sed -i 's/'"${password_placeholder}"'\b/'"${random_password}"'/g' "$file"
|
||||
checkSuccess "Updated ${password_placeholder} in $(basename "$file")."
|
||||
fi
|
||||
done <<< "$existing_placeholders"
|
||||
|
||||
@ -10,12 +10,16 @@ replaceLaravelAppKeys()
|
||||
|
||||
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDAPPKEY[0-9]*' "$file" 2>/dev/null | sort -u)
|
||||
|
||||
# \b on the substitution below: `sort -u` orders slots lexically (1, 10, 11,
|
||||
# 2), so without it slot 1's pattern matches inside slot 10's placeholder and
|
||||
# slots 10+ end up holding slot 1's secret with a digit appended. See
|
||||
# password_replace.sh for the full description.
|
||||
if [[ -n "$existing_placeholders" ]]; then
|
||||
while IFS= read -r placeholder; do
|
||||
if [[ -n "$placeholder" ]]; then
|
||||
local app_key
|
||||
app_key="base64:$(openssl rand -base64 32)"
|
||||
runCfgOp sed -i "s#${placeholder}#${app_key}#g" "$file"
|
||||
runCfgOp sed -i "s#${placeholder}\\b#${app_key}#g" "$file"
|
||||
checkSuccess "Updated ${placeholder} in $(basename "$file") with a new Laravel APP_KEY."
|
||||
fi
|
||||
done <<< "$existing_placeholders"
|
||||
|
||||
@ -7,11 +7,15 @@ replaceRandomUsernames()
|
||||
# Only scan for placeholders that actually exist in the file
|
||||
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDUSERNAME[0-9]+' "$file" 2>/dev/null | sort -u)
|
||||
|
||||
# \b on the substitution below: `sort -u` orders slots lexically (1, 10, 11,
|
||||
# 2), so without it slot 1's pattern matches inside slot 10's placeholder and
|
||||
# slots 10+ end up holding slot 1's secret with a digit appended. See
|
||||
# password_replace.sh for the full description.
|
||||
if [[ -n "$existing_placeholders" ]]; then
|
||||
while IFS= read -r username_placeholder; do
|
||||
if [[ -n "$username_placeholder" ]]; then
|
||||
local random_username=$(generateRandomUsername)
|
||||
runCfgOp sed -i 's/'"${username_placeholder}"'/'"${random_username}"'/g' "$file"
|
||||
runCfgOp sed -i 's/'"${username_placeholder}"'\b/'"${random_username}"'/g' "$file"
|
||||
checkSuccess "Updated ${username_placeholder} in $(basename "$file")."
|
||||
fi
|
||||
done <<< "$existing_placeholders"
|
||||
|
||||
@ -357,6 +357,111 @@ PORTEOF
|
||||
"tooltip": "MariaDB root password (auto-generated; kept separate from the app user password so root can be left alone if you rotate the app account)",
|
||||
"advanced": true
|
||||
},
|
||||
"MASTODON_DB_NAME": {
|
||||
"category": "advanced",
|
||||
"label": "Database Name",
|
||||
"type": "text",
|
||||
"tooltip": "Postgres database Mastodon uses (internal to the docker network). Set by initdb when the volume is created — changing it afterwards needs the same change in Postgres.",
|
||||
"advanced": true
|
||||
},
|
||||
"MASTODON_DB_USER": {
|
||||
"category": "advanced",
|
||||
"label": "Database User",
|
||||
"type": "text",
|
||||
"tooltip": "Postgres role Mastodon connects with (internal to the docker network). Set by initdb when the volume is created.",
|
||||
"advanced": true
|
||||
},
|
||||
"MASTODON_DB_PASSWORD": {
|
||||
"category": "advanced",
|
||||
"label": "Database Password",
|
||||
"type": "password",
|
||||
"tooltip": "Postgres password for the Mastodon role (auto-generated; persists across reinstalls)",
|
||||
"advanced": true
|
||||
},
|
||||
"MASTODON_SECRET_KEY_BASE": {
|
||||
"category": "advanced",
|
||||
"label": "Secret Key Base",
|
||||
"type": "password",
|
||||
"tooltip": "Signs and encrypts session cookies (auto-generated). Changing it logs every user out.",
|
||||
"advanced": true
|
||||
},
|
||||
"MASTODON_OTP_SECRET": {
|
||||
"category": "advanced",
|
||||
"label": "OTP Secret",
|
||||
"type": "password",
|
||||
"tooltip": "Protects stored two-factor enrolments (auto-generated). Changing it invalidates them — anyone with 2FA on needs it reset before they can log in.",
|
||||
"advanced": true
|
||||
},
|
||||
"MASTODON_VAPID_PRIVATE_KEY": {
|
||||
"category": "advanced",
|
||||
"label": "VAPID Private Key",
|
||||
"type": "password",
|
||||
"tooltip": "Web Push identity (auto-generated). Changing it makes browsers re-subscribe.",
|
||||
"advanced": true
|
||||
},
|
||||
"MASTODON_VAPID_PUBLIC_KEY": {
|
||||
"category": "advanced",
|
||||
"label": "VAPID Public Key",
|
||||
"type": "text",
|
||||
"tooltip": "Web Push identity (auto-generated). Changing it makes browsers re-subscribe.",
|
||||
"advanced": true
|
||||
},
|
||||
"OWNCLOUD_DB_NAME": {
|
||||
"category": "advanced",
|
||||
"label": "Database Name",
|
||||
"type": "text",
|
||||
"tooltip": "MariaDB schema ownCloud uses (internal to the docker network)",
|
||||
"advanced": true
|
||||
},
|
||||
"OWNCLOUD_DB_USER": {
|
||||
"category": "advanced",
|
||||
"label": "Database User",
|
||||
"type": "text",
|
||||
"tooltip": "MariaDB account ownCloud connects with (internal to the docker network)",
|
||||
"advanced": true
|
||||
},
|
||||
"OWNCLOUD_DB_PASSWORD": {
|
||||
"category": "advanced",
|
||||
"label": "Database Password",
|
||||
"type": "password",
|
||||
"tooltip": "MariaDB password for the ownCloud user (auto-generated; persists across reinstalls)",
|
||||
"advanced": true
|
||||
},
|
||||
"OWNCLOUD_DB_ROOT_PASSWORD": {
|
||||
"category": "advanced",
|
||||
"label": "Database Root Password",
|
||||
"type": "password",
|
||||
"tooltip": "MariaDB root password (auto-generated; kept separate from the app user password so root can be left alone if you rotate the app account)",
|
||||
"advanced": true
|
||||
},
|
||||
"MATTERMOST_DB_PASSWORD": {
|
||||
"category": "advanced",
|
||||
"label": "Database Password",
|
||||
"type": "password",
|
||||
"tooltip": "Postgres password for the mattermost role (auto-generated; persists across reinstalls)",
|
||||
"advanced": true
|
||||
},
|
||||
"MATRIX_DB_PASSWORD": {
|
||||
"category": "advanced",
|
||||
"label": "Database Password",
|
||||
"type": "password",
|
||||
"tooltip": "Postgres password for the synapse role (auto-generated; persists across reinstalls)",
|
||||
"advanced": true
|
||||
},
|
||||
"STOAT_RABBITMQ_PASSWORD": {
|
||||
"category": "advanced",
|
||||
"label": "RabbitMQ Password",
|
||||
"type": "password",
|
||||
"tooltip": "Broker password (auto-generated). RabbitMQ stores the user on first boot and ignores the value afterwards, so it persists across reinstalls.",
|
||||
"advanced": true
|
||||
},
|
||||
"STOAT_MINIO_PASSWORD": {
|
||||
"category": "advanced",
|
||||
"label": "MinIO Root Password",
|
||||
"type": "password",
|
||||
"tooltip": "Object store root password (auto-generated), shared by MinIO and the bucket-creation job.",
|
||||
"advanced": true
|
||||
},
|
||||
"VAULTWARDEN_ADMIN_TOKEN": {
|
||||
"category": "general",
|
||||
"label": "Vaultwarden Admin Token",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user