refactor(desudo): route scattered runtime sudo through privilege helpers

Convert the remaining ad-hoc 'sudo' calls across the data plane to the
run_privileged helpers so every file op lands as the correct owner with
no blanket root:

- DB/configs (manager-owned): db_list_all_apps, delete_db_file,
  install_sqlite, cli_webui_commands -> runInstallOp
- containers (dockerinstall-owned): scan_container_socket, delete_data,
  webui_task_files, webui_app_log, webui_config_patch,
  application_missing_variables, uninstall_app -> runFileOp/runFileWrite
- genuine root: passwd, tailscale, ufw-docker, sysctl grep, systemd
  unit read, authorized_keys read, nobody chown -> runSystem
- interactive editors and 'id -u': drop sudo entirely (run as caller)
- owncloud/adguard container-UID config edits -> runSystem (funnel;
  docker-exec rework deferred)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-24 18:00:19 +01:00
parent fb87d0e687
commit 8b14f26125
26 changed files with 53 additions and 53 deletions

View File

@ -33,7 +33,7 @@ appUpdateSpecifics()
# under its mounted data dir; fixPermissionsBeforeStart hands the dir to
# the install user, so give it to 65534 here or the server can't open
# the database. Restart so it picks the dir up.
sudo chown -R 65534:65534 "$containers_dir$app_name/data";
runSystem chown -R 65534:65534 "$containers_dir$app_name/data";
shouldrestart="true";
fi

View File

@ -18,7 +18,7 @@ authAdapter_adguard_setPassword() {
local tmp
tmp=$(sudo mktemp)
if ! sudo awk -v u="$user" -v pw="$bcrypt" '
if ! runSystem awk -v u="$user" -v pw="$bcrypt" '
/^users:/ { in_users=1; print; next }
in_users && /^[^[:space:]-]/ { in_users=0 }
in_users && /^[[:space:]]+name:/ && !done_user {
@ -29,13 +29,13 @@ authAdapter_adguard_setPassword() {
}
{ print }
END { exit (done_pw ? 0 : 1) }
' "$yaml" | sudo tee "$tmp" >/dev/null; then
sudo rm -f "$tmp"
' "$yaml" | runSystem tee "$tmp" >/dev/null; then
runSystem rm -f "$tmp"
isError "AdGuardHome.yaml does not contain a 'users:' password line."
return 1
fi
sudo cp "$tmp" "$yaml"
sudo rm -f "$tmp"
runSystem cp "$tmp" "$yaml"
runSystem rm -f "$tmp"
authPersistCfg adguard ADMIN_USER "$user"
authPersistCfg adguard ADMIN_PASSWORD "$password"

View File

@ -19,7 +19,7 @@ appOwnCloudSetupConfig()
local owncloud_wait_time=5 # seconds
# Remove the temporary folder in /tmp
result=$(sudo rm -rf "$owncloud_config")
result=$(runSystem rm -rf "$owncloud_config")
checkSuccess "Removed default config.php"
# Check when owncloud is setup.
@ -51,40 +51,40 @@ appOwnCloudSetupConfig()
# Backups and Creation of config files
# Copy the original config.php to the temporary file
# Create a temporary folder in /tmp
result=$(sudo mkdir -p "$tmp_folder")
result=$(runSystem mkdir -p "$tmp_folder")
checkSuccess "Created temporary folder: $tmp_folder"
# Backups and Creation of config files
# Copy the original config.php to the temporary file in /tmp
result=$(sudo cp "$owncloud_config" "$tmp_folder/config.php.tmp")
result=$(runSystem cp "$owncloud_config" "$tmp_folder/config.php.tmp")
checkSuccess "Copy the original config.php contents to the temporary file"
result=$(sudo cp "$owncloud_config" "$owncloud_config_folder/config.php.backup")
result=$(runSystem cp "$owncloud_config" "$owncloud_config_folder/config.php.backup")
checkSuccess "Backing up the original config.php file"
local result=$(sudo chmod -R 777 "$tmp_folder")
local result=$(runSystem chmod -R 777 "$tmp_folder")
checkSuccess "Set permissions to for temp folder & files."
local result=$(sudo chown -R $docker_install_user:$docker_install_user "$tmp_folder")
local result=$(runSystem chown -R $docker_install_user:$docker_install_user "$tmp_folder")
checkSuccess "Updating ownership on temp folder to $docker_install_user"
# Create another temporary file for awk output
local tmp_awk_output="$tmp_folder/config_awk_output.tmp"
# Use awk to delete lines for 'trusted_domains' from the temporary file
result=$(sudo awk '/'"'trusted_domains'"'/,/\),/{next} {print}' "$tmp_folder/config.php.tmp" > "$tmp_awk_output")
result=$(runSystem awk '/'"'trusted_domains'"'/,/\),/{next} {print}' "$tmp_folder/config.php.tmp" > "$tmp_awk_output")
checkSuccess "Use awk to delete lines for 'trusted_domains' from the temporary file"
# Remove the line containing 'overwrite.cli.url'
result=$(sudo sed -i '/overwrite\.cli\.url/d' "$tmp_awk_output")
result=$(runSystem sed -i '/overwrite\.cli\.url/d' "$tmp_awk_output")
checkSuccess "Remove line containing 'overwrite.cli.url'"
# Remove the existing ');' from the end of the file
result=$(sudo sed -i '$s/);//' "$tmp_awk_output")
result=$(runSystem sed -i '$s/);//' "$tmp_awk_output")
checkSuccess "Remove ');' from the end of the file"
# Remove empty lines from the temporary file
result=$(sudo sed -i '/^ *$/d' "$tmp_awk_output")
result=$(runSystem sed -i '/^ *$/d' "$tmp_awk_output")
checkSuccess "Remove empty lines from the temporary file"
if [[ $public == "true" ]]; then
@ -117,14 +117,14 @@ fi
# Update permissions
# Move the modified temporary file back to the original location
result=$(sudo mv "$tmp_awk_output" "$owncloud_config")
result=$(runSystem mv "$tmp_awk_output" "$owncloud_config")
checkSuccess "Overwrite the original config.php with the updated content"
result=$(sudo chown 165568:$docker_install_user $owncloud_config)
result=$(runSystem chown 165568:$docker_install_user $owncloud_config)
checkSuccess "Update permissions of ownCloud config to reflect container needs."
# Remove the temporary folder in /tmp
result=$(sudo rm -rf "$tmp_folder")
result=$(runSystem rm -rf "$tmp_folder")
checkSuccess "Removed temporary folder: $tmp_folder"
else
isError "Container is not healthy. Setup seems to have failed."

View File

@ -4,7 +4,7 @@ checkDockerRootlessRequirement()
{
if [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then
### Docker Rootless
if sudo grep -q "ROOTLESS" $sysctl; then
if runSystem grep -q "ROOTLESS" $sysctl; then
isSuccessful "Docker Rootless appears to be installed."
else
isNotice "Docker Rootless does not appear to be installed."

View File

@ -22,7 +22,7 @@ checkInstallTypeRequirement()
if [[ "$OS_TYPE" == "Ubuntu" || "$OS_TYPE" == "Debian" ]]; then
ISCOMP=$( (docker compose -v ) 2>&1 )
ISUFW=$( (runSystem ufw status ) 2>&1 )
ISUFWD=$( (sudo ufw-docker) 2>&1 )
ISUFWD=$( (runSystem ufw-docker) 2>&1 )
resolveDockerInstallUser
@ -31,7 +31,7 @@ checkInstallTypeRequirement()
ISACT=$( (runSystem systemctl is-active docker ) 2>&1 )
elif [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then
# Used for checking the rootless user
local ISUSER=$( (sudo id -u "$CFG_DOCKER_INSTALL_USER"))
local ISUSER=$( (id -u "$CFG_DOCKER_INSTALL_USER"))
if [[ "$ISUSER" == *"no such user"* ]]; then
ISACT=$(command -v docker &> /dev/null)
fi

View File

@ -78,8 +78,8 @@ cliWebuiLoginReset()
# Restore placeholders so the scan re-randomizes them
if [ -f "$webui_logins_file" ]; then
sudo sed -i -E 's/^(CFG_WEBUI_USERNAME=).*$/\1RANDOMIZEDUSERNAME1/' "$webui_logins_file"
sudo sed -i -E 's/^(CFG_WEBUI_PASSWORD=).*$/\1RANDOMIZEDPASSWORD1/' "$webui_logins_file"
runInstallOp sed -i -E 's/^(CFG_WEBUI_USERNAME=).*$/\1RANDOMIZEDUSERNAME1/' "$webui_logins_file"
runInstallOp sed -i -E 's/^(CFG_WEBUI_PASSWORD=).*$/\1RANDOMIZEDPASSWORD1/' "$webui_logins_file"
fi
# Remove auth file to force credential regeneration on next container start

View File

@ -22,7 +22,7 @@ editAppConfig()
local original_checksum=$(md5sum "$config_file")
# Open the file with $CFG_TEXT_EDITOR for editing
sudo $CFG_TEXT_EDITOR "$config_file"
$CFG_TEXT_EDITOR "$config_file"
# Calculate the checksum of the edited file
local edited_checksum=$(md5sum "$config_file")

View File

@ -50,7 +50,7 @@ viewAppConfigs()
local config_file="$containers_dir/${selected_app}/${selected_app}.config"
if [ -f "$config_file" ]; then
sudo $CFG_TEXT_EDITOR "$config_file"
$CFG_TEXT_EDITOR "$config_file"
createTouch "$config_file" $sudo_user_name
echo ""
isNotice "Configuration file for '$selected_app' has been updated."

View File

@ -9,7 +9,7 @@ checkApplicationsConfigFilesMissingVariables()
app=$(basename "$live" .config)
remote="$install_containers_dir$app/$app.config"
reconcileConfigFile "$live" "$remote"
done < <(sudo find "$containers_dir" -maxdepth 2 -type f -name '*.config' ! -name '*.bak')
done < <(runFileOp find "$containers_dir" -maxdepth 2 -type f -name '*.config' ! -name '*.bak')
isSuccessful "Application config reconciliation completed."
}

View File

@ -51,7 +51,7 @@ viewLibrePortalConfigs()
# Check if the specific config matches either the display name or config name
if [[ "$display_name" == "$specific_config" ]] || [[ "$config_name" == "$specific_config" ]]; then
sudo $CFG_TEXT_EDITOR "${config_files[i]}"
$CFG_TEXT_EDITOR "${config_files[i]}"
createTouch "${config_files[i]}" $sudo_user_name
echo ""
isNotice "Configuration file '$display_name' has been updated."
@ -109,7 +109,7 @@ viewLibrePortalConfigs()
local selected_file="${config_files[index]}"
local selected_display_name="${config_display_names[index]}"
sudo $CFG_TEXT_EDITOR "$selected_file"
$CFG_TEXT_EDITOR "$selected_file"
createTouch "$selected_file" $sudo_user_name
config_timestamps["$selected_display_name"]=$(stat -c "%y" "$selected_file")

View File

@ -84,7 +84,7 @@ viewComposeFiles()
local index=$((file_number - 1))
if ((index >= 0 && index < ${#selected_compose_files[@]})); then
local selected_file="${selected_compose_files[index]}"
sudo $CFG_TEXT_EDITOR "$selected_file"
$CFG_TEXT_EDITOR "$selected_file"
fi
done

View File

@ -5,7 +5,7 @@ databaseListAllApps()
if [[ "$toollistallapps" == [yY] ]]; then
# Check if sqlite3 is available
if ! command -v sudo sqlite3 &> /dev/null; then
if ! command -v runInstallOp sqlite3 &> /dev/null; then
isNotice "sqlite3 command not found. Make sure it's installed."
fi
@ -17,7 +17,7 @@ databaseListAllApps()
isHeader "Listing full apps database"
# Execute the SQLite query and store the output in a variable
local output=$(sudo sqlite3 -header -column $docker_dir/$db_file "SELECT * FROM apps;")
local output=$(runInstallOp sqlite3 -header -column $docker_dir/$db_file "SELECT * FROM apps;")
# Count the number of non-header lines (data rows) in the 'output'
local num_data_rows=$(echo "$output" | grep -v '^name[[:space:]]|')

View File

@ -3,7 +3,7 @@
databaseRemoveFile()
{
if [[ "$tooldeletedb" == [yY] ]]; then
local result=$(sudo rm $docker_dir/$db_file)
local result=$(runInstallOp rm $docker_dir/$db_file)
checkSuccess "Removing $db_file file"
fi
}

View File

@ -10,10 +10,10 @@ installSQLiteDatabase()
# Create SQLite database file
if [ ! -e "$docker_dir/$db_file" ]; then
local result=$(sudo touch $docker_dir/$db_file)
local result=$(runInstallOp touch $docker_dir/$db_file)
checkSuccess "Creating SQLite $db_file file"
local result=$(sudo chmod 755 $docker_dir/$db_file && sudo chown $sudo_user_name $docker_dir/$db_file)
local result=$(runInstallOp chmod 755 $docker_dir/$db_file)
checkSuccess "Changing permissions for SQLite $db_file file"
fi

View File

@ -7,7 +7,7 @@ dockerDeleteData()
if [[ "$app_name" == "" ]]; then
isError "No app_name provided, unable to continue..."
else
local result=$(sudo rm -rf $containers_dir$app_name)
local result=$(runFileOp rm -rf $containers_dir$app_name)
checkSuccess "Deleting $app_name install folder"
fi

View File

@ -98,12 +98,12 @@ dockerUninstallApp()
[[ ! -f "$_tf" ]] && continue
# Skip in-flight tasks — that includes the uninstall task
# we're currently inside, plus anything queued or running.
if sudo grep -qE "\"status\"[[:space:]]*:[[:space:]]*\"(running|queued|pending)\"" "$_tf" 2>/dev/null; then
if runFileOp grep -qE "\"status\"[[:space:]]*:[[:space:]]*\"(running|queued|pending)\"" "$_tf" 2>/dev/null; then
continue
fi
if sudo grep -q "\"app\"[[:space:]]*:[[:space:]]*\"${stored_app_name}\"" "$_tf" 2>/dev/null; then
if runFileOp grep -q "\"app\"[[:space:]]*:[[:space:]]*\"${stored_app_name}\"" "$_tf" 2>/dev/null; then
local _id=$(basename "$_tf" .json)
sudo rm -f "$_tf" "$_tasks_dir/${_id}.log" "$_tasks_dir/${_id}.cancel" 2>/dev/null
runFileOp rm -f "$_tf" "$_tasks_dir/${_id}.log" "$_tasks_dir/${_id}.cancel" 2>/dev/null
_removed=$((_removed + 1))
fi
done

View File

@ -16,12 +16,12 @@ dockerSwitcherScanContainersForSocket()
fi
isSuccessful "Found Docker socket to change in file: $file"
if [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then
local result=$(sudo sed -i \
local result=$(runFileOp sed -i \
-e "/#SOCKETHERE/s|.*| - /run/user/${docker_install_user_id}/docker.sock:/run/user/${docker_install_user_id}/docker.sock:ro #SOCKETHERE|" \
"$file")
checkSuccess "Updating docker socket for $app_name"
elif [[ $CFG_DOCKER_INSTALL_TYPE == "rooted" ]]; then
local result=$(sudo sed -i \
local result=$(runFileOp sed -i \
-e "/#SOCKETHERE/s|.*| - $docker_rooted_socket:$docker_rooted_socket:ro #SOCKETHERE|" \
"$file")
checkSuccess "Updating docker socket for $app_name"

View File

@ -2,6 +2,6 @@
updateDockerSudoPassword()
{
local result=$(echo -e "$CFG_LIBREPORTAL_USER_PASS\n$CFG_LIBREPORTAL_USER_PASS" | sudo passwd "$sudo_user_name" > /dev/null 2>&1)
local result=$(echo -e "$CFG_LIBREPORTAL_USER_PASS\n$CFG_LIBREPORTAL_USER_PASS" | runSystem passwd "$sudo_user_name" > /dev/null 2>&1)
checkSuccess "Updating the password for the $sudo_user_name user"
}

View File

@ -2,6 +2,6 @@
updateDockerInstallPassword()
{
local result=$(echo -e "$CFG_DOCKER_INSTALL_PASS\n$CFG_DOCKER_INSTALL_PASS" | sudo passwd "$CFG_DOCKER_INSTALL_USER" > /dev/null 2>&1)
local result=$(echo -e "$CFG_DOCKER_INSTALL_PASS\n$CFG_DOCKER_INSTALL_PASS" | runSystem passwd "$CFG_DOCKER_INSTALL_USER" > /dev/null 2>&1)
checkSuccess "Updating the password for the $CFG_DOCKER_INSTALL_USER user"
}

View File

@ -13,7 +13,7 @@ setupHeadscaleLocalhost()
setupHeadscaleGenerateAuthKey;
result=$(sudo tailscale up --login-server $headscale_live_hostname --authkey $headscale_preauthkey --force-reauth)
result=$(runSystem tailscale up --login-server $headscale_live_hostname --authkey $headscale_preauthkey --force-reauth)
checkSuccess "Connecting $app_name to Headscale Server"
result=$(rm -rf $headscale_preauthkey_file)
@ -32,7 +32,7 @@ setupHeadscaleLocalhost()
result=$(cd ~ && curl -fsSL https://tailscale.com/install.sh | sh)
checkSuccess "Setting up Headscale"
result=$(sudo tailscale up --login-server https://$CFG_HEADSCALE_HOST --authkey $CFG_HEADSCALE_KEY --force-reauth)
result=$(runSystem tailscale up --login-server https://$CFG_HEADSCALE_HOST --authkey $CFG_HEADSCALE_KEY --force-reauth)
checkSuccess "Connecting $app_name to $CFG_HEADSCALE_HOST Headscale Server"
fi
fi

View File

@ -31,7 +31,7 @@ viewLogs()
;;
e)
isNotice "Viewing libreportal.log:"
sudo $CFG_TEXT_EDITOR "$logs_dir/libreportal.log"
$CFG_TEXT_EDITOR "$logs_dir/libreportal.log"
viewLogs;
;;
x)

View File

@ -58,7 +58,7 @@ webuiPatchAppConfigJson() {
.apps |= map(if ((.command // "") | endswith(" " + $slug)) then .config = $cfg | .installed = $inst else . end)
' "$apps_json" > "$tmp" 2>/dev/null; then
runFileWrite "$apps_json" < "$tmp"; rm -f "$tmp"
sudo chown "$docker_install_user:$docker_install_user" "$apps_json" 2>/dev/null || true
runFileOp chown "$docker_install_user:$docker_install_user" "$apps_json" 2>/dev/null || true
return 0
fi
rm -f "$tmp"

View File

@ -28,7 +28,7 @@ webuiGenerateSshAccess()
fi
local keys_json="[" first=true line type comment info fpr
if sudo test -f "$akf"; then
if runSystem test -f "$akf"; then
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
type=$(awk '{print $1}' <<< "$line")
@ -39,7 +39,7 @@ webuiGenerateSshAccess()
$first || keys_json+=","
first=false
keys_json+="{\"type\":\"$(jsonEscape "$type")\",\"fingerprint\":\"$(jsonEscape "$fpr")\",\"comment\":\"$(jsonEscape "$comment")\"}"
done < <(sudo cat "$akf")
done < <(runSystem cat "$akf")
fi
keys_json+="]"

View File

@ -16,7 +16,7 @@ webuiEnsureTaskFiles() {
if [ ! -f "$task_dir/queue.json" ]; then
echo " Creating queue.json"
createTouch "$task_dir/queue.json" $docker_install_user "silent"
local result=$(echo "[]" | sudo tee "$task_dir/queue.json" > /dev/null)
local result=$(echo "[]" | runFileWrite "$task_dir/queue.json" > /dev/null)
checkSuccess "Created queue.json..."
else
echo " queue.json exists"
@ -26,7 +26,7 @@ webuiEnsureTaskFiles() {
if [ ! -f "$task_dir/current.json" ]; then
echo " Creating current.json"
createTouch "$task_dir/current.json" $docker_install_user "silent"
local result=$(echo '{}' | sudo tee "$task_dir/current.json" > /dev/null)
local result=$(echo '{}' | runFileWrite "$task_dir/current.json" > /dev/null)
checkSuccess "Created current.json..."
else
echo " current.json exists"

View File

@ -19,7 +19,7 @@ webuiUpdateAppLog()
# Create WebUI log file if it doesn't exist
if [ ! -f "${log_file}" ]; then
createTouch "$log_file" $sudo_user_name "silent"
echo "=== LibrePortal Installation Started at $(date) ===" | sudo tee "${log_file}" > /dev/null
echo "=== LibrePortal Installation Started at $(date) ===" | runFileWrite "${log_file}" > /dev/null
fi
elif [[ "$type" == "uninstall" ]]; then
# Remove app log file

View File

@ -67,7 +67,7 @@ EOF
)"
local current=""
[[ -f "$service_file" ]] && current="$(sudo cat "$service_file" 2>/dev/null)"
[[ -f "$service_file" ]] && current="$(runSystem cat "$service_file" 2>/dev/null)"
if [[ "$desired" != "$current" ]]; then
printf '%s\n' "$desired" | runSystem tee "$service_file" > /dev/null