fix(docker): run hostexec as container user

This commit is contained in:
Pi Web Agent
2026-06-21 17:40:45 +00:00
parent 3e52939aad
commit 92e87ff1e4
3 changed files with 96 additions and 16 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Run Docker host command bridge commands as the PI WEB container user by default, with `hostexec --root` for administrative commands.
+6 -6
View File
@@ -16,7 +16,7 @@ By design, the runtime containers get deliberate host access so PI WEB agents ca
- `/var/run/docker.sock` is mounted into the containers. The Docker socket is root-equivalent on the host. - `/var/run/docker.sock` is mounted into the containers. The Docker socket is root-equivalent on the host.
- `/srv`, `/opt`, and `/home` are mounted read/write. - `/srv`, `/opt`, and `/home` are mounted read/write.
- `/` is mounted read-only at `/host` for inspection. - `/` is mounted read-only at `/host` for inspection.
- `hostexec` can start a temporary privileged helper container and run explicit commands in the host namespaces. - `hostexec` can start a temporary privileged helper container and run explicit commands in the host namespaces. Commands run as the container user by default, and `hostexec --root` can still run explicit administrative commands as root.
Only install this on machines where the PI WEB user, the selected workspaces, and the browser/API clients are trusted. Review scripts before piping them to `sh` if you do not already trust this repository. Only install this on machines where the PI WEB user, the selected workspaces, and the browser/API clients are trusted. Review scripts before piping them to `sh` if you do not already trust this repository.
@@ -150,16 +150,16 @@ If you use a reverse proxy, keep the container bound to localhost or a private a
## `hostexec` examples ## `hostexec` examples
`hostexec <command...>` is the only host command bridge provided by this Docker setup. It intentionally does not abstract package managers or detect distributions. `hostexec [--root] <command...>` is the only host command bridge provided by this Docker setup. It intentionally does not abstract package managers or detect distributions. By default, commands run as the same numeric user/group as the PI WEB container. Use `--root` only for administrative host commands.
Run it from a PI WEB session, a PI WEB terminal, or by execing into the runtime container: Run it from a PI WEB session, a PI WEB terminal, or by execing into the runtime container:
```bash ```bash
hostexec uname -a hostexec uname -a
hostexec systemctl status docker hostexec systemctl status docker
hostexec zypper refresh hostexec --root zypper refresh
hostexec sh -lc 'zypper refresh && zypper dup -y' hostexec --root sh -lc 'zypper refresh && zypper dup -y'
hostexec apt-get update hostexec --root apt-get update
``` ```
From the host shell, for a quick smoke test: From the host shell, for a quick smoke test:
@@ -169,7 +169,7 @@ cd ~/.local/share/pi-web-docker
docker compose exec web hostexec uname -a docker compose exec web hostexec uname -a
``` ```
`hostexec` starts a temporary privileged helper container through the mounted Docker socket, enters the host namespaces with `nsenter`, and runs exactly the command you passed. Treat it like running a privileged host command. `hostexec` starts a temporary privileged helper container through the mounted Docker socket, enters the host namespaces with `nsenter`, and runs exactly the command you passed. Treat it like privileged host access even when the final command drops back to the container user.
## Development Docker setup ## Development Docker setup
+85 -10
View File
@@ -3,13 +3,36 @@ set -euo pipefail
usage() { usage() {
cat >&2 <<'EOF' cat >&2 <<'EOF'
Usage: hostexec <command...> Usage: hostexec [--root] [--] <command...>
Run a command on the Docker host by starting a temporary privileged helper Run a command on the Docker host by starting a temporary privileged helper
container through the mounted Docker socket and entering the host namespaces. container through the mounted Docker socket and entering the host namespaces.
Commands run as the current container UID/GID by default. Use --root to keep
root privileges for administrative host commands.
EOF EOF
} }
run_as_root=false
while [ "$#" -gt 0 ]; do
case "$1" in
--root)
run_as_root=true
shift
;;
--help|-h)
usage
exit 0
;;
--)
shift
break
;;
*)
break
;;
esac
done
if [ "$#" -eq 0 ]; then if [ "$#" -eq 0 ]; then
usage usage
exit 64 exit 64
@@ -30,19 +53,71 @@ if [[ "$docker_host" == unix://* ]]; then
fi fi
helper_image="${HOSTEXEC_IMAGE:-alpine:3.22}" helper_image="${HOSTEXEC_IMAGE:-alpine:3.22}"
target_uid="$(id -u)"
target_gid="$(id -g)"
tty_args=(--interactive) tty_args=(--interactive)
if [ -t 0 ] && [ -t 1 ]; then if [ -t 0 ] && [ -t 1 ]; then
tty_args+=(--tty) tty_args+=(--tty)
fi fi
docker_args=(
--rm
"${tty_args[@]}"
--pull=missing
--privileged
--security-opt label=disable
--pid=host
--network=host
--volume /:/host:rw
)
if [ "$run_as_root" = true ] || { [ "$target_uid" = 0 ] && [ "$target_gid" = 0 ]; }; then
exec docker run \
"${docker_args[@]}" \
"$helper_image" \
nsenter -t 1 -m -u -i -n -p -- "$@"
fi
run_as_container_user='target_uid="${HOSTEXEC_TARGET_UID:?}"
target_gid="${HOSTEXEC_TARGET_GID:?}"
target_user=""
if command -v getent >/dev/null 2>&1; then
passwd_entry="$(getent passwd "$target_uid" || true)"
if [ -n "$passwd_entry" ]; then
target_user="${passwd_entry%%:*}"
fi
fi
if [ -n "$target_user" ]; then
if command -v runuser >/dev/null 2>&1; then
exec runuser -u "$target_user" -- "$@"
fi
if command -v su >/dev/null 2>&1; then
exec su -s /bin/sh -c '\''exec "$@"'\'' -- "$target_user" hostexec-su "$@"
fi
fi
if command -v setpriv >/dev/null 2>&1; then
if [ -n "$target_user" ]; then
exec setpriv --reuid "$target_uid" --regid "$target_gid" --init-groups -- "$@"
fi
exec setpriv --reuid "$target_uid" --regid "$target_gid" --clear-groups -- "$@"
fi
if command -v nsenter >/dev/null 2>&1; then
exec nsenter -t 1 -m -u -i -n -p -S "$target_uid" -G "$target_gid" -- "$@"
fi
echo "hostexec: unable to switch to host uid:gid $target_uid:$target_gid" >&2
exit 69
'
exec docker run \ exec docker run \
--rm \ "${docker_args[@]}" \
"${tty_args[@]}" \ --env HOSTEXEC_TARGET_UID="$target_uid" \
--pull=missing \ --env HOSTEXEC_TARGET_GID="$target_gid" \
--privileged \
--security-opt label=disable \
--pid=host \
--network=host \
--volume /:/host:rw \
"$helper_image" \ "$helper_image" \
nsenter -t 1 -m -u -i -n -p -- "$@" nsenter -t 1 -m -u -i -n -p -- /bin/sh -c "$run_as_container_user" hostexec-user "$@"