From 0c719b5912d9f50c9dc0a424e48bd8a02b519975 Mon Sep 17 00:00:00 2001 From: librelad Date: Sat, 23 May 2026 23:45:42 +0100 Subject: [PATCH] harden(desudo): add runInstallOp helper + convert adguard/traefik/crowdsec/dashy - New runInstallOp helper for manager install-dir/template ops (rooted: sudo; rootless: run as the current manager user, which owns the tree). - adguard.sh, traefik.sh: container-config sed -> runFileOp. - crowdsec.sh: host crowdsec systemctl/apt-get -> runSystem. - dashy_update_conf.sh: conf-file mkdir/chown/md5sum/tee -> runFileOp/ runFileWrite; docker ps/restart -> dockerCommandRun. Deferred (cross-owner copy / temp-file across /tmp<->/docker, need rootless env to bridge correctly): owncloud_setup_config.sh, adguard_auth.sh. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- containers/adguard/adguard.sh | 10 ++++---- containers/crowdsec/crowdsec.sh | 16 ++++++------- containers/traefik/traefik.sh | 16 ++++++------- .../app/containers/dashy/dashy_update_conf.sh | 24 +++++++++---------- scripts/docker/command/run_privileged.sh | 15 ++++++++++++ 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/containers/adguard/adguard.sh b/containers/adguard/adguard.sh index 9f5f1b8..91f8104 100644 --- a/containers/adguard/adguard.sh +++ b/containers/adguard/adguard.sh @@ -169,11 +169,11 @@ JSON # provided a cert. if [[ $public == "true" ]]; then - result=$(sudo sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") + result=$(runFileOp sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") checkSuccess "Setting allow_unencrypted_doh to false for Traefik" fi - result=$(sudo sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") + result=$(runFileOp sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$containers_dir$app_name/conf/AdGuardHome.yaml") checkSuccess "Setting anonymize_client_ip to true for privacy reasons" # Force the admin web bind back to 0.0.0.0:3000 inside the container. @@ -185,10 +185,10 @@ JSON local adguard_yaml="$containers_dir$app_name/conf/AdGuardHome.yaml" if [[ -f "$adguard_yaml" ]]; then # New schema (v0.107+): single `address: 0.0.0.0:NN` line under `http:`. - sudo sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml" + runFileOp sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml" # Old schema fallback: separate `bind_host:` / `bind_port:` keys. - sudo sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml" - sudo sed -i 's|^\(\s*bind_port:\s*\)[0-9]\+|\13000|' "$adguard_yaml" + runFileOp sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml" + runFileOp sed -i 's|^\(\s*bind_port:\s*\)[0-9]\+|\13000|' "$adguard_yaml" checkSuccess "Pinned AdGuardHome admin bind to 0.0.0.0:3000 (matches the compose port mapping)." fi diff --git a/containers/crowdsec/crowdsec.sh b/containers/crowdsec/crowdsec.sh index 7b0cd38..f3455a0 100644 --- a/containers/crowdsec/crowdsec.sh +++ b/containers/crowdsec/crowdsec.sh @@ -58,18 +58,18 @@ uninstallCrowdsec() echo "" echo "---- $menu_number. Stopping CrowdSec host services." echo "" - local result=$(sudo systemctl disable --now crowdsec-firewall-bouncer 2>&1) + local result=$(runSystem systemctl disable --now crowdsec-firewall-bouncer 2>&1) checkSuccess "Disabling firewall bouncer" - local result=$(sudo systemctl disable --now crowdsec 2>&1) + local result=$(runSystem systemctl disable --now crowdsec 2>&1) checkSuccess "Disabling agent" ((menu_number++)) echo "" echo "---- $menu_number. Removing CrowdSec packages." echo "" - local result=$(sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -q crowdsec crowdsec-firewall-bouncer-nftables &1) + local result=$(runSystem DEBIAN_FRONTEND=noninteractive apt-get purge -y -q crowdsec crowdsec-firewall-bouncer-nftables &1) checkSuccess "Purged packages" - local result=$(sudo DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -q &1) + local result=$(runSystem DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -q &1) checkSuccess "Removed orphaned dependencies" crowdsecToggleLibrePortalLogMounts off @@ -81,9 +81,9 @@ uninstallCrowdsec() stopCrowdsec() { isNotice "Stopping CrowdSec host services..." - local result=$(sudo systemctl stop crowdsec-firewall-bouncer 2>&1) + local result=$(runSystem systemctl stop crowdsec-firewall-bouncer 2>&1) checkSuccess "Stopped firewall bouncer" - local result=$(sudo systemctl stop crowdsec 2>&1) + local result=$(runSystem systemctl stop crowdsec 2>&1) checkSuccess "Stopped agent" } @@ -93,8 +93,8 @@ stopCrowdsec() restartCrowdsec() { isNotice "Restarting CrowdSec host services..." - local result=$(sudo systemctl restart crowdsec 2>&1) + local result=$(runSystem systemctl restart crowdsec 2>&1) checkSuccess "Restarted agent" - local result=$(sudo systemctl restart crowdsec-firewall-bouncer 2>&1) + local result=$(runSystem systemctl restart crowdsec-firewall-bouncer 2>&1) checkSuccess "Restarted firewall bouncer" } diff --git a/containers/traefik/traefik.sh b/containers/traefik/traefik.sh index ddf3774..d5798fc 100755 --- a/containers/traefik/traefik.sh +++ b/containers/traefik/traefik.sh @@ -78,7 +78,7 @@ installTraefik() checkSuccess "Copy Traefik configuration file for $app_name" # Setup Debug Level - local result=$(sudo sed -i "s|DEBUGLEVEL|$CFG_TRAEFIK_LOGGING|g" "$containers_dir$app_name/etc/traefik.yml") + local result=$(runFileOp sed -i "s|DEBUGLEVEL|$CFG_TRAEFIK_LOGGING|g" "$containers_dir$app_name/etc/traefik.yml") checkSuccess "Configured Traefik debug level with: $CFG_TRAEFIK_LOGGING for $app_name" configSetupFileWithData $app_name "traefik.yml" "etc"; @@ -92,26 +92,26 @@ installTraefik() case "$access" in local-only) - sudo sed -i 's|^\(\s*insecure:\s*\).*$|\1true|' "$traefik_yml" + runFileOp sed -i 's|^\(\s*insecure:\s*\).*$|\1true|' "$traefik_yml" # Bind PORT_1 mapping to 127.0.0.1 only. Idempotent — only # adds the prefix if it isn't already there. - sudo sed -i '/#LIBREPORTAL|PORTS_TAG_1|/ { + runFileOp sed -i '/#LIBREPORTAL|PORTS_TAG_1|/ { /127\.0\.0\.1:/! s|"\([0-9]\+:[0-9]\+\)"|"127.0.0.1:\1"| }' "$compose_yml" checkSuccess "Dashboard access: local-only (loopback :8080 + auth-protected domain)" ;; domain-only) - sudo sed -i 's|^\(\s*insecure:\s*\).*$|\1false|' "$traefik_yml" + runFileOp sed -i 's|^\(\s*insecure:\s*\).*$|\1false|' "$traefik_yml" # Drop the :8080 entrypoint entirely — nothing should listen # there if the dashboard is domain-only. - sudo sed -i '/^\s*traefik:\s*$/,/^\s*address:\s*:8080\s*$/d' "$traefik_yml" + runFileOp sed -i '/^\s*traefik:\s*$/,/^\s*address:\s*:8080\s*$/d' "$traefik_yml" checkSuccess "Dashboard access: domain-only (auth-protected via Host route only)" ;; public) - sudo sed -i 's|^\(\s*insecure:\s*\).*$|\1true|' "$traefik_yml" + runFileOp sed -i 's|^\(\s*insecure:\s*\).*$|\1true|' "$traefik_yml" # Strip any 127.0.0.1: prefix the compose port may have # picked up from a previous local-only install. - sudo sed -i '/#LIBREPORTAL|PORTS_TAG_1|/ s|"127\.0\.0\.1:\([0-9]\+:[0-9]\+\)"|"\1"|' "$compose_yml" + runFileOp sed -i '/#LIBREPORTAL|PORTS_TAG_1|/ s|"127\.0\.0\.1:\([0-9]\+:[0-9]\+\)"|"\1"|' "$compose_yml" checkSuccess "Dashboard access: public (unauthenticated :8080 on all interfaces — legacy)" ;; *) @@ -131,7 +131,7 @@ installTraefik() checkSuccess "Copy Traefik Dynamic config.yml configuration file for $app_name" # Setup Error 404 Website - local result=$(sudo sed -i "s|ERRORWEBSITE|$CFG_TRAEFIK_404_SITE|g" "$containers_dir$app_name/etc/dynamic/config.yml") + local result=$(runFileOp sed -i "s|ERRORWEBSITE|$CFG_TRAEFIK_404_SITE|g" "$containers_dir$app_name/etc/dynamic/config.yml") checkSuccess "Configured Traefik error website with URL: $CFG_TRAEFIK_404_SITE for $app_name" configSetupFileWithData $app_name "config.yml" "etc/dynamic"; diff --git a/scripts/app/containers/dashy/dashy_update_conf.sh b/scripts/app/containers/dashy/dashy_update_conf.sh index aba334f..9db475e 100755 --- a/scripts/app/containers/dashy/dashy_update_conf.sh +++ b/scripts/app/containers/dashy/dashy_update_conf.sh @@ -17,7 +17,7 @@ appDashyUpdateConf() # row, so dockerCheckAppInstalled would say not_installed and bail. # Look at the actual docker container instead — if the container # exists or the install dir is present, generate the conf. - if ! sudo docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qE '^(dashy|dashy-service)$' \ + if ! dockerCommandRun "docker ps -a --format '{{.Names}}'" "sudo" 2>/dev/null | grep -qE '^(dashy|dashy-service)$' \ && [[ ! -d "${containers_dir}dashy" ]]; then return 0 fi @@ -30,8 +30,8 @@ appDashyUpdateConf() # bootstrap and the full render — will overwrite this. _dashyWriteSkeleton() { local install_name="${CFG_INSTALL_NAME:-LibrePortal}" - sudo mkdir -p "$(dirname "$conf_file")" - sudo tee "$conf_file" >/dev/null </dev/null || true + runFileOp chown "$docker_install_user:$docker_install_user" "$conf_file" 2>/dev/null || true } if ! command -v jq >/dev/null 2>&1; then @@ -63,9 +63,9 @@ EOF fi local original_md5="" - [[ -f "$conf_file" ]] && original_md5=$(sudo md5sum "$conf_file" 2>/dev/null | awk '{print $1}') + [[ -f "$conf_file" ]] && original_md5=$(runFileOp md5sum "$conf_file" 2>/dev/null | awk '{print $1}') - sudo mkdir -p "$(dirname "$conf_file")" + runFileOp mkdir -p "$(dirname "$conf_file")" # Build the selected-id set (empty CFG = include every URL). local _selected_set="" @@ -192,7 +192,7 @@ EOF [[ -z "$page_title" ]] && page_title="Dashy - LibrePortal - ${install_name}" [[ -z "$page_desc" ]] && page_desc="Welcome to your LibrePortal Dashy dashboard!" - sudo tee "$conf_file" >/dev/null </dev/null + | runFileWrite -a "$conf_file" local IFS_BAK="$IFS" local entry while IFS= read -r entry; do @@ -267,18 +267,18 @@ EOF fi printf -- " - title: %s\n description: %s\n icon: %s\n url: %s\n statusCheck: %s\n target: %s\n" \ "$tile_title" "$tile_desc" "$icon_ref" "$url" "$status_check" "$open_target" \ - | sudo tee -a "$conf_file" >/dev/null + | runFileWrite -a "$conf_file" _total_items=$((_total_items + 1)) done <<< "${_cat_buckets[$cat]}" IFS="$IFS_BAK" done - sudo chown "$docker_install_user:$docker_install_user" "$conf_file" 2>/dev/null || true + runFileOp chown "$docker_install_user:$docker_install_user" "$conf_file" 2>/dev/null || true - local updated_md5=$(sudo md5sum "$conf_file" 2>/dev/null | awk '{print $1}') + local updated_md5=$(runFileOp md5sum "$conf_file" 2>/dev/null | awk '{print $1}') if [[ "$original_md5" != "$updated_md5" ]]; then isNotice "Dashy config changed — restarting container..." - sudo docker restart dashy-service >/dev/null 2>&1 || sudo docker restart dashy >/dev/null 2>&1 || true + dockerCommandRun "docker restart dashy-service" "sudo" >/dev/null 2>&1 || dockerCommandRun "docker restart dashy" "sudo" >/dev/null 2>&1 || true local _cat_label="categories" [[ ${#_cat_order[@]} -eq 1 ]] && _cat_label="category" isSuccessful "Restarted dashy (${#_cat_order[@]} ${_cat_label}, ${_total_items} URL(s))." diff --git a/scripts/docker/command/run_privileged.sh b/scripts/docker/command/run_privileged.sh index 2e5db5c..30f2bf5 100644 --- a/scripts/docker/command/run_privileged.sh +++ b/scripts/docker/command/run_privileged.sh @@ -50,3 +50,18 @@ runFileWrite() { runSystem() { sudo "$@" } + +# Op on the manager install dir / shipped templates — the LibrePortal clone and +# its container templates, owned by the manager user that runs the runtime. +# rooted -> sudo (install tree is root-owned; byte-identical) +# rootless -> (the manager user already owns it — no privilege) +# For copies that read the install tree and write into /docker (two different +# owners in rootless), don't use this for the whole copy — read here and pipe +# into runFileWrite so each side runs as the correct owner. +runInstallOp() { + if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then + "$@" + else + sudo "$@" + fi +}