From c63cb4a2a7c83e41e8e2fd1aaa3d7be46057bbf4 Mon Sep 17 00:00:00 2001 From: librelad Date: Sun, 24 May 2026 20:01:52 +0100 Subject: [PATCH] fix(install): broad sudo during install, tighten to scoped only after MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install hands the heavy setup to the manager (completeInitMessage: sudo -u libreportal 'libreportal run install') — creating the docker-install user, rootless setup, apt, sysctl — which needs broad root. initUsers was installing the SCOPED sudoers up front, so that handoff died with 'sudo: a password is required' on useradd. Fix: initUsers installs a temporary NOPASSWD: ALL for the install phase; completeInitMessage calls the new initScopedSudoers to tighten to the runtime allowlist only after the install succeeds (on failure, broad sudo is left so the manual 'libreportal run install' retry works). This restores the documented 'kill NOPASSWD:ALL AFTER the runtime is set up' ordering. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- init.sh | 58 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/init.sh b/init.sh index d51b873..4fd7624 100755 --- a/init.sh +++ b/init.sh @@ -698,16 +698,42 @@ initUsers() sudo systemctl restart docker isSuccessful "User $sudo_user_name created successfully." fi - # Manager-user sudo: a SCOPED, validated /etc/sudoers.d drop-in (NOT - # NOPASSWD: ALL, and never appended to /etc/sudoers — a malformed main file - # locks out sudo entirely). Under Model A the runtime runs AS this user and - # reaches root ONLY via: the unprivileged docker-install user (data plane, - # confined by rootless), the root-owned /usr/local/lib/libreportal/ helpers - # (each a fixed, self-validated op the manager can't modify), and a fixed set - # of system binaries. Deliberately excluded: bash/su and tee/cp/chmod/chown/ - # sed/mv/rm/install (each root-equivalent). See scripts/system/libreportal-*. + # Install-phase sudo: the heavy install runs AS this user (see the handoff in + # completeInitMessage) and needs BROAD root — useradd for the docker-install + # user, rootless setup, apt, sysctl, etc. So grant a temporary validated + # NOPASSWD: ALL drop-in now (never appended to /etc/sudoers — a malformed main + # file locks out sudo entirely); completeInitMessage calls initScopedSudoers + # to tighten it to the scoped RUNTIME allowlist once the install succeeds. local sudoers_dropin="/etc/sudoers.d/${sudo_user_name}" - local install_user="${CFG_DOCKER_INSTALL_USER:-dockerinstall}" + local sudoers_tmp + sudoers_tmp=$(mktemp) + printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$sudo_user_name" > "$sudoers_tmp" + if sudo visudo -cf "$sudoers_tmp" >/dev/null 2>&1; then + sudo install -m 0440 -o root -g root "$sudoers_tmp" "$sudoers_dropin" + isSuccessful "Configured install-phase sudo for $sudo_user_name (tightened after install)." + else + isError "Refusing to install an invalid sudoers drop-in for $sudo_user_name." + fi + rm -f "$sudoers_tmp" + + initRootHelpers +} + +# Tighten the manager's sudo from the install-phase NOPASSWD: ALL down to the +# scoped RUNTIME allowlist. Called AFTER the (manager-run) install phase, which +# needs the broad root this deliberately withholds. The runtime then reaches root +# ONLY via: the unprivileged docker-install user (data plane, rootless-confined), +# the root-owned /usr/local/lib/libreportal/ helpers (each a fixed, self-validated +# op the manager can't modify), and a fixed system-binary set. Excluded: +# bash/su + tee/cp/chmod/chown/sed/mv/rm/install (each root-equivalent). Also +# clears legacy broad grants (a NOPASSWD: ALL in the main /etc/sudoers, sudo-group +# membership). See FOOTPRINT.md. +initScopedSudoers() +{ + local sudoers_dropin="/etc/sudoers.d/${sudo_user_name}" + local install_user + install_user=$(grep -h '^CFG_DOCKER_INSTALL_USER=' "$configs_dir/general/general_docker_install" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}') + install_user="${install_user:-${CFG_DOCKER_INSTALL_USER:-dockerinstall}}" local sudoers_tmp sudoers_tmp=$(mktemp) cat > "$sudoers_tmp" </dev/null 2>&1; then sudo install -m 0440 -o root -g root "$sudoers_tmp" "$sudoers_dropin" - # Defensive cleanup of legacy broad grants from older installs: a - # NOPASSWD: ALL line appended to the main /etc/sudoers, plus sudo-group - # membership (the scoped drop-in is user-specific; the group isn't needed). + # Clear legacy broad grants from older installs. if sudo grep -qE "^${sudo_user_name}[[:space:]]+ALL=\(ALL\)[[:space:]]+NOPASSWD:[[:space:]]+ALL" /etc/sudoers 2>/dev/null; then local main_tmp; main_tmp=$(mktemp) sudo grep -vE "^${sudo_user_name}[[:space:]]+ALL=\(ALL\)[[:space:]]+NOPASSWD:[[:space:]]+ALL$" /etc/sudoers > "$main_tmp" @@ -737,13 +761,11 @@ EOF rm -f "$main_tmp" fi sudo gpasswd -d "$sudo_user_name" sudo >/dev/null 2>&1 || true - isSuccessful "Configured scoped passwordless sudo for $sudo_user_name (/etc/sudoers.d/${sudo_user_name})." + isSuccessful "Tightened $sudo_user_name sudo to the scoped runtime allowlist." else - isError "Refusing to install an invalid sudoers drop-in for $sudo_user_name." + isError "Invalid scoped sudoers — left install-phase sudo in place for $sudo_user_name." fi rm -f "$sudoers_tmp" - - initRootHelpers } # Install the root-owned privilege helpers. Under Model A the runtime runs AS the @@ -1260,7 +1282,9 @@ completeInitMessage() # Switch to libreportal user and run the install command if sudo -u "$sudo_user_name" LIBREPORTAL_SKIP_LOGO=1 bash -c "libreportal run install"; then - : + # Install done — tighten the manager's broad install-phase sudo down to + # the scoped runtime allowlist. + initScopedSudoers else echo "" echo "⚠️ LibrePortal installation encountered issues."