Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
e8a2aa453e fix(auth): resolve slot-numbered credential keys, drop two dead ones
Only two of the four keys flagged as unused actually were. gitea and invidious
ADMIN_PASSWORD are written by their auth adapters through authPersistCfg, which
builds the name as CFG_${app^^}_${key} from a parameter — invisible to a literal
grep, which is why the earlier pass called them dead. They stay.

Worse, the slot rename broke that write path for five apps: adguard, bookstack,
gitea, invidious and nextcloud all persist ADMIN_PASSWORD, and the config now
holds ADMIN_PASSWORD_1. updateConfigOption only rewrites a key that already
exists, so the write became a no-op — the app's password would really change
while the config and the WebUI kept showing the old one.

authPersistCfg now falls back to the numbered slot when the bare key is absent,
so adapters never need to know how a credential is numbered and adding a slot
can't silently disconnect the adapter that writes it. When neither name exists
it warns and returns non-zero instead of failing silently, which surfaces a
pre-existing case: linkding's adapter persists ADMIN_USER and ADMIN_PASSWORD but
its config declares neither, and never did.

Deleted the two that really are dead: CFG_TRAEFIK_ADMIN_PASSWORD_1 (its adapter
uses CFG_TRAEFIK_USER/CFG_TRAEFIK_PASS from the system config) and
CFG_GLUETUN_CONTROL_SERVER_API_KEY_1, plus their WebUI field mappings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:55:52 +01:00
librelad
80b94fb21f fix(stoat): write every bind-mounted file before anything that can fail
The install read the generated LiveKit credentials with a plain grep, but
secrets.env is chmod 600 and owned by the docker install user while the hooks
run as the manager — so the read returned nothing, the hook errored out, and
Caddyfile and livekit.yml were never written. Compose then refused to start,
because a bind mount whose source does not exist is not a soft failure.

Read secrets through runFileOp, and reorder so the Caddyfile and the three
URL-bearing files are written first: any step that can fail now comes after
every mount source already exists. The missing LiveKit keys are downgraded from
fatal to a warning for the same reason — losing voice is worth reporting, but it
is no reason to take the other fifteen services down with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:54:49 +01:00
6 changed files with 59 additions and 36 deletions

View File

@ -40,7 +40,6 @@ CFG_GLUETUN_OPENVPN_USER=
CFG_GLUETUN_OPENVPN_PASSWORD=
CFG_GLUETUN_WIREGUARD_PRIVATE_KEY=
CFG_GLUETUN_WIREGUARD_ADDRESSES=
CFG_GLUETUN_CONTROL_SERVER_API_KEY_1=RANDOMIZEDPASSWORD1
# HEALTH_TARGETS = comma-separated host:port list pinged over HTTPS to
# confirm the VPN tunnel is healthy. Defaults are privacy-respecting
# (Mullvad — your VPN provider; EFF — privacy non-profit). Override

View File

@ -78,7 +78,7 @@ _stoatBaseUrl()
# would create directories in their place) and again once the port is known.
_stoatWriteUrlFiles()
{
local app_dir="$1" base="$2" video_enabled="$3"
local app_dir="$1" base="$2" video_enabled="$3" rabbit_pass="$4"
# ws:// for http, wss:// for https — a wss:// URL on a plain-HTTP origin
# fails to connect and the client hangs on "connecting".
@ -192,31 +192,44 @@ stoat_install_post_compose()
"$app_dir/data/caddy-data" "$app_dir/data/caddy-config")
checkSuccess "Creating $app_name data folders"
_stoatWriteSecrets "$app_dir/secrets.env"
# Read the LiveKit credentials back out — either the ones just generated or
# the ones preserved from a previous install — because livekit.yml has to
# carry the same pair the API is configured with.
local livekit_key livekit_secret
livekit_key=$(grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__KEY='\K[^']*" "$app_dir/secrets.env" 2>/dev/null)
livekit_secret=$(grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__SECRET='\K[^']*" "$app_dir/secrets.env" 2>/dev/null)
if [[ -z "$livekit_key" || -z "$livekit_secret" ]]; then
isError "Could not read the LiveKit credentials from secrets.env — voice will not work."
return 1
fi
# Ordering rule for everything below: every file bind-mounted into a
# container must be written before the first step that could fail. A missing
# mount source is not a soft failure — docker either creates a directory in
# its place or refuses to start the container, and both outcomes outlive the
# install and break every later run.
result=$(copyResource "$app_name" "Caddyfile" "" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying Caddyfile to $app_dir"
local video_enabled=""
[[ "$CFG_STOAT_ENABLE_VIDEO" != "false" ]] && video_enabled="true"
# The port is not allocated yet, so this is a guess whenever there is no
# domain. The files still have to exist now: they are bind-mounted, and
# docker would silently create directories in their place otherwise.
# stoat_install_post_start rewrites them once the real port is known.
# domain; stoat_install_post_start rewrites these once it is known.
local base
base=$(_stoatBaseUrl "$app_name")
_stoatWriteUrlFiles "$app_dir" "$base" "$video_enabled"
checkSuccess "Writing .env.web, stoat.json and Revolt.toml for $base"
_stoatWriteSecrets "$app_dir/secrets.env"
# Read the LiveKit credentials back out — either the ones just generated or
# the ones preserved from a previous install — because livekit.yml has to
# carry the same pair the API is configured with.
#
# Read via runFileOp: secrets.env is chmod 600 and owned by the docker
# install user, while these hooks run as the manager, so a plain grep gets
# EACCES and silently yields nothing.
local livekit_key livekit_secret
livekit_key=$(runFileOp grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__KEY='\K[^']*" "$app_dir/secrets.env" 2>/dev/null)
livekit_secret=$(runFileOp grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__SECRET='\K[^']*" "$app_dir/secrets.env" 2>/dev/null)
if [[ -z "$livekit_key" || -z "$livekit_secret" ]]; then
# Deliberately not fatal. livekit.yml still gets written below so the
# bind mount is a file; voice is broken until the keys are fixed, but
# the other fifteen services come up and text chat works.
isError "Could not read the LiveKit credentials from secrets.env — voice will not work."
isNotice "Fix the keys in $app_dir/secrets.env and livekit.yml, then restart $app_name."
fi
# use_external_ip lets LiveKit discover the address to advertise for WebRTC.
# The port range matches the literal UDP mapping in the compose file; change
# one and you must change the other.
@ -243,9 +256,6 @@ webhook:
EOF
checkSuccess "Writing livekit.yml"
result=$(copyResource "$app_name" "Caddyfile" "" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying Caddyfile to $app_dir"
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir"
checkSuccess "Setting ownership on the $app_name install directory"
}

View File

@ -88,4 +88,3 @@ CFG_TRAEFIK_PORT_3="traefik-service|http|80:80|disabled|tcp|false|false|false|HT
# AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user)
CFG_TRAEFIK_AUTH_PROFILE=single_password
CFG_TRAEFIK_ADMIN_USER=
CFG_TRAEFIK_ADMIN_PASSWORD_1=RANDOMIZEDPASSWORD1

View File

@ -160,8 +160,8 @@ are preserved from then on.
The convention is now uniform: **if a config key holds a generated value, its name
ends in a slot number.** `CFG_<APP>_DB_PASSWORD` became
`CFG_<APP>_DB_PASSWORD_1`, `CFG_TRAEFIK_ADMIN_PASSWORD` became
`CFG_TRAEFIK_ADMIN_PASSWORD_1`, and so on — 42 keys across the catalog. An app
`CFG_<APP>_DB_PASSWORD_1`, `CFG_GITEA_ADMIN_PASSWORD` became
`CFG_GITEA_ADMIN_PASSWORD_1`, and so on — 40 keys across the catalog. An app
that needs a second credential of the same kind just adds `_2`; nothing has to be
registered, because the tag name is derived from the key.

View File

@ -36,12 +36,39 @@ authAdapterCall() {
}
# Persist a value to CFG_<APP>_<KEY> in the per-app config file.
#
# Adapters call with the bare name (ADMIN_PASSWORD), but a key whose value is
# generated carries a slot number (CFG_<APP>_ADMIN_PASSWORD_1). Resolve to the
# slot when the bare key isn't there, so an adapter never has to know how a
# credential is numbered — and so adding a slot to a config can't quietly
# disconnect the adapter that writes it.
#
# updateConfigOption only rewrites a key that already exists; handed a name that
# is absent it just prints a notice. That is a silent failure from the caller's
# side: the app's password really did change, but the config and the WebUI carry
# on showing the old one. Hence the explicit return 1 and warning below.
authPersistCfg() {
local app="$1" key="$2" value="$3"
local cfg="${containers_dir}${app}/${app}.config"
[[ ! -f "$cfg" ]] && cfg="${install_containers_dir}/${app}/${app}.config"
[[ ! -f "$cfg" ]] && return 1
updateConfigOption "CFG_${app^^}_${key}" "$value" "$cfg"
local app_upper="${app^^}"
app_upper="${app_upper//-/_}"
local name="CFG_${app_upper}_${key}"
if ! grep -q "^${name}=" "$cfg" 2>/dev/null; then
local slotted
slotted=$(grep -oE "^CFG_${app_upper}_${key}_[0-9]+=" "$cfg" 2>/dev/null | head -1)
if [[ -n "$slotted" ]]; then
name="${slotted%=}"
else
isNotice "$app has no ${name} (or numbered slot) in $(basename "$cfg") — the new value was applied to the app but not recorded in its config."
return 1
fi
fi
updateConfigOption "$name" "$value" "$cfg"
}
# Read a tool-modal arg (pipe-encoded) and unescape pipes.

View File

@ -525,12 +525,6 @@ PORTEOF
"type": "password",
"tooltip": "Password for the mailbox created on install"
},
"TRAEFIK_ADMIN_PASSWORD_1": {
"category": "general",
"label": "Traefik Admin Password",
"type": "password",
"tooltip": "Traefik dashboard password (auto-generated)"
},
"BOOKSTACK_APP_KEY_1": {
"category": "advanced",
"label": "Application Key",
@ -604,12 +598,6 @@ PORTEOF
"type": "password",
"tooltip": "Password for the seeded Authelia admin account"
},
"GLUETUN_CONTROL_SERVER_API_KEY_1": {
"category": "general",
"label": "Control Server API Key",
"type": "password",
"tooltip": "API key for the gluetun HTTP control server (blank disables auth)"
},
"GLUETUN_VPN_SERVICE_PROVIDER": {
"category": "general",
"label": "VPN Provider",