diff --git a/scripts/docker/install/rootless/rootless_apparmor.sh b/scripts/docker/install/rootless/rootless_apparmor.sh new file mode 100644 index 0000000..8a326a7 --- /dev/null +++ b/scripts/docker/install/rootless/rootless_apparmor.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# AppArmor profile shim for the rootless Docker + pasta combo. +# +# Debian's stock `passt` profile (shipped by the `passt` package, used for the +# pasta network driver) is enforce-mode and denies the accesses pasta needs to +# wire up rootlesskit's net namespace: +# +# - ptrace(read) into the rootlesskit child to enter its user namespace +# - read /run/user//dockerd-rootless/netns +# - read /proc//net/{tcp,tcp6,udp,udp6} for implicit port-forwarding +# +# Without these the daemon fails to start with +# error: failed to setup network: pasta failed with exit code 1: +# Couldn't open user namespace /proc//ns/user: Permission denied +# +# We extend the profile via the standard local-override pattern: +# /etc/apparmor.d/local/usr.bin.passt — our rules +# /etc/apparmor.d/usr.bin.passt — include if exists +# +# Idempotent — safe to call on every rootless install. The local file survives +# `apt upgrade passt` (it's outside the package's managed paths). The one-line +# include statement could theoretically get reverted by an apt upgrade; we +# re-add it on every run as a belt-and-braces guard. + +installRootlessApparmorForPasta() +{ + # Bail silently when apparmor isn't on this system at all — Fedora/RHEL, + # ungrouped Debian minimals, etc. Nothing to patch. + if [[ ! -d /etc/apparmor.d ]]; then + return 0 + fi + if [[ ! -f /etc/apparmor.d/usr.bin.passt ]]; then + isNotice "AppArmor present but no /etc/apparmor.d/usr.bin.passt — passt may be too old or not installed under apparmor." + return 0 + fi + + isHeader "AppArmor: enabling pasta access for rootless Docker" + + # 1. Add `include if exists ` to the main profile if + # it isn't already there. The line lives just before the closing `}` of + # the profile block. + if ! sudo grep -q "include if exists " /etc/apparmor.d/usr.bin.passt; then + # Insert right before the final `}` of the file. We don't try to be + # surgical about which `}` — passt's profile has exactly one top-level + # close, and any nested `{` `}` (e.g. inside abstractions) are + # behind `include` lines, not literal in this file. + sudo awk ' + /^}/ && !done { print " include if exists "; done=1 } + { print } + ' /etc/apparmor.d/usr.bin.passt | sudo tee /etc/apparmor.d/usr.bin.passt.new > /dev/null + sudo mv /etc/apparmor.d/usr.bin.passt.new /etc/apparmor.d/usr.bin.passt + checkSuccess "Added include-local line to /etc/apparmor.d/usr.bin.passt" + else + isSuccessful "Main passt profile already sources /etc/apparmor.d/local/usr.bin.passt" + fi + + # 2. Write (or overwrite — same content every time) the local rules file. + sudo mkdir -p /etc/apparmor.d/local + sudo tee /etc/apparmor.d/local/usr.bin.passt > /dev/null <<'EOF' +# Managed by LibrePortal (installRootlessApparmorForPasta). Edits here are +# fine but will be overwritten on the next rootless reinstall — put your +# customisations in a sibling file (e.g. local/usr.bin.passt.local) and +# include them yourself if you need to extend this further. +# +# These rules permit rootless Docker's pasta network driver to: +# - ptrace_read the rootlesskit child to enter its user namespace +# - read the netns file rootlesskit writes +# - read per-process socket tables for implicit port-forwarding + + ptrace (read) peer=unconfined, + /run/user/[0-9]*/dockerd-rootless/netns r, + /proc/*/net/tcp r, + /proc/*/net/tcp6 r, + /proc/*/net/udp r, + /proc/*/net/udp6 r, +EOF + checkSuccess "Wrote /etc/apparmor.d/local/usr.bin.passt" + + # 3. Reload the profile. apparmor_parser -r updates an in-kernel profile + # in place, so already-running pasta processes see the new rules on the + # next syscall — no daemon restart strictly needed, but the caller + # restarts docker anyway when override.conf changes. + if sudo apparmor_parser -r /etc/apparmor.d/usr.bin.passt 2>/dev/null; then + isSuccessful "Reloaded passt AppArmor profile" + else + isError "apparmor_parser -r failed — passt may still deny pasta. Check /var/log/syslog or journalctl for the error." + return 1 + fi +} diff --git a/scripts/docker/install/rootless/rootless_docker.sh b/scripts/docker/install/rootless/rootless_docker.sh index a3b340f..b0337ce 100755 --- a/scripts/docker/install/rootless/rootless_docker.sh +++ b/scripts/docker/install/rootless/rootless_docker.sh @@ -157,6 +157,14 @@ EOL" local 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 diff --git a/scripts/source/files/arrays/files_docker.sh b/scripts/source/files/arrays/files_docker.sh index 38bf19c..eac14d2 100755 --- a/scripts/source/files/arrays/files_docker.sh +++ b/scripts/source/files/arrays/files_docker.sh @@ -39,6 +39,7 @@ docker_scripts=( "docker/install/rooted/rooted_docker_check.sh" "docker/install/rooted/rooted_docker_compose.sh" "docker/install/rooted/rooted_docker.sh" + "docker/install/rootless/rootless_apparmor.sh" "docker/install/rootless/rootless_docker.sh" "docker/install/rootless/rootless_start_setup.sh" "docker/install/rootless/rootless_uninstall.sh"