feat(layout): three-root split + ownership model (phase 2)
Split the single tree into three owner-isolated roots and fix the backup permission failure (restic, running as the container user, could not write the manager-owned /docker/backups). Ownership helper (libreportal-ownership), rewritten for three baked roots: SYSTEM_DIR (manager) CONTAINERS_DIR + BACKUPS_DIR (container user) - reconcile now drives each tree to its single owner; backups + the WebUI dir go to the container user (the actual fix). The container user reaches only the WebUI bind-mount sources (configs/webui/*) via a scoped _webui_bind_access — traverse the system root + configs, read configs/webui only, nothing else. - defence-in-depth: refuse dangerous/relative roots even if mis-baked; new backups-top action. Baking: init.sh initRootHelpers now seds __SYSTEM_DIR__/__CONTAINERS_DIR__/ __BACKUPS_DIR__ (alongside __MANAGER__) into every helper at install — the trust boundary stays root-controlled. svc/socket/appcfg helpers updated to derive from the baked SYSTEM_DIR; the svc unit now exports LP_*_DIR so the processor resolves roots authoritatively. A baking-safe '*"__"*' sentinel check survives the sed. Install/uninstall: initFolders creates the three roots; initContainerLayer hands containers + backups to the container user; uninstall removes all three (idempotent on legacy single-tree installs). Remaining functional /docker literals in init.sh (config reads, setupConfigsFromRepo, uninstall) parameterised. Compose: the WebUI's two relative ../../configs mounts (the only cross-tree relative mounts in the tree) are now absolute, filled at generation via a new CONFIGS_DIR_TAG; CONTAINERS_DIR_TAG likewise for the LP_CONTAINERS_DIR env. Live box unaffected: installed helpers + the live compose only change on reinstall/ rebuild (both of which fill the tags); the CLI-wrapper heredoc paths are baked in phase 3. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
fc2c6a6197
commit
edcdf00aca
@ -21,8 +21,10 @@ services:
|
|||||||
- ./backend/utils:/app/backend/utils
|
- ./backend/utils:/app/backend/utils
|
||||||
- ./backend/server.js:/app/backend/server.js
|
- ./backend/server.js:/app/backend/server.js
|
||||||
- ./libreportal.config:/app/libreportal.config:ro
|
- ./libreportal.config:/app/libreportal.config:ro
|
||||||
- ../../configs/webui/webui_logins:/app/webui_logins:ro
|
# Absolute (filled at generation) — the containers root is now separate from
|
||||||
- ../../configs/webui/webui_logs:/app/webui_logs:ro
|
# the system tree, so the old relative ../../configs no longer reaches it.
|
||||||
|
- CONFIGS_DIR_DATA/webui/webui_logins:/app/webui_logins:ro #LIBREPORTAL|CONFIGS_DIR_TAG|CONFIGS_DIR_DATA
|
||||||
|
- CONFIGS_DIR_DATA/webui/webui_logs:/app/webui_logs:ro #LIBREPORTAL|CONFIGS_DIR_TAG|CONFIGS_DIR_DATA
|
||||||
# >>> crowdsec-host-logs >>>
|
# >>> crowdsec-host-logs >>>
|
||||||
#- /var/log/crowdsec.log:/host/var/log/crowdsec.log:ro
|
#- /var/log/crowdsec.log:/host/var/log/crowdsec.log:ro
|
||||||
#- /var/log/crowdsec-firewall-bouncer.log:/host/var/log/crowdsec-firewall-bouncer.log:ro
|
#- /var/log/crowdsec-firewall-bouncer.log:/host/var/log/crowdsec-firewall-bouncer.log:ro
|
||||||
|
|||||||
58
init.sh
58
init.sh
@ -220,7 +220,7 @@ if [[ "${BASH_SOURCE[0]}" == "$0" && "$param1" != "uninstall" && -z "$param7" ]]
|
|||||||
# downgrade an existing git install to local (that disables the updater
|
# downgrade an existing git install to local (that disables the updater
|
||||||
# and blanks the saved creds). Honor a git URL already saved from a
|
# and blanks the saved creds). Honor a git URL already saved from a
|
||||||
# prior install — only fall back to local when there's no git history.
|
# prior install — only fall back to local when there's no git history.
|
||||||
saved_git_url=$(grep -E '^CFG_GIT_URL=' /docker/configs/general/general_install 2>/dev/null \
|
saved_git_url=$(grep -E '^CFG_GIT_URL=' "${configs_dir}general/general_install" 2>/dev/null \
|
||||||
| sed -E 's/^[^=]+=([^[:space:]#]*).*/\1/')
|
| sed -E 's/^[^=]+=([^[:space:]#]*).*/\1/')
|
||||||
if [[ -n "$param3" || -n "$param4" || -n "$param5" ]]; then
|
if [[ -n "$param3" || -n "$param4" || -n "$param5" ]]; then
|
||||||
# Git parameters provided, set to git mode
|
# Git parameters provided, set to git mode
|
||||||
@ -845,7 +845,15 @@ initRootHelpers()
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
helper_tmp=$(mktemp)
|
helper_tmp=$(mktemp)
|
||||||
sed "s/__MANAGER__/${sudo_user_name}/g" "$helper_src" > "$helper_tmp"
|
# Bake the manager name + the three relocatable roots into the installed
|
||||||
|
# (root-owned, manager-immutable) helper. This is the trust boundary: the
|
||||||
|
# helpers operate on FIXED paths chosen at install by root, never read from
|
||||||
|
# manager-writable config. '#' delimiter since the values are paths.
|
||||||
|
sed -e "s/__MANAGER__/${sudo_user_name}/g" \
|
||||||
|
-e "s#__SYSTEM_DIR__#${LP_SYSTEM_DIR}#g" \
|
||||||
|
-e "s#__CONTAINERS_DIR__#${LP_CONTAINERS_DIR}#g" \
|
||||||
|
-e "s#__BACKUPS_DIR__#${LP_BACKUPS_DIR}#g" \
|
||||||
|
"$helper_src" > "$helper_tmp"
|
||||||
if bash -n "$helper_tmp" 2>/dev/null; then
|
if bash -n "$helper_tmp" 2>/dev/null; then
|
||||||
sudo install -m 0755 -o root -g root "$helper_tmp" "$helper_dst"
|
sudo install -m 0755 -o root -g root "$helper_tmp" "$helper_dst"
|
||||||
isSuccessful "Installed root-owned helper ($helper)."
|
isSuccessful "Installed root-owned helper ($helper)."
|
||||||
@ -900,20 +908,24 @@ initContainerLayer()
|
|||||||
isSuccessful "Created container user '$duser'."
|
isSuccessful "Created container user '$duser'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# /docker is manager-owned and initFolders makes it 750; give it the rootless
|
# The system root is manager-owned and initFolders makes it 750; give it the
|
||||||
# traversal bit (o+x → 751, its documented rootless mode) so the container
|
# rootless traversal bit (o+x → 751) so the container user can reach the few
|
||||||
# user can traverse INTO /docker to reach its containers/ dir. Without this
|
# bind-mount sources it must read there (configs/webui/*). The container + backup
|
||||||
# the boot scan can't enter /docker at all, no matter who owns containers/.
|
# roots are SEPARATE roots now, so the container user no longer traverses the
|
||||||
|
# system tree to reach its own data.
|
||||||
[[ -d "$docker_dir" ]] && sudo chmod o+x "$docker_dir"
|
[[ -d "$docker_dir" ]] && sudo chmod o+x "$docker_dir"
|
||||||
|
|
||||||
# Hand containers/ to the container user (it owns per-app data in rootless) so
|
# Hand the container + backup roots to the container user — it owns per-app data
|
||||||
# the manager-run startup config scans can read it. 751: owner full; the
|
# in rootless, and restic (which runs AS that user) writes the backup repos.
|
||||||
# manager (other) can traverse in to known paths (it lists/writes via runFileOp).
|
# 751: owner full; the manager (other) can traverse in to known paths.
|
||||||
if [[ -d "$containers_dir" ]]; then
|
local d
|
||||||
sudo chown "$duser:$duser" "$containers_dir"
|
for d in "$containers_dir" "$backup_dir"; do
|
||||||
sudo chmod 751 "$containers_dir"
|
if [[ -d "$d" ]]; then
|
||||||
isSuccessful "containers/ handed to '$duser' (+ /docker traversable)."
|
sudo chown "$duser:$duser" "$d"
|
||||||
fi
|
sudo chmod 751 "$d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
isSuccessful "containers/ + backups/ handed to '$duser' (system root traversable)."
|
||||||
}
|
}
|
||||||
|
|
||||||
setupConfigsFromRepo()
|
setupConfigsFromRepo()
|
||||||
@ -921,7 +933,7 @@ setupConfigsFromRepo()
|
|||||||
isNotice "Setting up configuration files from repository..."
|
isNotice "Setting up configuration files from repository..."
|
||||||
|
|
||||||
local src="$script_dir/configs"
|
local src="$script_dir/configs"
|
||||||
local dst="/docker/configs"
|
local dst="${configs_dir%/}"
|
||||||
|
|
||||||
if [[ ! -d "$src" ]]; then
|
if [[ ! -d "$src" ]]; then
|
||||||
isError "Source configs directory missing: $src"
|
isError "Source configs directory missing: $src"
|
||||||
@ -1334,7 +1346,7 @@ initUpdateConfigs()
|
|||||||
initUpdateConfigOption "CFG_INSTALL_MODE" "$param7" && isSuccessful "Updated Installation Mode"
|
initUpdateConfigOption "CFG_INSTALL_MODE" "$param7" && isSuccessful "Updated Installation Mode"
|
||||||
|
|
||||||
isHeader "Verifying Saved Configuration"
|
isHeader "Verifying Saved Configuration"
|
||||||
local cfg_file="/docker/configs/general/general_install"
|
local cfg_file="${configs_dir}general/general_install"
|
||||||
if [[ ! -f "$cfg_file" ]]; then
|
if [[ ! -f "$cfg_file" ]]; then
|
||||||
isError "Expected $cfg_file is missing — install cannot proceed."
|
isError "Expected $cfg_file is missing — install cannot proceed."
|
||||||
exit 1
|
exit 1
|
||||||
@ -1402,7 +1414,7 @@ runFullUninstall()
|
|||||||
{
|
{
|
||||||
local mgr="${sudo_user_name:-libreportal}"
|
local mgr="${sudo_user_name:-libreportal}"
|
||||||
local iuser
|
local iuser
|
||||||
iuser=$(grep -h '^CFG_DOCKER_INSTALL_USER=' /docker/configs/general/general_docker_install 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
iuser=$(grep -h '^CFG_DOCKER_INSTALL_USER=' "${configs_dir}general/general_docker_install" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
||||||
iuser="${iuser:-dockerinstall}"
|
iuser="${iuser:-dockerinstall}"
|
||||||
|
|
||||||
# --skip-docker-images: keep the rootless docker layer (the daemon, the
|
# --skip-docker-images: keep the rootless docker layer (the daemon, the
|
||||||
@ -1415,7 +1427,9 @@ runFullUninstall()
|
|||||||
isHeader "LibrePortal — FULL Uninstall"
|
isHeader "LibrePortal — FULL Uninstall"
|
||||||
isError "This PERMANENTLY removes EVERYTHING — there is no undo:"
|
isError "This PERMANENTLY removes EVERYTHING — there is no undo:"
|
||||||
echo " - all containers + images + the rootless docker setup"
|
echo " - all containers + images + the rootless docker setup"
|
||||||
echo " - /docker (ALL app data, configs, database)"
|
echo " - $docker_dir (system: configs, database, install)"
|
||||||
|
echo " - $containers_dir (live app data)"
|
||||||
|
echo " - $backup_dir (backup repos)"
|
||||||
echo " - the '$mgr' and '$iuser' users + their home directories"
|
echo " - the '$mgr' and '$iuser' users + their home directories"
|
||||||
echo " - /usr/local/lib/libreportal/ + the /usr/local/bin/libreportal command"
|
echo " - /usr/local/lib/libreportal/ + the /usr/local/bin/libreportal command"
|
||||||
echo " - /etc/sudoers.d/$mgr, the systemd service, the sysctl drop-ins"
|
echo " - /etc/sudoers.d/$mgr, the systemd service, the sysctl drop-ins"
|
||||||
@ -1484,9 +1498,11 @@ runFullUninstall()
|
|||||||
rm -f /root/init.sh
|
rm -f /root/init.sh
|
||||||
isSuccessful "Removed the system-integration footprint"
|
isSuccessful "Removed the system-integration footprint"
|
||||||
|
|
||||||
# 4. Remove all app data.
|
# 4. Remove all app data — the three roots (on a legacy single-tree install the
|
||||||
rm -rf /docker
|
# container/backup roots are subdirs of the system root, so this is safe and
|
||||||
isSuccessful "Removed /docker"
|
# idempotent either way).
|
||||||
|
rm -rf "$docker_dir" "$containers_dir" "$backup_dir"
|
||||||
|
isSuccessful "Removed $docker_dir, $containers_dir, $backup_dir"
|
||||||
|
|
||||||
# 5. Remove the LibrePortal users + their subuid/subgid ranges + home dirs.
|
# 5. Remove the LibrePortal users + their subuid/subgid ranges + home dirs.
|
||||||
# Terminate each user's session/linger and kill its processes first, or
|
# Terminate each user's session/linger and kill its processes first, or
|
||||||
|
|||||||
@ -24,6 +24,9 @@ tagsProcessorStandardReplacements()
|
|||||||
# Host live-app-data root, passed into the WebUI container (only the
|
# Host live-app-data root, passed into the WebUI container (only the
|
||||||
# libreportal compose carries this tag; "only update tags that exist").
|
# libreportal compose carries this tag; "only update tags that exist").
|
||||||
tagsManagerUpdateUniversalTag "$full_file_path" "CONTAINERS_DIR_TAG" "${containers_dir%/}"
|
tagsManagerUpdateUniversalTag "$full_file_path" "CONTAINERS_DIR_TAG" "${containers_dir%/}"
|
||||||
|
# Host system-tree configs root — absolute bind-mount source for the WebUI's
|
||||||
|
# configs/webui/* (the containers root is separate from the system tree now).
|
||||||
|
tagsManagerUpdateUniversalTag "$full_file_path" "CONFIGS_DIR_TAG" "${configs_dir%/}"
|
||||||
|
|
||||||
isSuccessful "Standard LibrePortal tag replacements applied using universal tag manager"
|
isSuccessful "Standard LibrePortal tag replacements applied using universal tag manager"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,12 @@ set -u
|
|||||||
|
|
||||||
[[ $EUID -eq 0 ]] || { echo "libreportal-appcfg: must run as root" >&2; exit 1; }
|
[[ $EUID -eq 0 ]] || { echo "libreportal-appcfg: must run as root" >&2; exit 1; }
|
||||||
|
|
||||||
CONTAINERS_DIR="/docker/containers"
|
# Baked at install; unbaked copies keep the "__" sentinel.
|
||||||
DB_CFG="/docker/configs/general/general_docker_install"
|
SYSTEM_DIR="__SYSTEM_DIR__"
|
||||||
|
CONTAINERS_DIR="__CONTAINERS_DIR__"
|
||||||
|
[[ "$SYSTEM_DIR" == *"__"* || -z "$SYSTEM_DIR" ]] && SYSTEM_DIR="/libreportal-system"
|
||||||
|
[[ "$CONTAINERS_DIR" == *"__"* || -z "$CONTAINERS_DIR" ]] && CONTAINERS_DIR="/libreportal-containers"
|
||||||
|
DB_CFG="$SYSTEM_DIR/configs/general/general_docker_install"
|
||||||
|
|
||||||
_install_user() {
|
_install_user() {
|
||||||
local u
|
local u
|
||||||
|
|||||||
@ -4,50 +4,70 @@
|
|||||||
#
|
#
|
||||||
# Why this exists: under Model A the runtime executes AS the manager (libreportal),
|
# Why this exists: under Model A the runtime executes AS the manager (libreportal),
|
||||||
# so establishing the ownership model (manager owns the control plane, the docker
|
# so establishing the ownership model (manager owns the control plane, the docker
|
||||||
# install user owns the containers) needs root. Granting the manager a blanket
|
# install user owns the containers + backups) needs root. Granting the manager a
|
||||||
# `sudo chown`/`sudo chmod` in the scoped sudoers would be root-equivalent (chown
|
# blanket `sudo chown`/`sudo chmod` would be root-equivalent (chown /etc/sudoers,
|
||||||
# /etc/sudoers, etc.). Instead this script — installed root:root 0755 to
|
# etc.). Instead this script — installed root:root 0755 to /usr/local/lib/libreportal/
|
||||||
# /usr/local/sbin by init.sh, so the manager cannot modify it — performs a FIXED
|
# by init.sh, so the manager cannot modify it — performs a FIXED set of reconciles
|
||||||
# set of reconciles on FIXED LibrePortal paths only. Owners are derived from
|
# on FIXED LibrePortal paths only. The roots and the manager name are BAKED at
|
||||||
# config + the baked manager name, never from the caller; the single free argument
|
# install (sed placeholders), never read at runtime from a manager-writable config;
|
||||||
# (an app name) is strictly validated and must resolve to an existing dir under
|
# the single free argument (an app name / relpath) is strictly validated.
|
||||||
# /docker/containers.
|
|
||||||
#
|
#
|
||||||
# Self-contained ON PURPOSE: it must NOT source any manager-owned code, or it
|
# Layout — three independently-relocatable roots, each owned by ONE principal:
|
||||||
# would re-open the very escalation it exists to close. init.sh is the source of
|
# SYSTEM_DIR manager-owned control plane (configs/logs/install/db/ssl/ssh/…)
|
||||||
# truth for the install; it bakes the manager name into the installed copy.
|
# CONTAINERS_DIR container-user-owned live app data (apps live directly under it)
|
||||||
|
# BACKUPS_DIR container-user-owned backup repos (own mount-able)
|
||||||
|
#
|
||||||
|
# Self-contained ON PURPOSE: it must NOT source any manager-owned code (incl.
|
||||||
|
# paths.sh), or it would re-open the very escalation it exists to close. init.sh is
|
||||||
|
# the source of truth for the install; it bakes the values into the installed copy.
|
||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
[[ $EUID -eq 0 ]] || { echo "libreportal-ownership: must run as root" >&2; exit 1; }
|
[[ $EUID -eq 0 ]] || { echo "libreportal-ownership: must run as root" >&2; exit 1; }
|
||||||
|
|
||||||
# Baked by init.sh at install (placeholder replaced); default if run unbaked.
|
# Baked by init.sh at install (placeholders replaced). An unbaked copy (run
|
||||||
|
# directly from the repo before baking) still contains the "__" sentinel, which no
|
||||||
|
# real absolute path does — fall back to the defaults in that case only.
|
||||||
MANAGER="__MANAGER__"
|
MANAGER="__MANAGER__"
|
||||||
[[ "$MANAGER" == "__MANAGER__" || -z "$MANAGER" ]] && MANAGER="libreportal"
|
CONTAINERS_DIR="__CONTAINERS_DIR__"
|
||||||
|
BACKUPS_DIR="__BACKUPS_DIR__"
|
||||||
|
SYSTEM_DIR="__SYSTEM_DIR__"
|
||||||
|
[[ "$MANAGER" == *"__"* || -z "$MANAGER" ]] && MANAGER="libreportal"
|
||||||
|
[[ "$SYSTEM_DIR" == *"__"* || -z "$SYSTEM_DIR" ]] && SYSTEM_DIR="/libreportal-system"
|
||||||
|
[[ "$CONTAINERS_DIR" == *"__"* || -z "$CONTAINERS_DIR" ]] && CONTAINERS_DIR="/libreportal-containers"
|
||||||
|
[[ "$BACKUPS_DIR" == *"__"* || -z "$BACKUPS_DIR" ]] && BACKUPS_DIR="/libreportal-backups"
|
||||||
|
|
||||||
DOCKER_DIR="/docker"
|
# Refuse to operate on dangerous roots even if mis-baked (defence in depth).
|
||||||
CONFIGS_DIR="$DOCKER_DIR/configs"
|
for _d in "$SYSTEM_DIR" "$CONTAINERS_DIR" "$BACKUPS_DIR"; do
|
||||||
LOGS_DIR="$DOCKER_DIR/logs"
|
case "$_d" in
|
||||||
INSTALL_DIR="$DOCKER_DIR/install"
|
/|/etc|/usr|/bin|/sbin|/lib|/lib64|/boot|/proc|/sys|/dev|/run|/home|/root|/var|/tmp)
|
||||||
CONTAINERS_DIR="$DOCKER_DIR/containers"
|
echo "libreportal-ownership: refusing dangerous root '$_d'" >&2; exit 1 ;;
|
||||||
SSL_DIR="$DOCKER_DIR/ssl"
|
/*) ;; # absolute — ok
|
||||||
SSH_DIR="$DOCKER_DIR/ssh"
|
*) echo "libreportal-ownership: root must be absolute: '$_d'" >&2; exit 1 ;;
|
||||||
BACKUP_DIR="$DOCKER_DIR/backups"
|
esac
|
||||||
RESTORE_DIR="$DOCKER_DIR/restore"
|
done
|
||||||
MIGRATE_DIR="$DOCKER_DIR/migrate"
|
|
||||||
DB_PATH="$DOCKER_DIR/database.db"
|
CONFIGS_DIR="$SYSTEM_DIR/configs"
|
||||||
|
LOGS_DIR="$SYSTEM_DIR/logs"
|
||||||
|
INSTALL_DIR="$SYSTEM_DIR/install"
|
||||||
|
SSL_DIR="$SYSTEM_DIR/ssl"
|
||||||
|
SSH_DIR="$SYSTEM_DIR/ssh"
|
||||||
|
RESTORE_DIR="$SYSTEM_DIR/restore"
|
||||||
|
MIGRATE_DIR="$SYSTEM_DIR/migrate"
|
||||||
|
DB_PATH="$SYSTEM_DIR/database.db"
|
||||||
WEBUI_DIR="$CONTAINERS_DIR/libreportal"
|
WEBUI_DIR="$CONTAINERS_DIR/libreportal"
|
||||||
TASK_DIR="$WEBUI_DIR/frontend/data/tasks"
|
TASK_DIR="$WEBUI_DIR/frontend/data/tasks"
|
||||||
DB_CFG="$CONFIGS_DIR/general/general_docker_install"
|
DB_CFG="$CONFIGS_DIR/general/general_docker_install"
|
||||||
|
|
||||||
# Current docker mode, read authoritatively from config.
|
# Current docker mode, read authoritatively from config (read-only — informs the
|
||||||
|
# container OWNER choice, not any path).
|
||||||
_mode() {
|
_mode() {
|
||||||
local m
|
local m
|
||||||
m=$(grep -h '^CFG_DOCKER_INSTALL_TYPE=' "$DB_CFG" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
m=$(grep -h '^CFG_DOCKER_INSTALL_TYPE=' "$DB_CFG" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
||||||
echo "${m:-rootless}"
|
echo "${m:-rootless}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Who owns container data for a mode: rooted -> the manager; rootless -> the
|
# Who owns container/backup data for a mode: rooted -> the manager; rootless -> the
|
||||||
# configured docker install user (must be a real account, else fall back).
|
# configured docker install user (must be a real account, else fall back).
|
||||||
_container_owner() {
|
_container_owner() {
|
||||||
local mode="$1" appusr=""
|
local mode="$1" appusr=""
|
||||||
@ -72,38 +92,61 @@ _app_dir() {
|
|||||||
printf '%s' "$d"
|
printf '%s' "$d"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Control plane -> manager; structural container dirs -> container owner. Owner
|
# Let the rootless container user reach the few system-tree files it must read as
|
||||||
# only, never reset mode bits; only ADD o+x (traversal) and o+r (DB read).
|
# bind-mount sources (the WebUI's configs/webui/*), WITHOUT exposing the rest of
|
||||||
|
# the control plane: traverse SYSTEM_DIR + configs, read configs/webui only.
|
||||||
|
_webui_bind_access() {
|
||||||
|
chmod o+x "$SYSTEM_DIR" 2>/dev/null
|
||||||
|
[[ -d "$CONFIGS_DIR" ]] && chmod o+x "$CONFIGS_DIR" 2>/dev/null
|
||||||
|
if [[ -d "$CONFIGS_DIR/webui" ]]; then
|
||||||
|
chmod o+rx "$CONFIGS_DIR/webui" 2>/dev/null
|
||||||
|
find "$CONFIGS_DIR/webui" -maxdepth 1 -type f -exec chmod o+r {} \; 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Control plane -> manager; container + backup roots -> container owner.
|
||||||
reconcile() {
|
reconcile() {
|
||||||
local mode="${1:-$(_mode)}"
|
local mode="${1:-$(_mode)}"
|
||||||
[[ -d "$DOCKER_DIR" ]] || return 0
|
|
||||||
chown "$MANAGER:$MANAGER" "$DOCKER_DIR"
|
|
||||||
chmod o+x "$DOCKER_DIR"
|
|
||||||
local p
|
|
||||||
for p in "$CONFIGS_DIR" "$LOGS_DIR" "$INSTALL_DIR" "$DB_PATH"; do
|
|
||||||
[[ -e "$p" ]] && chown -R "$MANAGER:$MANAGER" "$p"
|
|
||||||
done
|
|
||||||
[[ -f "$DB_PATH" ]] && chmod o+r "$DB_PATH"
|
|
||||||
local cowner; cowner="$(_container_owner "$mode")"
|
local cowner; cowner="$(_container_owner "$mode")"
|
||||||
if [[ -d "$CONTAINERS_DIR" ]]; then
|
|
||||||
chown "$cowner:$cowner" "$CONTAINERS_DIR"
|
if [[ -d "$SYSTEM_DIR" ]]; then
|
||||||
chmod o+x "$CONTAINERS_DIR"
|
chown "$MANAGER:$MANAGER" "$SYSTEM_DIR"
|
||||||
|
local p
|
||||||
|
for p in "$CONFIGS_DIR" "$LOGS_DIR" "$INSTALL_DIR" "$SSL_DIR" "$SSH_DIR" \
|
||||||
|
"$RESTORE_DIR" "$MIGRATE_DIR" "$DB_PATH"; do
|
||||||
|
[[ -e "$p" ]] && chown -R "$MANAGER:$MANAGER" "$p"
|
||||||
|
done
|
||||||
|
[[ -f "$DB_PATH" ]] && chmod o+r "$DB_PATH"
|
||||||
|
_webui_bind_access
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Data + backups: wholly the container owner's (rootless requires it; this is
|
||||||
|
# also what lets restic — which runs AS that user — write the backup repos).
|
||||||
|
local d
|
||||||
|
for d in "$CONTAINERS_DIR" "$BACKUPS_DIR"; do
|
||||||
|
if [[ -d "$d" ]]; then
|
||||||
|
chown "$cowner:$cowner" "$d"
|
||||||
|
chmod o+x "$d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
[[ -d "$WEBUI_DIR" ]] && chown -R "$cowner:$cowner" "$WEBUI_DIR"
|
[[ -d "$WEBUI_DIR" ]] && chown -R "$cowner:$cowner" "$WEBUI_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Traversal (+x) bits only, on the structural LibrePortal dirs.
|
# Traversal (+x) bits only, on the structural LibrePortal dirs.
|
||||||
traversal() {
|
traversal() {
|
||||||
[[ -d "$DOCKER_DIR" ]] && chmod +x "$DOCKER_DIR"
|
[[ -d "$SYSTEM_DIR" ]] && chmod o+x "$SYSTEM_DIR"
|
||||||
local d
|
local d
|
||||||
for d in "$INSTALL_DIR" "$SSL_DIR" "$SSH_DIR" "$BACKUP_DIR" "$RESTORE_DIR" "$MIGRATE_DIR"; do
|
for d in "$INSTALL_DIR" "$SSL_DIR" "$SSH_DIR" "$RESTORE_DIR" "$MIGRATE_DIR"; do
|
||||||
[[ -d "$d" ]] && find "$d" -maxdepth 2 -type d -exec chmod +x {} \;
|
[[ -d "$d" ]] && find "$d" -maxdepth 2 -type d -exec chmod +x {} \;
|
||||||
done
|
done
|
||||||
|
_webui_bind_access
|
||||||
local mode cowner; mode="$(_mode)"; cowner="$(_container_owner "$mode")"
|
local mode cowner; mode="$(_mode)"; cowner="$(_container_owner "$mode")"
|
||||||
if [[ -d "$CONTAINERS_DIR" ]]; then
|
for d in "$CONTAINERS_DIR" "$BACKUPS_DIR"; do
|
||||||
chown "$cowner:$cowner" "$CONTAINERS_DIR"
|
if [[ -d "$d" ]]; then
|
||||||
chmod o+x "$CONTAINERS_DIR"
|
chown "$cowner:$cowner" "$d"
|
||||||
fi
|
chmod o+x "$d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Per-app structural perms + ownership of the LibrePortal-managed files only.
|
# Per-app structural perms + ownership of the LibrePortal-managed files only.
|
||||||
@ -131,7 +174,7 @@ webui() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Ensure the apps DB is manager-owned + world-readable (reclaims a stray
|
# Ensure the apps DB is manager-owned + world-readable (reclaims a stray
|
||||||
# root/other-owned DB; the WebUI container reads it).
|
# root/other-owned DB; the WebUI reads it).
|
||||||
db_own() {
|
db_own() {
|
||||||
[[ -f "$DB_PATH" ]] || return 0
|
[[ -f "$DB_PATH" ]] || return 0
|
||||||
chown "$MANAGER:$MANAGER" "$DB_PATH"
|
chown "$MANAGER:$MANAGER" "$DB_PATH"
|
||||||
@ -147,6 +190,15 @@ containers_top() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The backups root -> container owner + traversable (restic runs AS that user).
|
||||||
|
backups_top() {
|
||||||
|
local mode cowner; mode="$(_mode)"; cowner="$(_container_owner "$mode")"
|
||||||
|
if [[ -d "$BACKUPS_DIR" ]]; then
|
||||||
|
chown "$cowner:$cowner" "$BACKUPS_DIR"
|
||||||
|
chmod o+x "$BACKUPS_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# The task IPC dir -> container owner (reclaims stale manager/root-owned files).
|
# The task IPC dir -> container owner (reclaims stale manager/root-owned files).
|
||||||
taskdir() {
|
taskdir() {
|
||||||
local mode cowner; mode="$(_mode)"; cowner="$(_container_owner "$mode")"
|
local mode cowner; mode="$(_mode)"; cowner="$(_container_owner "$mode")"
|
||||||
@ -159,8 +211,7 @@ app_data_nobody() {
|
|||||||
[[ -d "$d/data" ]] && chown -R 65534:65534 "$d/data"
|
[[ -d "$d/data" ]] && chown -R 65534:65534 "$d/data"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Chown one LibrePortal-managed file under an app dir to the container owner
|
# Chown one LibrePortal-managed file under an app dir to the container owner.
|
||||||
# (e.g. traefik acme.json / traefik.yml the container creates as another uid).
|
|
||||||
# relpath is validated: no traversal, no absolute path, safe charset only.
|
# relpath is validated: no traversal, no absolute path, safe charset only.
|
||||||
app_file() {
|
app_file() {
|
||||||
local d rel mode cowner
|
local d rel mode cowner
|
||||||
@ -177,11 +228,12 @@ case "$action" in
|
|||||||
reconcile) reconcile "${1:-}";;
|
reconcile) reconcile "${1:-}";;
|
||||||
traversal) traversal;;
|
traversal) traversal;;
|
||||||
containers-top) containers_top;;
|
containers-top) containers_top;;
|
||||||
|
backups-top) backups_top;;
|
||||||
db-own) db_own;;
|
db-own) db_own;;
|
||||||
app-perms) app_perms;;
|
app-perms) app_perms;;
|
||||||
webui) webui;;
|
webui) webui;;
|
||||||
taskdir) taskdir;;
|
taskdir) taskdir;;
|
||||||
app-data-nobody) app_data_nobody "${1:-}";;
|
app-data-nobody) app_data_nobody "${1:-}";;
|
||||||
app-file) app_file "${1:-}" "${2:-}";;
|
app-file) app_file "${1:-}" "${2:-}";;
|
||||||
*) echo "usage: libreportal-ownership {reconcile [mode]|traversal|containers-top|db-own|app-perms|webui|taskdir|app-data-nobody <app>|app-file <app> <relpath>}" >&2; exit 2;;
|
*) echo "usage: libreportal-ownership {reconcile [mode]|traversal|containers-top|backups-top|db-own|app-perms|webui|taskdir|app-data-nobody <app>|app-file <app> <relpath>}" >&2; exit 2;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -11,7 +11,10 @@ set -u
|
|||||||
|
|
||||||
[[ $EUID -eq 0 ]] || { echo "libreportal-socket: must run as root" >&2; exit 1; }
|
[[ $EUID -eq 0 ]] || { echo "libreportal-socket: must run as root" >&2; exit 1; }
|
||||||
|
|
||||||
DB_CFG="/docker/configs/general/general_docker_install"
|
# SYSTEM_DIR baked at install; unbaked copies keep the "__" sentinel.
|
||||||
|
SYSTEM_DIR="__SYSTEM_DIR__"
|
||||||
|
[[ "$SYSTEM_DIR" == *"__"* || -z "$SYSTEM_DIR" ]] && SYSTEM_DIR="/libreportal-system"
|
||||||
|
DB_CFG="$SYSTEM_DIR/configs/general/general_docker_install"
|
||||||
ROOTED_SOCK="/var/run/docker.sock"
|
ROOTED_SOCK="/var/run/docker.sock"
|
||||||
|
|
||||||
_rootless_sock() {
|
_rootless_sock() {
|
||||||
|
|||||||
@ -14,13 +14,21 @@ set -u
|
|||||||
|
|
||||||
[[ $EUID -eq 0 ]] || { echo "libreportal-svc: must run as root" >&2; exit 1; }
|
[[ $EUID -eq 0 ]] || { echo "libreportal-svc: must run as root" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Baked at install (placeholders replaced). Unbaked copies still contain the "__"
|
||||||
|
# sentinel, which no real absolute path does — fall back to defaults then.
|
||||||
MANAGER="__MANAGER__"
|
MANAGER="__MANAGER__"
|
||||||
[[ "$MANAGER" == "__MANAGER__" || -z "$MANAGER" ]] && MANAGER="libreportal"
|
SYSTEM_DIR="__SYSTEM_DIR__"
|
||||||
|
CONTAINERS_DIR="__CONTAINERS_DIR__"
|
||||||
|
BACKUPS_DIR="__BACKUPS_DIR__"
|
||||||
|
[[ "$MANAGER" == *"__"* || -z "$MANAGER" ]] && MANAGER="libreportal"
|
||||||
|
[[ "$SYSTEM_DIR" == *"__"* || -z "$SYSTEM_DIR" ]] && SYSTEM_DIR="/libreportal-system"
|
||||||
|
[[ "$CONTAINERS_DIR" == *"__"* || -z "$CONTAINERS_DIR" ]] && CONTAINERS_DIR="/libreportal-containers"
|
||||||
|
[[ "$BACKUPS_DIR" == *"__"* || -z "$BACKUPS_DIR" ]] && BACKUPS_DIR="/libreportal-backups"
|
||||||
|
|
||||||
SERVICE_FILE="/etc/systemd/system/libreportal.service"
|
SERVICE_FILE="/etc/systemd/system/libreportal.service"
|
||||||
INSTALL_SCRIPTS_DIR="/docker/install/scripts"
|
INSTALL_SCRIPTS_DIR="$SYSTEM_DIR/install/scripts"
|
||||||
TASK_PROCESSOR="$INSTALL_SCRIPTS_DIR/crontab/task/crontab_task_processor.sh"
|
TASK_PROCESSOR="$INSTALL_SCRIPTS_DIR/crontab/task/crontab_task_processor.sh"
|
||||||
DB_CFG="/docker/configs/general/general_docker_install"
|
DB_CFG="$SYSTEM_DIR/configs/general/general_docker_install"
|
||||||
|
|
||||||
_mode() {
|
_mode() {
|
||||||
local m
|
local m
|
||||||
@ -50,6 +58,11 @@ Type=simple
|
|||||||
User=$MANAGER
|
User=$MANAGER
|
||||||
Group=$MANAGER
|
Group=$MANAGER
|
||||||
WorkingDirectory=$INSTALL_SCRIPTS_DIR
|
WorkingDirectory=$INSTALL_SCRIPTS_DIR
|
||||||
|
# Relocatable path roots — baked here by root so the processor resolves them
|
||||||
|
# authoritatively (not via the legacy compat default in paths.sh).
|
||||||
|
Environment=LP_SYSTEM_DIR=$SYSTEM_DIR
|
||||||
|
Environment=LP_CONTAINERS_DIR=$CONTAINERS_DIR
|
||||||
|
Environment=LP_BACKUPS_DIR=$BACKUPS_DIR
|
||||||
ExecStart=$TASK_PROCESSOR start_script
|
ExecStart=$TASK_PROCESSOR start_script
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user