#!/bin/bash installDockerRootless() { if [[ $CFG_DOCKER_INSTALL_TYPE == "rootless" ]]; then isHeader "Install Docker Rootless" #dockerComposeDownAllApps root; #dockerServiceStop root; ((menu_number++)) echo "" echo "---- $menu_number. Installing System Requirements." echo "" local docker_install_user_id=$(id -u "$CFG_DOCKER_INSTALL_USER") local docker_install_bashrc="/home/$CFG_DOCKER_INSTALL_USER/.bashrc" isNotice "Fetching and installing the necessary packages...this may take a moment..." local result; result=$(runSystem apt-get install -y apt-transport-https ca-certificates curl gnupg software-properties-common uidmap dbus-user-session fuse-overlayfs passt) checkSuccess "Installing necessary packages" local result; result=$(runSystem systemctl disable --now docker.service docker.socket) checkSuccess "Disabling Docker service & Socket" ((menu_number++)) echo "" echo "---- $menu_number. Installing slirp4netns." echo "" # slirp4netns update and install if ! command -v slirp4netns &> /dev/null; then isNotice "slirp4netns is not installed. Installing..." local result; result=$(runSystem apt-get install -y slirp4netns) checkSuccess "Installing slirp4netns" else isNotice "slirp4netns is already installed" # `slirp4netns --version` prints several lines (version, commit, libslirp, # SLIRP_CONFIG_VERSION_MAX); read only the first line and take the version field. installed_version=$(slirp4netns --version | head -n1 | awk '{print $3}') latest_version=$(curl -s https://api.github.com/repos/rootless-containers/slirp4netns/releases/latest | grep tag_name | cut -d '"' -f 4) latest_version=${latest_version#v} # GitHub tags are vX.Y.Z; strip the v to compare if [[ -n "$latest_version" && "$installed_version" != "$latest_version" ]]; then isNotice "slirp4netns version $installed_version is outdated." isNotice "Installing version $latest_version..." local result; result=$(runSystem apt-get update) checkSuccess "Updating apt packages" local result; result=$(runSystem apt-get install -y slirp4netns) checkSuccess "Installing slirp4netns" else isSuccessful "slirp4netns version $installed_version is up to date" fi fi if [[ $(lsb_release -rs) == "10" ]]; then ((menu_number++)) echo "" echo "---- $menu_number. Updating the sysctl file for Updating Debian 10." echo "" if sudo grep -qs "kernel.unprivileged_userns_clone=1" $sysctl; then isNotice "kernel.unprivileged_userns_clone=1 already exists in $sysctl" else local result; result=$(echo "kernel.unprivileged_userns_clone=1" | sudo tee -a $sysctl > /dev/null) checkSuccess "Adding kernel.unprivileged_userns_clone=1 to $sysctl..." local result; result=$(runSystem sysctl --system) checkSuccess "Running runAsManager sysctl --system..." fi fi ((menu_number++)) echo "" echo "---- $menu_number. Update the .bashrc file." echo "" if ! grep -qF "# DOCKER ROOTLESS BASHRC START" "$docker_install_bashrc"; then local result; result=$(echo '# DOCKER ROOTLESS BASHRC START' | sudo tee -a "$docker_install_bashrc" > /dev/null) checkSuccess "Adding rootless header to .bashrc" local result; result=$(echo 'export XDG_RUNTIME_DIR=/run/user/${UID}' | sudo tee -a "$docker_install_bashrc" > /dev/null) checkSuccess "Adding export path to .bashrc" local result; result=$(echo 'export PATH=/usr/bin:$PATH' | sudo tee -a "$docker_install_bashrc" > /dev/null) checkSuccess "Adding export path to .bashrc" local result; result=$(echo 'export DOCKER_HOST=unix:///run/user/${UID}/docker.sock' | sudo tee -a "$docker_install_bashrc" > /dev/null) checkSuccess "Adding export DOCKER_HOST path to .bashrc" local result; result=$(echo 'export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${UID}/bus"' | sudo tee -a "$docker_install_bashrc" > /dev/null) checkSuccess "Adding export DBUS_SESSION_BUS_ADDRESS path to .bashrc" local result; result=$(echo '# DOCKER ROOTLESS BASHRC END' | sudo tee -a "$docker_install_bashrc" > /dev/null) checkSuccess "Adding rootless header to .bashrc" isSuccessful "Added $CFG_DOCKER_INSTALL_USER to bashrc file" else isNotice "Rootless .bashrc already configured for $CFG_DOCKER_INSTALL_USER — skipping" fi ((menu_number++)) echo "" echo "---- $menu_number. Setting up Rootless Docker." echo "" local result; result=$(runSystem loginctl enable-linger $CFG_DOCKER_INSTALL_USER) checkSuccess "Adding automatic start (linger)" # Install rootless Docker and enable the user service, but do NOT # start it here. The rootless network override (override.conf, # written further down) and the daemon-reload that picks it up # haven't happened yet, so a start at this point comes up with no # valid net/port-driver config and fails ("Job for docker.service # failed") — a guaranteed, harmless first-start error that only # noises the error report. The first real start is the # `systemctl --user restart docker` below, once the override is in # place. rootless_install=$(cat < implicit (fastest; propagates the real client IP) # slirp4netns -> builtin (legacy fallback) local rootless_net="${CFG_ROOTLESS_NET:-pasta}" local rootless_port_driver case "$rootless_net" in pasta) rootless_port_driver="implicit" ;; slirp4netns) rootless_port_driver="builtin" ;; *) isNotice "Unknown CFG_ROOTLESS_NET='$rootless_net'; falling back to pasta." rootless_net="pasta" rootless_port_driver="implicit" ;; esac echo "" echo "---- $menu_number. Configuring rootless networking ($rootless_net + $rootless_port_driver port driver)." echo "" systemd_user_dir="/home/$CFG_DOCKER_INSTALL_USER/.config/systemd/user" local result; result=$(dockerCommandRunInstallUser "mkdir -p $systemd_user_dir") checkSuccess "Create the systemd user directory if it doesn't exist" local result; result=$(dockerCommandRunInstallUser "mkdir -p $systemd_user_dir/docker.service.d") checkSuccess "Create the docker.service.d directory if it doesn't exist" override_conf_file="$systemd_user_dir/docker.service.d/override.conf" local result; result=$(sudo touch $override_conf_file) checkSuccess "Create the override.conf in docker.service.d" sudo bash -c "cat < '$override_conf_file' [Service] Environment='DOCKERD_ROOTLESS_ROOTLESSKIT_NET=$rootless_net' Environment='DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=$rootless_port_driver' Environment='DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=$CFG_NETWORK_MTU' EOL" local result; result=$(sudo chown $CFG_DOCKER_INSTALL_USER:$CFG_DOCKER_INSTALL_USER $override_conf_file) checkSuccess "Updating ownership for override.conf" # Pasta needs explicit AppArmor permissions that the Debian-shipped # passt profile doesn't include by default (ptrace_read on the # rootlesskit child + a couple of /proc + /run paths). Skip silently # when slirp4netns is selected — that path doesn't go through passt. if [[ "$rootless_net" == "pasta" ]]; then installRootlessApparmorForPasta fi # NOTE: we deliberately do NOT set "userland-proxy": false here. Disabling # it makes rootless dockerd require br_netfilter # (/proc/sys/net/bridge/bridge-nf-call-iptables), which isn't present in # the rootless netns on Debian — the daemon then fails to create the # default bridge and won't start. The userland proxy's lack of source-IP # propagation doesn't matter here: apps sit behind Traefik, which carries # the real client IP via X-Forwarded-For at L7. local result; result=$(dockerCommandRunInstallUser "systemctl --user daemon-reload") checkSuccess "Reload the systemd user manager configuration" isNotice "Restarting docker service...this may take a moment..." local result; result=$(dockerCommandRunInstallUser "systemctl --user restart docker") checkSuccess "Reload the systemd user docker service" local result; result=$(sudo cp $sysctl $sysctl.bak) checkSuccess "Backing up sysctl file" ((menu_number++)) echo "" echo "---- $menu_number. Setting up sysctl file to work with LetsEncrypt." echo "" # Update sysctl file if ! grep -qsF "# DOCKER ROOTLESS SYSCTL START" "$sysctl"; then local result; result=$(echo '# DOCKER ROOTLESS SYSCTL START' | sudo tee -a "$sysctl" > /dev/null) checkSuccess "Adding rootless header to sysctl" local result; result=$(echo 'net.ipv4.ip_unprivileged_port_start=0' | sudo tee -a "$sysctl" > /dev/null) checkSuccess "Adding ip_unprivileged_port_start to sysctl" local result; result=$(echo 'kernel.unprivileged_userns_clone=1' | sudo tee -a "$sysctl" > /dev/null) checkSuccess "Adding unprivileged_userns_clone to sysctl" local result; result=$(echo '# DOCKER ROOTLESS SYSCTL END' | sudo tee -a "$sysctl" > /dev/null) checkSuccess "Adding rootless end to sysctl" isSuccessful "Updated the sysctl with Docker Rootless configuration" fi # Enabling unprivileged user namespaces (needed for rootless) widens the # kernel attack surface reachable by unprivileged users. Offset that by # closing the surfaces that local-privilege-escalation chains lean on: # kptr_restrict hides kernel pointers (info-leak primitives), ptrace_scope # blocks cross-process ptrace (credential theft post-compromise), and # bpf_jit_harden hardens the JIT against spraying. Written under # /etc/sysctl.d/ so `sysctl --system` actually loads it — LibrePortal's # own $sysctl path (/etc/sysctl/…) is non-standard and is NOT read by # `sysctl --system`. local hardening_conf="/etc/sysctl.d/99-libreportal-hardening.conf" sudo bash -c "cat > '$hardening_conf'" <<'EOL' # LibrePortal kernel LPE-surface hardening (paired with rootless unprivileged userns) kernel.kptr_restrict=2 kernel.yama.ptrace_scope=1 net.core.bpf_jit_harden=2 EOL checkSuccess "Writing kernel LPE-surface hardening to $hardening_conf" local result; result=$(runSystem sysctl --system) checkSuccess "Applying changes to sysctl" menu_number=0 fi }