diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3ff975..62122a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,15 +16,32 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends binfmt-support qemu-user-static + + - name: hack in qemu-binfmt + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends binfmt-support qemu-user-static + - run: ./build.sh + + # explicitly test our (native) amd64/i386 binaries - run: ./test.sh gosu-amd64 - run: ./test.sh gosu-i386 - run: ./test.sh --debian gosu-amd64 - run: ./test.sh --debian gosu-i386 + + # now that we've successfully tested gosu itself, let's hack the test suite a little bit to not use setuid (and to have util-linux's setpriv installed) so we can also smoke test "setpriv-wrapper.sh" + - name: hack tests for setpriv + run: | + sed -ri -e '/^USER /d' Dockerfile.test-* + awk '{ print } toupper($1) == "FROM" { print "RUN apk add --no-cache setpriv" }' Dockerfile.test-alpine > Dockerfile.test-alpine.new + mv Dockerfile.test-alpine.new Dockerfile.test-alpine + - run: ./test.sh setpriv-wrapper.sh + - run: ./test.sh --debian setpriv-wrapper.sh + + # smoke test our Docker image builds - run: docker build --pull --file hub/Dockerfile.alpine hub - run: docker build --pull --file hub/Dockerfile.debian hub + # run "govulncheck" automatically to ensure we don't have any new/unknown vulnerabilities - uses: actions/setup-go@v4 with: go-version: 1.18 diff --git a/Dockerfile.test-alpine b/Dockerfile.test-alpine index 1277bd9..66d839f 100644 --- a/Dockerfile.test-alpine +++ b/Dockerfile.test-alpine @@ -1,4 +1,4 @@ -FROM alpine:3.19 +FROM alpine:3.20 # add "nobody" to ALL groups (makes testing edge cases more interesting) RUN cut -d: -f1 /etc/group | xargs -rtn1 addgroup nobody diff --git a/README.md b/README.md index 06dd7e5..2b0bb1f 100644 --- a/README.md +++ b/README.md @@ -58,24 +58,26 @@ If you're curious about the edge cases that `gosu` handles, see [`Dockerfile.tes ## Alternatives -### `chroot` +### `setpriv` -With the `--userspec` flag, `chroot` can provide similar benefits/behavior: +Available in newer `util-linux` (`>= 2.32.1-0.2`, in Debian; https://manpages.debian.org/buster/util-linux/setpriv.1.en.html): ```console -$ docker run -it --rm ubuntu:trusty chroot --userspec=nobody / ps aux +$ docker run -it --rm buildpack-deps:buster-scm setpriv --reuid=nobody --regid=nogroup --init-groups ps faux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND -nobody 1 5.0 0.0 7136 756 ? Rs+ 17:04 0:00 ps aux +nobody 1 5.0 0.0 9592 1252 pts/0 RNs+ 23:21 0:00 ps faux ``` -### `setpriv` +In this repository, you'll find an official `setpriv-wrapper.sh` which implements a `gosu`-compatible interface which passes `gosu`'s test suite. -Available in newer `util-linux` (`>= 2.32.1-0.2`, in Debian; https://manpages.debian.org/buster/util-linux/setpriv.1.en.html): +### `chroot` + +With the `--userspec` flag, `chroot` can provide similar benefits/behavior: ```console -$ docker run -it --rm buildpack-deps:buster-scm setpriv --reuid=nobody --regid=nogroup --init-groups ps faux +$ docker run -it --rm ubuntu:trusty chroot --userspec=nobody / ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND -nobody 1 5.0 0.0 9592 1252 pts/0 RNs+ 23:21 0:00 ps faux +nobody 1 5.0 0.0 7136 756 ? Rs+ 17:04 0:00 ps aux ``` ### `su-exec` diff --git a/setpriv-wrapper.sh b/setpriv-wrapper.sh new file mode 100755 index 0000000..5076597 --- /dev/null +++ b/setpriv-wrapper.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env sh +set -eu + +# +# This script is a wrapper around "setpriv" (part of util-linux) which implements a fully gosu-compatible interface (and passes gosu's test suite). +# +# It is written in POSIX shell for maximum compatbility, but notably does *not* work with BusyBox's setpriv (as of 2024-06-03) as BusyBox does not implement enough functionality. +# + +# TODO GOSU_PLEASE_LET_ME_BE_COMPLETELY_INSECURE_I_GET_TO_KEEP_ALL_THE_PIECES (block setuid/setgid) -- however, you can't effectively setuid a shell script/interpreted file, so maybe it's fine? 🤔 + +usage() { + cat <<-EOU + Usage: $0 user-spec command [args] + eg: $0 tianon bash + $0 nobody:root bash -c 'whoami && id' + $0 1000:1 id + + $0 license: Apache-2.0 (full text at https://github.com/tianon/gosu) + + EOU +} + +case "${1:-}" in + --help | -h | '-?') usage; exit 0 ;; + --version | -v) echo '???'; exit 0 ;; +esac +if [ "$#" -lt 2 ]; then + usage >&2 + exit 1 +fi + +spec="$1"; shift +: "${spec:=0}" +spec="${spec%:}" # "0:" is parsed by moby/sys/user the same as "0" +case "$spec" in + *:*) + user="${spec%%:*}" + group="${spec#$user:}" + [ "$group" != "$spec" ] + : "${user:=0}" + passwd="$(getent passwd "$user" || :)" # for HOME scraping below + set -- --reuid "$user" --regid "$group" --clear-groups -- "$@" + ;; + + *) + user="$spec" + if passwd="$(getent passwd "$user")" && [ -n "$passwd" ]; then + group="$(printf '%s' "$passwd" | cut -d: -f4)" + set -- --reuid "$user" --regid "$group" --init-groups -- "$@" + else + passwd= # to be safe/explicit (for HOME scraping below) + case "$user" in + *[!0-9]* | '') echo >&2 "error: '$user' is not a user (and is also not numeric)"; exit 1 ;; + *) group='0' ;; + # (thanks to https://stackoverflow.com/a/16444570/433558 for this perfect pure-POSIX "is it fully numeric" hack!) + esac + set -- --reuid "$user" --regid "$group" --clear-groups -- "$@" + fi + ;; +esac + +unset HOME +HOME="$(printf '%s' "$passwd" | cut -d: -f6)" +: "${HOME:=/}" # see "setup-user.go" +export HOME + +exec setpriv "$@"