fix(install): broad sudo during install, tighten to scoped only after

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 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-24 20:01:52 +01:00
parent d7aae3f47e
commit c63cb4a2a7

58
init.sh
View File

@ -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" <<EOF
@ -727,9 +753,7 @@ ${sudo_user_name} ALL=(root) NOPASSWD: LP_HELPERS, LP_SYSTEM
EOF
if sudo visudo -cf "$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."