diff --git a/containers/mastodon/docker-compose.yml b/containers/mastodon/docker-compose.yml index 01cb99e..711c4ea 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_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: diff --git a/containers/mastodon/mastodon.config b/containers/mastodon/mastodon.config index ccbd85c..142d011 100755 --- a/containers/mastodon/mastodon.config +++ b/containers/mastodon/mastodon.config @@ -20,6 +20,31 @@ CFG_MASTODON_AUTHELIA=false CFG_MASTODON_HEADSCALE=false # # ============================================================================= +# SECRETS +# ============================================================================= +# These feed the compose via #LIBREPORTAL|MASTODON__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 diff --git a/containers/matrix/docker-compose.yml b/containers/matrix/docker-compose.yml index 5213989..cdf54d8 100644 --- a/containers/matrix/docker-compose.yml +++ b/containers/matrix/docker-compose.yml @@ -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 diff --git a/containers/matrix/matrix.config b/containers/matrix/matrix.config index 2abe9f9..f304575 100644 --- a/containers/matrix/matrix.config +++ b/containers/matrix/matrix.config @@ -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 diff --git a/containers/matrix/scripts/matrix_install_hooks.sh b/containers/matrix/scripts/matrix_install_hooks.sh index 78c4b34..e5a8f56 100644 --- a/containers/matrix/scripts/matrix_install_hooks.sh +++ b/containers/matrix/scripts/matrix_install_hooks.sh @@ -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 diff --git a/containers/mattermost/docker-compose.yml b/containers/mattermost/docker-compose.yml index 48ac9af..eab60c7 100644 --- a/containers/mattermost/docker-compose.yml +++ b/containers/mattermost/docker-compose.yml @@ -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 diff --git a/containers/mattermost/mattermost.config b/containers/mattermost/mattermost.config index 4e83408..1a3f867 100644 --- a/containers/mattermost/mattermost.config +++ b/containers/mattermost/mattermost.config @@ -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 diff --git a/containers/owncloud/docker-compose.yml b/containers/owncloud/docker-compose.yml index 488055d..a482291 100755 --- a/containers/owncloud/docker-compose.yml +++ b/containers/owncloud/docker-compose.yml @@ -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 diff --git a/containers/owncloud/owncloud.config b/containers/owncloud/owncloud.config index 2a8c42f..0c6e685 100755 --- a/containers/owncloud/owncloud.config +++ b/containers/owncloud/owncloud.config @@ -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__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 diff --git a/containers/stoat/docker-compose.yml b/containers/stoat/docker-compose.yml index aa7e624..39f319a 100644 --- a/containers/stoat/docker-compose.yml +++ b/containers/stoat/docker-compose.yml @@ -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 diff --git a/containers/stoat/stoat.config b/containers/stoat/stoat.config index 5863383..1165d4e 100644 --- a/containers/stoat/stoat.config +++ b/containers/stoat/stoat.config @@ -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__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 diff --git a/docs/guide/upgrade-notes.md b/docs/guide/upgrade-notes.md index c06867c..fbea729 100644 --- a/docs/guide/upgrade-notes.md +++ b/docs/guide/upgrade-notes.md @@ -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 `.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 `.config` as `RANDOMIZEDPASSWORD` / `RANDOMIZEDUSERNAME`, 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_`, `RANDOM_TAG_`, `HEX_TAG_`, `VAPID_TAG_`). 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 `.config` as `RANDOMIZED*` placeholders and reach +the compose through the same `#LIBREPORTAL|__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 `, 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' //docker-compose.yml +``` + +then paste each value into the matching `CFG__` in +`//.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. diff --git a/scripts/config/password/bcrypt/password_process_bcrypt.sh b/scripts/config/password/bcrypt/password_process_bcrypt.sh index aa265c1..4c3d999 100755 --- a/scripts/config/password/bcrypt/password_process_bcrypt.sh +++ b/scripts/config/password/bcrypt/password_process_bcrypt.sh @@ -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 diff --git a/scripts/config/password/password_generate.sh b/scripts/config/password/password_generate.sh index a7cd210..abb11ea 100755 --- a/scripts/config/password/password_generate.sh +++ b/scripts/config/password/password_generate.sh @@ -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" } diff --git a/scripts/config/password/password_replace hex.sh b/scripts/config/password/password_replace hex.sh index 9947caa..e787608 100755 --- a/scripts/config/password/password_replace hex.sh +++ b/scripts/config/password/password_replace hex.sh @@ -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" diff --git a/scripts/config/password/password_replace vapid.sh b/scripts/config/password/password_replace vapid.sh index 9010a3e..7c795b4 100755 --- a/scripts/config/password/password_replace vapid.sh +++ b/scripts/config/password/password_replace vapid.sh @@ -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" diff --git a/scripts/config/password/password_replace.sh b/scripts/config/password/password_replace.sh index eb92b8c..4b97524 100755 --- a/scripts/config/password/password_replace.sh +++ b/scripts/config/password/password_replace.sh @@ -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//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" diff --git a/scripts/config/password/password_replace_appkey.sh b/scripts/config/password/password_replace_appkey.sh index 60d3fe4..d1e2be4 100644 --- a/scripts/config/password/password_replace_appkey.sh +++ b/scripts/config/password/password_replace_appkey.sh @@ -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" diff --git a/scripts/config/password/password_user_replace.sh b/scripts/config/password/password_user_replace.sh index 0f7bedd..61788a3 100755 --- a/scripts/config/password/password_user_replace.sh +++ b/scripts/config/password/password_user_replace.sh @@ -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" diff --git a/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh b/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh index 1bb601a..1e742f2 100755 --- a/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh +++ b/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh @@ -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",