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>
225 lines
10 KiB
Bash
225 lines
10 KiB
Bash
#!/bin/bash
|
|
|
|
# Matrix (Synapse + Element) install hooks.
|
|
#
|
|
# Synapse takes essentially no configuration from the environment — it reads
|
|
# homeserver.yaml and nothing else — so the real install work is done here:
|
|
# generate the signing key, write homeserver.yaml from the template with the
|
|
# server name and secrets filled in, write Element's config.json, and create the
|
|
# first admin account once the homeserver is answering.
|
|
|
|
matrix_install_pre()
|
|
{
|
|
local app_name="$1"
|
|
if ! appInstallCheckRequirements "$app_name" "$CFG_MATRIX_REQUIRES"; then
|
|
matrix=n
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# The homeserver's public host, read back out of the deployed compose after tag
|
|
# substitution has run. DOMAINSUBNAME_TAG_1 belongs to CFG_MATRIX_PORT_1 (the
|
|
# Synapse router), so this is the host that becomes server_name — deliberately
|
|
# not $host_setup, which for this two-host app points at Element instead.
|
|
_matrixServerName()
|
|
{
|
|
local app_name="$1"
|
|
tagsManagerGetTagContent "$containers_dir$app_name/docker-compose.yml" "DOMAINSUBNAME_TAG_1"
|
|
}
|
|
|
|
matrix_install_post_compose()
|
|
{
|
|
local app_name="$1"
|
|
local app_dir="$containers_dir$app_name"
|
|
local data_dir="$app_dir/data"
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Generating the Synapse homeserver configuration"
|
|
echo ""
|
|
|
|
local server_name
|
|
server_name=$(_matrixServerName "$app_name")
|
|
if [[ -z "$server_name" ]]; then
|
|
isError "Could not determine the homeserver name from the compose file — aborting Synapse configuration."
|
|
isNotice "Check that CFG_MATRIX_PORT_1 is public and Traefik-managed, then reinstall."
|
|
return 1
|
|
fi
|
|
|
|
# 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" "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
|
|
|
|
local result
|
|
result=$(createFolders "loud" "$docker_install_user" "$data_dir" "$app_dir/element")
|
|
checkSuccess "Creating $app_name data folders"
|
|
|
|
# Synapse signs every federated event with this key, and a peer that has
|
|
# seen one key will reject events signed by a different one. So: generate it
|
|
# exactly once, and never regenerate it over an existing install.
|
|
#
|
|
# Upstream's `generate` command is used rather than hand-rolling the key
|
|
# file, because the format encodes a key ID that other servers cache. It
|
|
# also emits a homeserver.yaml and a log config, which we throw away in
|
|
# favour of the templates below.
|
|
if [[ ! -s "$data_dir/signing.key" ]]; then
|
|
local synapse_image
|
|
synapse_image=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_VERSION_TAG")
|
|
synapse_image="matrixdotorg/synapse:${synapse_image:-latest}"
|
|
|
|
result=$(runFileOp docker run --rm \
|
|
-e SYNAPSE_SERVER_NAME="$server_name" \
|
|
-e SYNAPSE_REPORT_STATS=no \
|
|
-v "$data_dir":/data \
|
|
"$synapse_image" generate 2>&1)
|
|
checkSuccess "Generating the Synapse signing key with $synapse_image"
|
|
|
|
# `generate` names the key after the server; homeserver.yaml expects it
|
|
# at a fixed path so the file does not have to be renamed if the app is
|
|
# ever restored under a different name.
|
|
if [[ -f "$data_dir/$server_name.signing.key" ]]; then
|
|
result=$(runFileOp mv "$data_dir/$server_name.signing.key" "$data_dir/signing.key")
|
|
checkSuccess "Storing the signing key at data/signing.key"
|
|
fi
|
|
# Ours replace both of these.
|
|
result=$(runFileOp rm -f "$data_dir/homeserver.yaml" "$data_dir/$server_name.log.config")
|
|
checkSuccess "Discarding the generated config in favour of the LibrePortal template"
|
|
else
|
|
isNotice "An existing signing key was found — keeping it (regenerating would break federation)."
|
|
fi
|
|
|
|
if [[ ! -s "$data_dir/signing.key" ]]; then
|
|
isError "No signing key was produced — Synapse will not start. Check that the image could be pulled."
|
|
return 1
|
|
fi
|
|
|
|
result=$(copyResource "$app_name" "homeserver.yaml" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
|
|
checkSuccess "Copying homeserver.yaml to $data_dir"
|
|
|
|
result=$(copyResource "$app_name" "log.config" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
|
|
checkSuccess "Copying log.config to $data_dir"
|
|
|
|
local homeserver_file="$data_dir/homeserver.yaml"
|
|
|
|
# Three independent secrets, each generated fresh. registration_shared_secret
|
|
# can mint an account on this server, so it is as sensitive as an admin
|
|
# password — it is why homeserver.yaml is chmod 600 below.
|
|
local registration_secret macaroon_secret form_secret
|
|
registration_secret=$(openssl rand -hex 32)
|
|
macaroon_secret=$(openssl rand -hex 32)
|
|
form_secret=$(openssl rand -hex 32)
|
|
|
|
local enable_registration="false"
|
|
[[ "$CFG_MATRIX_ENABLE_REGISTRATION" == "true" ]] && enable_registration="true"
|
|
|
|
runFileOp sed -i "s|SYNAPSE_SERVER_NAME_PLACEHOLDER|$server_name|g" "$homeserver_file"
|
|
runFileOp sed -i "s|SYNAPSE_DB_PASSWORD_PLACEHOLDER|$db_password|g" "$homeserver_file"
|
|
runFileOp sed -i "s|SYNAPSE_REGISTRATION_SECRET_PLACEHOLDER|$registration_secret|g" "$homeserver_file"
|
|
runFileOp sed -i "s|SYNAPSE_MACAROON_SECRET_PLACEHOLDER|$macaroon_secret|g" "$homeserver_file"
|
|
runFileOp sed -i "s|SYNAPSE_FORM_SECRET_PLACEHOLDER|$form_secret|g" "$homeserver_file"
|
|
runFileOp sed -i "s|SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER|$enable_registration|g" "$homeserver_file"
|
|
checkSuccess "Writing homeserver.yaml (server_name=$server_name registration=$enable_registration)"
|
|
|
|
runFileOp chmod 600 "$homeserver_file" "$data_dir/signing.key"
|
|
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$data_dir"
|
|
checkSuccess "Restricting permissions on the Synapse secrets"
|
|
|
|
# Element is a static bundle; config.json is the only thing that makes it
|
|
# point at this homeserver rather than matrix.org.
|
|
result=$(copyResource "$app_name" "element-config.json" "element" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
|
|
checkSuccess "Copying Element configuration to $app_dir/element"
|
|
|
|
result=$(runFileOp mv "$app_dir/element/element-config.json" "$app_dir/element/config.json")
|
|
checkSuccess "Renaming Element configuration to config.json"
|
|
|
|
runFileOp sed -i "s|ELEMENT_HOMESERVER_PLACEHOLDER|$server_name|g" "$app_dir/element/config.json"
|
|
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir/element"
|
|
checkSuccess "Pointing Element at https://$server_name"
|
|
}
|
|
|
|
matrix_install_post_start()
|
|
{
|
|
local app_name="$1"
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Creating the first Matrix admin account"
|
|
echo ""
|
|
|
|
# Synapse runs its database migrations on first boot, which on an empty
|
|
# Postgres takes appreciably longer than the container takes to start.
|
|
# /health answers only once it is actually serving.
|
|
#
|
|
# Probed with python rather than curl: the Synapse image is debian-slim with
|
|
# no curl or wget in it, but python is what Synapse itself runs on, so it is
|
|
# always there.
|
|
local attempts=0
|
|
while ((attempts < 60)); do
|
|
if runFileOp docker exec matrix-synapse python -c \
|
|
"import urllib.request; urllib.request.urlopen('http://localhost:8008/health', timeout=5)" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
((attempts++))
|
|
done
|
|
|
|
if ((attempts >= 60)); then
|
|
isError "Synapse did not become ready in time — no admin account was created."
|
|
isNotice "Check 'docker logs matrix-synapse'. Once it is up, create the account with:"
|
|
isNotice " docker exec -it matrix-synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008"
|
|
return 0
|
|
fi
|
|
|
|
local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}"
|
|
local admin_pass="${CFG_MATRIX_ADMIN_PASSWORD}"
|
|
if [[ -z "$admin_pass" || "$admin_pass" == RANDOMIZEDPASSWORD* ]]; then
|
|
isNotice "No admin password is set in matrix.config — skipping admin account creation."
|
|
return 0
|
|
fi
|
|
|
|
# Idempotent in practice: on a reinstall over existing data the account
|
|
# already exists and register_new_matrix_user fails with "User ID already
|
|
# taken", which is not worth failing the install over.
|
|
local result
|
|
result=$(runFileOp docker exec matrix-synapse register_new_matrix_user \
|
|
-u "$admin_user" -p "$admin_pass" -a \
|
|
-c /data/homeserver.yaml http://localhost:8008 2>&1)
|
|
if [[ "$result" == *"already taken"* ]]; then
|
|
isNotice "Matrix admin '$admin_user' already exists — leaving the existing account alone."
|
|
else
|
|
checkSuccess "Creating Matrix admin account '$admin_user'"
|
|
fi
|
|
}
|
|
|
|
matrix_install_post()
|
|
{
|
|
local app_name="$1"
|
|
local server_name
|
|
server_name=$(_matrixServerName "$app_name")
|
|
local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}"
|
|
|
|
echo ""
|
|
isNotice "Matrix homeserver:"
|
|
echo ""
|
|
echo " Server name : ${server_name}"
|
|
echo " Your user ID : @${admin_user}:${server_name}"
|
|
echo " Password : ${CFG_MATRIX_ADMIN_PASSWORD}"
|
|
echo ""
|
|
echo " Sign in through the Element web interface, or any Matrix client"
|
|
echo " (Element mobile/desktop, FluffyChat, Nheko) using the server name"
|
|
echo " above."
|
|
echo ""
|
|
echo " Registration is ${CFG_MATRIX_ENABLE_REGISTRATION:-false}. To invite"
|
|
echo " others while it stays closed, create their accounts with:"
|
|
echo " docker exec -it matrix-synapse register_new_matrix_user \\"
|
|
echo " -c /data/homeserver.yaml http://localhost:8008"
|
|
echo ""
|
|
}
|