Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
babedd08b3 Merge claude/2 2026-05-31 02:33:10 +01:00
librelad
6089eb0882 fix(de-sudo): route container-tree writes through the privileged path
Two more cases of the manager writing directly into the container-owned
/libreportal-containers tree (same class as the regen-poll stamp), both masked
by a '✓ Success' that printed anyway:

- Password replacers (config/password/*): used 'runInstallOp sed -i' (manager)
  on app configs copied into the container tree, so sed -i EACCES'd its temp
  file and the substitution silently failed — the adguard.config 'couldn't open
  temporary file', leaving the literal RANDOMIZEDPASSWORD placeholder. Added
  runCfgOp (picks runFileOp vs runInstallOp by the target file's location) and
  routed every $file grep/sed/awk through it: password, username, hex, vapid,
  appkey, and bcrypt.

- Updater generator (webui_updater_scan): 'runFileOp cp <manager-tmp>' can't
  read the manager's 0600 mktemp as the container user, so it fell through to a
  manager 'cp' that EACCES'd on the container-owned out_dir. Switched the three
  writes to 'runFileWrite < tmp' (manager shell reads the tmp; container user
  tees the write).

Both deploy via the normal quick path (relocatable scripts) — no footprint bump,
no reinstall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 02:33:10 +01:00
11 changed files with 43 additions and 18 deletions

View File

@ -20,7 +20,7 @@ exportBcryptPassword()
# Extract the correct variable name (e.g., PASSWORD_HASH) before the placeholder
local variable_name
variable_name=$(runInstallOp awk -F= '/'"$placeholder"'/ { gsub(/^[ \t-]+/, "", $1); print $1; exit }' "$file")
variable_name=$(runCfgOp awk -F= '/'"$placeholder"'/ { gsub(/^[ \t-]+/, "", $1); print $1; exit }' "$file")
if [ -n "$variable_name" ]; then
# Remove old password entries for this app & variable

View File

@ -8,7 +8,7 @@ processBcryptPassword()
# Extract the variable name before the placeholder
local variable_name
variable_name=$(runInstallOp awk -F= '/'"$placeholder"'/ { gsub(/^[ \t-]+/, "", $1); print $1; exit }' "$file")
variable_name=$(runCfgOp awk -F= '/'"$placeholder"'/ { gsub(/^[ \t-]+/, "", $1); print $1; exit }' "$file")
if [ -z "$variable_name" ]; then
isError " Could not extract variable name before $placeholder."
@ -35,11 +35,11 @@ processBcryptPassword()
# Remove any single quotes from the bcrypt hash
bcrypt_password=$(echo "$bcrypt_password" | tr -d "'")
local result=$(runInstallOp sed -i -E "s#$placeholder#$bcrypt_password#g" "$file")
local result=$(runCfgOp sed -i -E "s#$placeholder#$bcrypt_password#g" "$file")
checkSuccess "Use sed to replace placeholder with bcrypt hash"
# Verify replacement
if runInstallOp grep -q "$bcrypt_password" "$file"; then
if runCfgOp grep -q "$bcrypt_password" "$file"; then
isSuccessful "Updated $variable_name in $(basename "$file")."
else
isError "ERROR: sed failed to replace $placeholder in $file."

View File

@ -7,7 +7,7 @@ replaceBcryptPasswords()
app_name=$(basename "$(dirname "$file")")
# Only scan for bcrypt placeholders that actually exist in the file
local existing_placeholders=$(runInstallOp grep -oE 'RANDOMIZEDBCRYPTPASSWORD[0-9]*' "$file" 2>/dev/null | sort -u)
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDBCRYPTPASSWORD[0-9]*' "$file" 2>/dev/null | sort -u)
if [[ -n "$existing_placeholders" ]]; then
while IFS= read -r placeholder; do

View File

@ -5,7 +5,7 @@ replaceHexKeys()
local file="$1"
# Only scan for hex placeholders that actually exist in the file
local existing_placeholders=$(runInstallOp grep -oE 'RANDOMIZEDHEX[0-9]*' "$file" 2>/dev/null | sort -u)
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDHEX[0-9]*' "$file" 2>/dev/null | sort -u)
if [[ -n "$existing_placeholders" ]]; then
while IFS= read -r placeholder; do
@ -13,7 +13,7 @@ replaceHexKeys()
local hex_key
hex_key=$(openssl rand -hex 32)
runInstallOp sed -i "s/${placeholder}/${hex_key}/g" "$file"
runCfgOp sed -i "s/${placeholder}/${hex_key}/g" "$file"
checkSuccess "Updated ${placeholder} in $(basename "$file") with a new hex key."
fi
done <<< "$existing_placeholders"

View File

@ -5,7 +5,7 @@ replaceVAPIDKeys()
local file="$1"
# Only scan for VAPID placeholders that actually exist in the file
local existing_placeholders=$(runInstallOp grep -oE 'RANDOMIZEDVAPID[0-9]*' "$file" 2>/dev/null | sort -u)
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDVAPID[0-9]*' "$file" 2>/dev/null | sort -u)
if [[ -n "$existing_placeholders" ]]; then
while IFS= read -r placeholder; do
@ -13,7 +13,7 @@ replaceVAPIDKeys()
local vapid_key
vapid_key=$(openssl rand -base64 32 | tr -d '+/=' | tr -cd '[:alnum:]')
runInstallOp sed -i "s/${placeholder}/${vapid_key}/g" "$file"
runCfgOp sed -i "s/${placeholder}/${vapid_key}/g" "$file"
checkSuccess "Updated ${placeholder} in $(basename "$file") with a new VAPID key."
fi
done <<< "$existing_placeholders"

View File

@ -5,13 +5,13 @@ replacePlainPasswords()
local file="$1"
# Only scan for placeholders that actually exist in the file
local existing_placeholders=$(runInstallOp grep -oE 'RANDOMIZEDPASSWORD[0-9]+' "$file" 2>/dev/null | sort -u)
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)
runInstallOp sed -i 's/'"${password_placeholder}"'/'"${random_password}"'/g' "$file"
runCfgOp sed -i 's/'"${password_placeholder}"'/'"${random_password}"'/g' "$file"
checkSuccess "Updated ${password_placeholder} in $(basename "$file")."
fi
done <<< "$existing_placeholders"

View File

@ -8,14 +8,14 @@ replaceLaravelAppKeys()
{
local file="$1"
local existing_placeholders=$(runInstallOp grep -oE 'RANDOMIZEDAPPKEY[0-9]*' "$file" 2>/dev/null | sort -u)
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDAPPKEY[0-9]*' "$file" 2>/dev/null | sort -u)
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)"
runInstallOp sed -i "s#${placeholder}#${app_key}#g" "$file"
runCfgOp sed -i "s#${placeholder}#${app_key}#g" "$file"
checkSuccess "Updated ${placeholder} in $(basename "$file") with a new Laravel APP_KEY."
fi
done <<< "$existing_placeholders"

View File

@ -5,13 +5,13 @@ replaceRandomUsernames()
local file="$1"
# Only scan for placeholders that actually exist in the file
local existing_placeholders=$(runInstallOp grep -oE 'RANDOMIZEDUSERNAME[0-9]+' "$file" 2>/dev/null | sort -u)
local existing_placeholders=$(runCfgOp grep -oE 'RANDOMIZEDUSERNAME[0-9]+' "$file" 2>/dev/null | sort -u)
if [[ -n "$existing_placeholders" ]]; then
while IFS= read -r username_placeholder; do
if [[ -n "$username_placeholder" ]]; then
local random_username=$(generateRandomUsername)
runInstallOp sed -i 's/'"${username_placeholder}"'/'"${random_username}"'/g' "$file"
runCfgOp sed -i 's/'"${username_placeholder}"'/'"${random_username}"'/g' "$file"
checkSuccess "Updated ${username_placeholder} in $(basename "$file")."
fi
done <<< "$existing_placeholders"

View File

@ -77,6 +77,23 @@ runInstallWrite() {
runAsManager tee "${append_flag[@]}" "$dest" >/dev/null
}
# Run a read/edit op against a CONFIG FILE, auto-selecting elevation by where the
# file lives: the container data-plane (/libreportal-containers, install-user-owned
# in rootless) -> runFileOp; the manager-owned control plane (configs/, the clone,
# backup-location configs) -> runInstallOp. The target file must be the LAST arg
# (true for the grep/sed/awk calls in the password replacers). Without this,
# sed -i EACCES'd its own temp file whenever the manager edited an app config
# copied into the container tree (the adguard.config "couldn't open temporary
# file" bug — the substitution silently failed, leaving the placeholder).
runCfgOp() {
local _file="${!#}"
if [[ -n "$containers_dir" && "$_file" == "$containers_dir"* ]]; then
runFileOp "$@"
else
runInstallOp "$@"
fi
}
# Backup-engine command (borg/restic/kopia) run AS the dedicated backup user
# ($docker_install_user), with the environment preserved (-E) so the repo
# password and BORG_/RESTIC_/KOPIA_ env vars reach the tool. Never root — the

View File

@ -742,6 +742,7 @@ declare -gA LP_FN_MAP=(
[runAsManager]="docker/command/run_privileged.sh"
[runBackupOp]="docker/command/run_privileged.sh"
[runBinInstall]="docker/command/run_privileged.sh"
[runCfgOp]="docker/command/run_privileged.sh"
[runCrowdsec]="docker/command/run_privileged.sh"
[runFileOp]="docker/command/run_privileged.sh"
[runFileWrite]="docker/command/run_privileged.sh"
@ -1648,6 +1649,7 @@ declare -gA LP_FN_ROOT=(
[runAsManager]="scripts"
[runBackupOp]="scripts"
[runBinInstall]="scripts"
[runCfgOp]="scripts"
[runCrowdsec]="scripts"
[runFileOp]="scripts"
[runFileWrite]="scripts"
@ -2574,6 +2576,7 @@ runAppCfg() { source "${install_scripts_dir}docker/command/run_privileged.sh"; r
runAsManager() { source "${install_scripts_dir}docker/command/run_privileged.sh"; runAsManager "$@"; }
runBackupOp() { source "${install_scripts_dir}docker/command/run_privileged.sh"; runBackupOp "$@"; }
runBinInstall() { source "${install_scripts_dir}docker/command/run_privileged.sh"; runBinInstall "$@"; }
runCfgOp() { source "${install_scripts_dir}docker/command/run_privileged.sh"; runCfgOp "$@"; }
runCrowdsec() { source "${install_scripts_dir}docker/command/run_privileged.sh"; runCrowdsec "$@"; }
runFileOp() { source "${install_scripts_dir}docker/command/run_privileged.sh"; runFileOp "$@"; }
runFileWrite() { source "${install_scripts_dir}docker/command/run_privileged.sh"; runFileWrite "$@"; }

View File

@ -68,7 +68,12 @@ EOF
{ "generated_at": "$now", "apps": [${entries}
] }
EOF
runFileOp cp "$tmp" "$out_dir/updates.json" 2>/dev/null || cp "$tmp" "$out_dir/updates.json"
# Write as the container user that owns out_dir. cp'ing the manager-owned
# mktemp would fail (the container user can't read a 600 /tmp file), so the
# old `runFileOp cp || cp` fell through to a manager cp that EACCES'd on the
# container-owned dir. runFileWrite reads the tmp in this (manager) shell and
# tees it as the container user — works in both modes.
runFileWrite "$out_dir/updates.json" < "$tmp"
rm -f "$tmp"
# CVE data — pluggable. Wire trivy/grype per image here and emit per-app
@ -76,14 +81,14 @@ EOF
if [ ! -f "$out_dir/cves.json" ]; then
local ctmp; ctmp="$(mktemp)"
printf '{ "generated_at": "%s", "apps": [], "totals": { "critical": 0, "high": 0, "medium": 0, "low": 0 } }\n' "$now" > "$ctmp"
runFileOp cp "$ctmp" "$out_dir/cves.json" 2>/dev/null || cp "$ctmp" "$out_dir/cves.json"
runFileWrite "$out_dir/cves.json" < "$ctmp"
rm -f "$ctmp"
fi
# Ensure a valid (possibly empty) history file exists for the WebUI.
if [ ! -f "$out_dir/history.json" ]; then
local htmp; htmp="$(mktemp)"
printf '{ "entries": [] }\n' > "$htmp"
runFileOp cp "$htmp" "$out_dir/history.json" 2>/dev/null || cp "$htmp" "$out_dir/history.json"
runFileWrite "$out_dir/history.json" < "$htmp"
rm -f "$htmp"
fi