From 78e7651ea07f63bbf33f74b7de585df6ccadf65d Mon Sep 17 00:00:00 2001 From: librelad Date: Sun, 24 May 2026 18:09:20 +0100 Subject: [PATCH] feat(desudo): run start.sh AS the manager (Model A flip) + fix exposed writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI wrapper already runs as the manager (libreportal) but then did 'sudo ./start.sh', so the whole runtime executed as root — the reason NOPASSWD:ALL was load-bearing. Drop that sudo so start.sh runs as the manager; also drop the now-redundant sudo from the wrapper's own manager-owned ops (config sed, /docker/configs + /docker/install mkdir/cp/chown/rm, 'sudo -u libreportal' git clone, chmod). Only the 'cp -f init.sh /root/' copies stay root. Running as the manager surfaced data-plane writes that only worked under root; fixed to be owner-correct: - webui_system_metrics: .metrics_{cpu,net}_prev state via runFileWrite - atomicWriteWebUI: path-aware temp+chmod+mv (atomic same-dir rename as the path owner) instead of bare >/mv - webui_app_config last_update trigger via runFileWrite Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- init.sh | 20 +++++++++---------- .../data/generators/apps/webui_app_config.sh | 2 +- .../generators/system/webui_system_metrics.sh | 4 ++-- .../webui/data/utils/webui_atomic_write.sh | 16 +++++++++++---- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/init.sh b/init.sh index c11fc5f..34ec053 100755 --- a/init.sh +++ b/init.sh @@ -909,9 +909,9 @@ commandUpdateConfigOption() { # Replace the value, preserving comment if it existed if [[ -n "$comment_part" ]]; then - sudo sed -i "s|^$config_option=.*|$config_option=$escaped_value $comment_part|" "$config_file" + sed -i "s|^$config_option=.*|$config_option=$escaped_value $comment_part|" "$config_file" else - sudo sed -i "s|^$config_option=.*|$config_option=$escaped_value|" "$config_file" + sed -i "s|^$config_option=.*|$config_option=$escaped_value|" "$config_file" fi source "$config_file" return 0 @@ -1051,13 +1051,13 @@ sync_configs_from_install() { echo "ERROR: $src missing — clone broken." return 1 fi - sudo mkdir -p "$dst" + mkdir -p "$dst" # No-clobber: preserve the user's live config values; only add new files. - if ! sudo cp -an "$src"/. "$dst"/; then + if ! cp -an "$src"/. "$dst"/; then echo "ERROR: Failed to sync configs from $src to $dst." return 1 fi - sudo chown -R libreportal:libreportal "$dst" + chown -R libreportal:libreportal "$dst" if [ ! -f "$dst/general/general_install" ]; then echo "ERROR: $dst/general/general_install missing after sync." return 1 @@ -1070,11 +1070,11 @@ sync_configs_from_install() { } clone_repo() { - sudo rm -rf /docker/install + rm -rf /docker/install local clone_url if [ "$CFG_GIT_USER" != "empty" ]; then for clone_url in "$AUTH_HTTPS_REPO_URL" "$AUTH_HTTP_REPO_URL"; do - if sudo -u libreportal git clone -q "$clone_url" "/docker/install" 2>/dev/null; then + if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then sudo cp -f /docker/install/init.sh /root/ sync_configs_from_install || return 1 echo "SUCCESS: Clone complete. Run 'libreportal run' to continue." @@ -1085,7 +1085,7 @@ clone_repo() { return 1 fi for clone_url in "https://${CLEAN_GIT_URL}.git" "http://${CLEAN_GIT_URL}.git"; do - if sudo -u libreportal git clone -q "$clone_url" "/docker/install" 2>/dev/null; then + if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then sudo cp -f /docker/install/init.sh /root/ sync_configs_from_install || return 1 echo "SUCCESS: Clone complete. Run 'libreportal run' to continue." @@ -1111,9 +1111,9 @@ cd /docker/ if [[ $command1 == "reset" ]]; then clone_and_install elif [ -f "/docker/install/start.sh" ]; then - sudo chmod 0755 /docker/install/* + chmod 0755 /docker/install/* cd /docker/install - sudo ./start.sh "$command1" "$command2" "$command3" "$command4" "$command5" "$command6" "$command7" "$command8" "$command9" + ./start.sh "$command1" "$command2" "$command3" "$command4" "$command5" "$command6" "$command7" "$command8" "$command9" else clone_and_install fi diff --git a/scripts/webui/data/generators/apps/webui_app_config.sh b/scripts/webui/data/generators/apps/webui_app_config.sh index 519b75a..0aa083c 100755 --- a/scripts/webui/data/generators/apps/webui_app_config.sh +++ b/scripts/webui/data/generators/apps/webui_app_config.sh @@ -21,7 +21,7 @@ updateAppConfig() { # Trigger web UI refresh if needed if [ -f "$containers_dir/frontend/data/last_update" ]; then - date -Iseconds > "$containers_dir/frontend/data/last_update" + date -Iseconds | runFileWrite "$containers_dir/frontend/data/last_update" fi else echo "❌ Failed to update config for $app_name" diff --git a/scripts/webui/data/generators/system/webui_system_metrics.sh b/scripts/webui/data/generators/system/webui_system_metrics.sh index 48b3622..9d51604 100644 --- a/scripts/webui/data/generators/system/webui_system_metrics.sh +++ b/scripts/webui/data/generators/system/webui_system_metrics.sh @@ -51,7 +51,7 @@ webuiSystemMetrics() { 'BEGIN { dt = t - pt; di = i - pi; if (dt <= 0) { print "0.0" } else { printf "%.1f", (1 - di/dt) * 100 } }') fi fi - [[ -n "$cur_total" ]] && echo "$cur_total $cur_idle" > "$cpu_state" 2>/dev/null + [[ -n "$cur_total" ]] && echo "$cur_total $cur_idle" | runFileWrite "$cpu_state" 2>/dev/null # Load average (1/5/15) and 1-min load as a % of total cores. local load1 load5 load15 @@ -109,7 +109,7 @@ webuiSystemMetrics() { net_tx_rate=$(awk -v c="$cur_tx" -v p="${prev_tx:-0}" -v d="$dt" 'BEGIN{v=(c-p)/d; if(v<0)v=0; printf "%.0f", v}') fi fi - echo "$now_epoch $cur_rx $cur_tx" > "$net_state" 2>/dev/null + echo "$now_epoch $cur_rx $cur_tx" | runFileWrite "$net_state" 2>/dev/null # --- Docker summary --------------------------------------------------- local d_running=0 d_total=0 d_images=0 d_volumes=0 diff --git a/scripts/webui/data/utils/webui_atomic_write.sh b/scripts/webui/data/utils/webui_atomic_write.sh index fee39cb..a408811 100755 --- a/scripts/webui/data/utils/webui_atomic_write.sh +++ b/scripts/webui/data/utils/webui_atomic_write.sh @@ -9,17 +9,25 @@ atomicWriteWebUI() { local target_file="$2" local temp_file="${target_file}.tmp.$$" + # Every step runs as the path's owner so the manager-run runtime (Model A) + # can write the dockerinstall-owned WebUI/app files. Temp + rename share the + # target's directory, so the mv stays atomic (same filesystem, same owner). + local op="runInstallOp" wop="runInstallWrite" + if [[ "$target_file" == "$containers_dir"* || "$target_file" == /docker/containers/* ]]; then + op="runFileOp"; wop="runFileWrite" + fi + # Ensure directory exists - mkdir -p "$(dirname "$target_file")" + $op mkdir -p "$(dirname "$target_file")" # Write to temp file first - printf '%s' "$content" > "$temp_file" + printf '%s' "$content" | $wop "$temp_file" # Set proper permissions - chmod 644 "$temp_file" + $op chmod 644 "$temp_file" # Atomic rename (instantaneous - no partial reads) - mv "$temp_file" "$target_file" + $op mv "$temp_file" "$target_file" if [ $? -eq 0 ]; then echo "✓ Atomic write successful: $target_file"