LibrePortal/scripts/update/check_update.sh
librelad b28268a61f feat(system): "Verified" integrity check against the signed release manifest
Adds per-file integrity attestation on top of the existing signed-tarball
release flow. make_release now generates a SHA256SUMS manifest over the shipped
tree and (when a key is configured) signs it, riding both inside the release
tarball so they land in the install tree with no extra download.

lpVerifyInstall (scripts/source/verify.sh) re-hashes the install tree against
that manifest and verifies the manifest's minisign signature against the
root-owned footprint pubkey, yielding states: verified / modified / tampered /
unsigned / unverifiable / development. webuiSystemVerify writes verify_status.json
(throttled daily, force on demand, also after each update apply), surfaced as an
Integrity line + "Verify now" button on the Admin → Overview Updates card and a
row in the update details panel. `libreportal verify` exposes the same check on
the CLI.

Honest framing: this is a self-check (run by the software it verifies), so red
fires only for genuine modified/tampered states; the badge tooltip points to
out-of-band `minisign -Vm` for an independent guarantee.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 19:41:22 +01:00

250 lines
8.9 KiB
Bash
Executable File

#!/bin/bash
# webuiRunUpdate — non-interactive LibrePortal update for the WebUI.
#
# Invoked as a task via `libreportal update apply` (see cli_update_commands.sh),
# so it runs under the task processor with stdin closed and
# LIBREPORTAL_NONINTERACTIVE=1. It must therefore never block on a prompt:
# every decision is resolved up front from config, and it bails out cleanly
# (non-zero) instead of asking a question.
#
# Flow: guard -> forced update check -> if behind, run the proven file update
# (gitPerformUpdate) -> redeploy the portal so the new WebUI is live ->
# regenerate WebUI data and refresh the out-of-date status.
webuiRunUpdate()
{
isHeader "LibrePortal Update"
sourceCheckFiles;
local install_mode="${CFG_INSTALL_MODE:-git}"
local git_updates="${CFG_GIT_UPDATES:-true}"
if [[ "$install_mode" == "local" ]]; then
isError "This is a local installation — updates are managed manually, nothing to pull."
return 1
fi
if [[ "$git_updates" != "true" ]]; then
isError "Updates are disabled (CFG_GIT_UPDATES). Enable them to update from the WebUI."
return 1
fi
# Release mode: version-compare the channel's latest against the local VERSION
# and, if newer, fetch + verify the new tarball and redeploy. No config backup
# dance — lpFetchRelease replaces only the install tree; configs/logs live in
# the separate system tree.
if [[ "$install_mode" == "release" ]]; then
webuiSystemUpdateCheck "force"
local cur lat
cur=$(tr -d ' \t\n\r' < "$script_dir/VERSION" 2>/dev/null)
lat=$(lpReleaseLatestVersion 2>/dev/null)
if [[ -z "$lat" ]]; then
isError "Could not reach the release server ($(lpReleaseBaseUrl))."
return 1
fi
if ! lpVersionGt "$lat" "$cur"; then
isSuccessful "LibrePortal is already up to date (v${cur})."
return 0
fi
# If the release bumps the root-owned footprint (helpers/wrapper/unit), a
# manager-run apply can't install it — those are deliberately not
# manager-writable. Require a root re-install, which fetches + re-bakes
# everything atomically.
local inst_fp rel_fp
inst_fp=$(lpInstalledFootprintVersion 2>/dev/null)
rel_fp=$(lpReleaseLatestFootprint 2>/dev/null)
if [[ -n "$rel_fp" && "${rel_fp:-0}" -gt "${inst_fp:-0}" ]]; then
isError "Update v${lat} changes system components — it must be applied as root, not from the WebUI."
isNotice "Run on the host: curl -fsSL $(lpReleaseBaseUrl)/install.sh | sudo bash"
isNotice "(or, from the install tree: sudo ${script_dir}/init.sh --unattended init ... )"
return 1
fi
isNotice "Update found — v${cur} → v${lat}. Fetching the verified release..."
lpFetchRelease "$lat" || { isError "Release fetch failed — install unchanged."; return 1; }
isNotice "Redeploying LibrePortal with the new version..."
dockerInstallApp "libreportal"
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
webuiSystemUpdateCheck "force"
webuiSystemVerify "force"
isSuccessful "LibrePortal has been updated to v${lat}."
return 0
fi
# Credential guard — gitReset()/gitCheckGitDetails() would otherwise prompt
# for missing git details and hang the task forever. Fail fast with a clear
# message instead.
if [[ "$install_mode" == "git" ]]; then
if [[ -z "$CFG_GIT_USER" || "$CFG_GIT_USER" == "changeme" ]]; then
isError "Git credentials are not configured. Run the 'libreportal' command once on the host, then retry."
return 1
fi
if [[ "$CFG_GIT_USER" != "empty" && ( -z "$CFG_GIT_KEY" || "$CFG_GIT_KEY" == "changeme" ) ]]; then
isError "Git access token is not configured. Run the 'libreportal' command once on the host, then retry."
return 1
fi
fi
cd "$script_dir" || { isError "Cannot access the install directory ($script_dir)."; return 1; }
runAsManager git config core.fileMode false
# Force a fresh fetch + status write so the decision below (and the badge)
# reflect reality right now, not a stale throttled snapshot.
webuiSystemUpdateCheck "force"
local branch behind
branch=$(runAsManager git -C "$script_dir" rev-parse --abbrev-ref HEAD 2>/dev/null)
[[ -z "$branch" || "$branch" == "HEAD" ]] && branch="main"
behind=$(runAsManager git -C "$script_dir" rev-list --count "HEAD..refs/remotes/origin/$branch" 2>/dev/null)
[[ -z "$behind" ]] && behind=0
if [[ "$behind" -eq 0 ]]; then
isSuccessful "LibrePortal is already up to date."
return 0
fi
isNotice "Update found — $behind commit(s) behind origin/$branch. Updating now..."
# Proven file-level update: back up configs/logs, re-clone, restore.
gitPerformUpdate;
# Redeploy the portal from the freshly pulled source so the new WebUI goes
# live. This is exactly what `libreportal app install libreportal` does, so
# it's already safe to run non-interactively. It restarts the container; the
# task processor runs on the host, so this task survives the restart.
isNotice "Redeploying LibrePortal with the new version..."
dockerInstallApp "libreportal"
# Regenerate WebUI data (new version, configs, etc.) and clear the
# out-of-date flag.
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
webuiSystemUpdateCheck "force"
webuiSystemVerify "force"
isSuccessful "LibrePortal has been updated."
}
checkUpdates()
{
local param1="$1"
sourceCheckFiles;
# Skip Git updates if installation mode is local or Git updates are disabled
if [[ $CFG_INSTALL_MODE == "local" ]]; then
isNotice "Local installation detected - Git updates are disabled."
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
return
fi
if [[ $CFG_GIT_UPDATES == "true" ]]; then
isHeader "Checking for Updates"
# Ask user if they want to check for updates
while true; do
if [[ $CFG_GIT_UPDATES == "false" ]]; then
isQuestion "Would you like to check for updates? (y/n): "
read -p "" check_updates_choice
elif [[ $CFG_GIT_UPDATES == "true" ]]; then
check_updates_choice=y
fi
case $check_updates_choice in
[yY])
# DNS Query Test with Quad9
isNotice "Testing internet DNS, please wait..."
if command -v dig >/dev/null 2>&1; then
dns_check_cmd="dig +short +time=3 +tries=1 @9.9.9.9 quad9.net"
elif command -v getent >/dev/null 2>&1; then
dns_check_cmd="getent hosts quad9.net"
else
dns_check_cmd="ping -c 1 -W 3 9.9.9.9"
fi
if $dns_check_cmd >/dev/null 2>&1; then
isSuccessful "Internet DNS is working."
else
isError "Internet DNS is not working."
exit 1
fi
cd "$script_dir" || { isError " Cannot navigate to the repository directory"; exit 1; }
# Update Git to ignore changes in file permissions
runAsManager git config core.fileMode false
# Update Git with email address
runAsManager git config --global user.name "$CFG_INSTALL_NAME"
runAsManager git config --global user.email "noreply@${CFG_INSTALL_NAME,,}.libreportal.local"
# Check if there are edited (modified) files
if git status --porcelain | grep -q "^ M"; then
isNotice "There are uncommitted changes in the repository."
while true; do
isQuestion "Do you want to discard these changes and update the repository? (y/n): "
read -p "" customupdatesfound
case $customupdatesfound in
[yY])
remove_changes=true
gitCheckForUpdate;
gitCheckConfigs;
fixPermissionsBeforeStart "" "update";
sourceCheckFiles;
if [[ $init_run_flag == "true" ]]; then
isSuccessful "Starting/Restarting LibrePortal"
startLoad;
fi
;;
[nN])
isNotice "Custom changes will be kept, continuing..."
remove_changes=false
gitCheckForUpdate;
gitCheckConfigs;
fixPermissionsBeforeStart "" "update";
sourceCheckFiles;
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
;;
*)
isNotice "Please provide a valid input (y or n)."
;;
esac
done
fi
# Make sure an update happens after custom code check
if [[ $update_done != "true" ]]; then
gitCheckForUpdate;
gitCheckConfigs;
fixPermissionsBeforeStart "" "update";
sourceCheckFiles;
if [[ $init_run_flag == "true" ]]; then
isSuccessful "Starting/Restarting LibrePortal"
startLoad;
fi
fi
;;
[nN])
echo ""
isNotice "Skipping update check. Starting application..."
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
;;
*)
isNotice "Please enter y or n."
;;
esac
done
else
if [[ $init_run_flag == "true" ]]; then
startLoad;
fi
fi
}