diff --git a/containers/libreportal/docker-compose.yml b/containers/libreportal/docker-compose.yml index a16cbd2..537e634 100644 --- a/containers/libreportal/docker-compose.yml +++ b/containers/libreportal/docker-compose.yml @@ -21,8 +21,10 @@ services: - ./backend/utils:/app/backend/utils - ./backend/server.js:/app/backend/server.js - ./libreportal.config:/app/libreportal.config:ro - - ../../configs/webui/webui_logins:/app/webui_logins:ro - - ../../configs/webui/webui_logs:/app/webui_logs:ro + # Absolute (filled at generation) — the containers root is now separate from + # 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 >>> #- /var/log/crowdsec.log:/host/var/log/crowdsec.log:ro #- /var/log/crowdsec-firewall-bouncer.log:/host/var/log/crowdsec-firewall-bouncer.log:ro diff --git a/init.sh b/init.sh index 4e3bd0d..9bea5a9 100755 --- a/init.sh +++ b/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 # 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. - 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/') if [[ -n "$param3" || -n "$param4" || -n "$param5" ]]; then # Git parameters provided, set to git mode @@ -845,7 +845,15 @@ initRootHelpers() continue fi 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 sudo install -m 0755 -o root -g root "$helper_tmp" "$helper_dst" isSuccessful "Installed root-owned helper ($helper)." @@ -900,20 +908,24 @@ initContainerLayer() isSuccessful "Created container user '$duser'." fi - # /docker is manager-owned and initFolders makes it 750; give it the rootless - # traversal bit (o+x → 751, its documented rootless mode) so the container - # user can traverse INTO /docker to reach its containers/ dir. Without this - # the boot scan can't enter /docker at all, no matter who owns containers/. + # The system root is manager-owned and initFolders makes it 750; give it the + # rootless traversal bit (o+x → 751) so the container user can reach the few + # bind-mount sources it must read there (configs/webui/*). The container + backup + # 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" - # Hand containers/ to the container user (it owns per-app data in rootless) so - # the manager-run startup config scans can read it. 751: owner full; the - # manager (other) can traverse in to known paths (it lists/writes via runFileOp). - if [[ -d "$containers_dir" ]]; then - sudo chown "$duser:$duser" "$containers_dir" - sudo chmod 751 "$containers_dir" - isSuccessful "containers/ handed to '$duser' (+ /docker traversable)." - fi + # Hand the container + backup roots to the container user — it owns per-app data + # in rootless, and restic (which runs AS that user) writes the backup repos. + # 751: owner full; the manager (other) can traverse in to known paths. + local d + for d in "$containers_dir" "$backup_dir"; do + if [[ -d "$d" ]]; then + sudo chown "$duser:$duser" "$d" + sudo chmod 751 "$d" + fi + done + isSuccessful "containers/ + backups/ handed to '$duser' (system root traversable)." } setupConfigsFromRepo() @@ -921,7 +933,7 @@ setupConfigsFromRepo() isNotice "Setting up configuration files from repository..." local src="$script_dir/configs" - local dst="/docker/configs" + local dst="${configs_dir%/}" if [[ ! -d "$src" ]]; then isError "Source configs directory missing: $src" @@ -1334,7 +1346,7 @@ initUpdateConfigs() initUpdateConfigOption "CFG_INSTALL_MODE" "$param7" && isSuccessful "Updated Installation Mode" 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 isError "Expected $cfg_file is missing — install cannot proceed." exit 1 @@ -1402,7 +1414,7 @@ runFullUninstall() { local mgr="${sudo_user_name:-libreportal}" 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}" # --skip-docker-images: keep the rootless docker layer (the daemon, the @@ -1415,7 +1427,9 @@ runFullUninstall() isHeader "LibrePortal — FULL Uninstall" isError "This PERMANENTLY removes EVERYTHING — there is no undo:" 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 " - /usr/local/lib/libreportal/ + the /usr/local/bin/libreportal command" echo " - /etc/sudoers.d/$mgr, the systemd service, the sysctl drop-ins" @@ -1484,9 +1498,11 @@ runFullUninstall() rm -f /root/init.sh isSuccessful "Removed the system-integration footprint" - # 4. Remove all app data. - rm -rf /docker - isSuccessful "Removed /docker" + # 4. Remove all app data — the three roots (on a legacy single-tree install the + # container/backup roots are subdirs of the system root, so this is safe and + # 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. # Terminate each user's session/linger and kill its processes first, or diff --git a/scripts/config/tags/processors/tags_processor_standard_replacements.sh b/scripts/config/tags/processors/tags_processor_standard_replacements.sh index dfa3be4..e9f627d 100755 --- a/scripts/config/tags/processors/tags_processor_standard_replacements.sh +++ b/scripts/config/tags/processors/tags_processor_standard_replacements.sh @@ -24,6 +24,9 @@ tagsProcessorStandardReplacements() # Host live-app-data root, passed into the WebUI container (only the # libreportal compose carries this tag; "only update tags that exist"). 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" } diff --git a/scripts/system/libreportal-appcfg b/scripts/system/libreportal-appcfg index 95592ae..2baffb9 100644 --- a/scripts/system/libreportal-appcfg +++ b/scripts/system/libreportal-appcfg @@ -10,8 +10,12 @@ set -u [[ $EUID -eq 0 ]] || { echo "libreportal-appcfg: must run as root" >&2; exit 1; } -CONTAINERS_DIR="/docker/containers" -DB_CFG="/docker/configs/general/general_docker_install" +# Baked at install; unbaked copies keep the "__" sentinel. +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() { local u diff --git a/scripts/system/libreportal-ownership b/scripts/system/libreportal-ownership index e26d6fd..861c5c8 100644 --- a/scripts/system/libreportal-ownership +++ b/scripts/system/libreportal-ownership @@ -4,50 +4,70 @@ # # 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 -# install user owns the containers) needs root. Granting the manager a blanket -# `sudo chown`/`sudo chmod` in the scoped sudoers would be root-equivalent (chown -# /etc/sudoers, etc.). Instead this script — installed root:root 0755 to -# /usr/local/sbin by init.sh, so the manager cannot modify it — performs a FIXED -# set of reconciles on FIXED LibrePortal paths only. Owners are derived from -# config + the baked manager name, never from the caller; the single free argument -# (an app name) is strictly validated and must resolve to an existing dir under -# /docker/containers. +# install user owns the containers + backups) needs root. Granting the manager a +# blanket `sudo chown`/`sudo chmod` would be root-equivalent (chown /etc/sudoers, +# etc.). Instead this script — installed root:root 0755 to /usr/local/lib/libreportal/ +# by init.sh, so the manager cannot modify it — performs a FIXED set of reconciles +# on FIXED LibrePortal paths only. The roots and the manager name are BAKED at +# install (sed placeholders), never read at runtime from a manager-writable config; +# the single free argument (an app name / relpath) is strictly validated. # -# Self-contained ON PURPOSE: it must NOT source any manager-owned code, 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 manager name into the installed copy. +# Layout — three independently-relocatable roots, each owned by ONE principal: +# SYSTEM_DIR manager-owned control plane (configs/logs/install/db/ssl/ssh/…) +# 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 [[ $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__" || -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" -CONFIGS_DIR="$DOCKER_DIR/configs" -LOGS_DIR="$DOCKER_DIR/logs" -INSTALL_DIR="$DOCKER_DIR/install" -CONTAINERS_DIR="$DOCKER_DIR/containers" -SSL_DIR="$DOCKER_DIR/ssl" -SSH_DIR="$DOCKER_DIR/ssh" -BACKUP_DIR="$DOCKER_DIR/backups" -RESTORE_DIR="$DOCKER_DIR/restore" -MIGRATE_DIR="$DOCKER_DIR/migrate" -DB_PATH="$DOCKER_DIR/database.db" +# Refuse to operate on dangerous roots even if mis-baked (defence in depth). +for _d in "$SYSTEM_DIR" "$CONTAINERS_DIR" "$BACKUPS_DIR"; do + case "$_d" in + /|/etc|/usr|/bin|/sbin|/lib|/lib64|/boot|/proc|/sys|/dev|/run|/home|/root|/var|/tmp) + echo "libreportal-ownership: refusing dangerous root '$_d'" >&2; exit 1 ;; + /*) ;; # absolute — ok + *) echo "libreportal-ownership: root must be absolute: '$_d'" >&2; exit 1 ;; + esac +done + +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" TASK_DIR="$WEBUI_DIR/frontend/data/tasks" 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() { local m m=$(grep -h '^CFG_DOCKER_INSTALL_TYPE=' "$DB_CFG" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}') 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). _container_owner() { local mode="$1" appusr="" @@ -72,38 +92,61 @@ _app_dir() { printf '%s' "$d" } -# Control plane -> manager; structural container dirs -> container owner. Owner -# only, never reset mode bits; only ADD o+x (traversal) and o+r (DB read). +# Let the rootless container user reach the few system-tree files it must read as +# 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() { 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")" - if [[ -d "$CONTAINERS_DIR" ]]; then - chown "$cowner:$cowner" "$CONTAINERS_DIR" - chmod o+x "$CONTAINERS_DIR" + + if [[ -d "$SYSTEM_DIR" ]]; then + 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 + + # 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" } # Traversal (+x) bits only, on the structural LibrePortal dirs. traversal() { - [[ -d "$DOCKER_DIR" ]] && chmod +x "$DOCKER_DIR" + [[ -d "$SYSTEM_DIR" ]] && chmod o+x "$SYSTEM_DIR" 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 {} \; done + _webui_bind_access local mode cowner; mode="$(_mode)"; cowner="$(_container_owner "$mode")" - if [[ -d "$CONTAINERS_DIR" ]]; then - chown "$cowner:$cowner" "$CONTAINERS_DIR" - chmod o+x "$CONTAINERS_DIR" - fi + for d in "$CONTAINERS_DIR" "$BACKUPS_DIR"; do + if [[ -d "$d" ]]; then + chown "$cowner:$cowner" "$d" + chmod o+x "$d" + fi + done } # 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 -# root/other-owned DB; the WebUI container reads it). +# root/other-owned DB; the WebUI reads it). db_own() { [[ -f "$DB_PATH" ]] || return 0 chown "$MANAGER:$MANAGER" "$DB_PATH" @@ -147,6 +190,15 @@ containers_top() { 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). taskdir() { 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" } -# 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). +# Chown one LibrePortal-managed file under an app dir to the container owner. # relpath is validated: no traversal, no absolute path, safe charset only. app_file() { local d rel mode cowner @@ -177,11 +228,12 @@ case "$action" in reconcile) reconcile "${1:-}";; traversal) traversal;; containers-top) containers_top;; + backups-top) backups_top;; db-own) db_own;; app-perms) app_perms;; webui) webui;; taskdir) taskdir;; app-data-nobody) app_data_nobody "${1:-}";; 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-file }" >&2; exit 2;; + *) echo "usage: libreportal-ownership {reconcile [mode]|traversal|containers-top|backups-top|db-own|app-perms|webui|taskdir|app-data-nobody |app-file }" >&2; exit 2;; esac diff --git a/scripts/system/libreportal-socket b/scripts/system/libreportal-socket index 913e7bb..144a06d 100644 --- a/scripts/system/libreportal-socket +++ b/scripts/system/libreportal-socket @@ -11,7 +11,10 @@ set -u [[ $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" _rootless_sock() { diff --git a/scripts/system/libreportal-svc b/scripts/system/libreportal-svc index 132a649..9f76427 100644 --- a/scripts/system/libreportal-svc +++ b/scripts/system/libreportal-svc @@ -14,13 +14,21 @@ set -u [[ $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__" || -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" -INSTALL_SCRIPTS_DIR="/docker/install/scripts" +INSTALL_SCRIPTS_DIR="$SYSTEM_DIR/install/scripts" 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() { local m @@ -50,6 +58,11 @@ Type=simple User=$MANAGER Group=$MANAGER 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 Restart=always RestartSec=5