Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/rootless.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,14 @@ root and several users, `brig-ctl rootless` against one shared install is the
better trade.

The host prerequisites above still apply, and `install.sh --user` checks them
before it downloads anything. When sudo runs without a password, it goes on and
the rootless setup makes them with sudo at the end. When it does not, the
installer stops there. It prints the commands that prepare the host for this
user and this prefix, as one block for root:
before it downloads anything. When sudo runs without a password, as on a cloud
image, the installer makes them itself: it installs `uidmap` and `acl` and adds
a subuid range before the download, and the rootless setup makes the rest at
the end. When sudo needs a password, a user in the `sudo`, `admin` or `wheel` group is
asked for it once on the terminal, and the install goes on the same way. Any
other user, or a run with no terminal, stops there. The installer prints the
commands that prepare the host for this user and this prefix, as one block for
root:

```console
$ curl -fsSL https://raw.githubusercontent.com/NOFireAI/brig-standalone-linux/main/install.sh | sh -
Expand Down
116 changes: 101 additions & 15 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ setup_env() {
case "${INSTALL_BRIG_VERBOSE:-false}" in true|1) VERBOSE=true ;; esac
# A user install is the rootless path, so it implies the rootless bundle.
WANT_ROOTLESS="${INSTALL_BRIG_ROOTLESS:-false}"
# Where sudo can ask for a password. A test points it at a file.
TTY="${INSTALL_BRIG_TTY:-/dev/tty}"
[ "$INSTALL_MODE" = "user" ] && WANT_ROOTLESS=true

SKIP_START="${INSTALL_BRIG_SKIP_START:-false}"
Expand Down Expand Up @@ -237,7 +239,10 @@ verify_system() {
command -v "$tool" >/dev/null 2>&1 || fatal "'$tool' is required and was not found"
done

command -v iptables >/dev/null 2>&1 || warn "iptables not found, urunc cannot set up NAT for guests"
# A user install makes it with the other packages: see verify_rootless_prereqs.
if [ "$INSTALL_MODE" != "user" ]; then
have_iptables || warn "iptables not found, so a sandbox network cannot start: the CNI firewall plugin runs it"
fi

# Probe for devmapper thin-pool support so the pool can be provisioned (and
# devmapper made switchable at runtime). Missing tools or a kernel without
Expand All @@ -263,23 +268,87 @@ verify_system() {
#
# - No root can fix a missing login session or systemd, so those stop the
# install.
# - Packages and a subuid range are root's, and brig-rootless-setup.sh does
# not make them, so those stop the install too.
# - Packages and a subuid range brig-rootless-setup.sh does not make. With a
# sudo that runs without a password, the installer makes them here, before
# the download. A fresh Ubuntu cloud image lacks uidmap and acl, and
# stopping on them stopped a user who had that sudo.
# - The rest brig-rootless-setup.sh makes with sudo at the end of the
# install. Without a sudo that runs without a password, it would stop at a
# prompt after the whole bundle is downloaded and unpacked. So the install
# stops here instead.
# install.
#
# Without a sudo that runs without a password, any of these would stop at a
# prompt after the whole bundle is downloaded and unpacked. So the install
# stops here instead.
#
# Whatever needs root is printed as one block of commands for this user and
# this prefix. An admin runs it once, and the next install needs no sudo. The
# checks and the commands mirror brig-rootless-setup.sh; keep them in step.
# Debian keeps it in /usr/sbin, which a user's PATH does not have, and the CNI
# plugins find it there all the same.
have_iptables() {
command -v iptables >/dev/null 2>&1 || [ -x /usr/sbin/iptables ] || [ -x /sbin/iptables ]
}

# install_tools_cmd <tool>...: the root command that installs the tools, with the
# package manager this host has and the names it gives them. newuidmap is in
# uidmap on Debian and Ubuntu and in shadow elsewhere; setfacl is in acl
# everywhere; iptables is iptables-nft on the RHEL family and iptables elsewhere. A host with none of these gets a comment, which installs nothing,
# so the check that follows still stops on the missing tool.
install_tools_cmd() {
names=""
for tool in "$@"; do
case "$tool" in
newuidmap) if command -v apt-get >/dev/null 2>&1; then names="$names uidmap"
elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then names="$names shadow-utils"
else names="$names shadow"; fi ;;
setfacl) names="$names acl" ;;
iptables) if command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then names="$names iptables-nft"
else names="$names iptables"; fi ;;
esac
done
if command -v apt-get >/dev/null 2>&1; then
echo "DEBIAN_FRONTEND=noninteractive apt-get install -y$names </dev/null"
elif command -v dnf >/dev/null 2>&1; then
echo "dnf install -y$names"
elif command -v yum >/dev/null 2>&1; then
echo "yum install -y$names"
elif command -v zypper >/dev/null 2>&1; then
echo "zypper --non-interactive install$names"
elif command -v pacman >/dev/null 2>&1; then
echo "pacman -S --noconfirm --needed$names"
else
echo "# install$names with this host's package manager: none that this installer knows is here"
fi
}

# Whether sudo runs without a password now. Under `curl ... | sh`, stdin is the
# script, so a user whose sudo needs a password is asked for it here, once, on
# the terminal: a user in a sudo group, and only when there is a terminal.
sudo_ready() {
sudo -n true 2>/dev/null && return 0
[ -z "${SUDO_ASKED:-}" ] || return 1
SUDO_ASKED=true
case " $(id -nG 2>/dev/null) " in
*" sudo "* | *" admin "* | *" wheel "*) ;;
*) return 1 ;;
esac
( : < "$TTY" ) 2>/dev/null || return 1
warn "this host needs root's setup, so sudo asks for your password once"
# The terminal is where the password comes from, not a file sudo reads.
# shellcheck disable=SC2024
sudo -v < "$TTY" || return 1
sudo -n true 2>/dev/null
}

verify_rootless_prereqs() {
me="$(id -un)"
miss=""
need=""
setup_can=true
fix="$TMP_DIR/root-commands.sh"
# The part of $fix that brig-rootless-setup.sh does not make.
own="$TMP_DIR/root-own.sh"
: > "$fix"
: > "$own"

[ -d "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" ] \
|| miss="$miss\n no ${XDG_RUNTIME_DIR:-/run/user/$(id -u)}: log in as this user first"
Expand All @@ -288,12 +357,17 @@ verify_rootless_prereqs() {

pkgs=""
command -v newuidmap >/dev/null 2>&1 \
|| { need="$need\n newuidmap is missing"; pkgs="$pkgs uidmap"; }
|| { need="$need\n newuidmap is missing"; pkgs="$pkgs newuidmap"; }
command -v setfacl >/dev/null 2>&1 \
|| { need="$need\n setfacl is missing"; pkgs="$pkgs acl"; }
|| { need="$need\n setfacl is missing"; pkgs="$pkgs setfacl"; }
# The CNI firewall plugin of every sandbox network runs it. openSUSE Leap 16
# ships nftables alone, and there every run failed in the network setup.
have_iptables \
|| { need="$need\n iptables is missing"; pkgs="$pkgs iptables"; }
if [ -n "$pkgs" ]; then
setup_can=false
echo "DEBIAN_FRONTEND=noninteractive apt-get install -y$pkgs </dev/null" >> "$fix"
# shellcheck disable=SC2086
install_tools_cmd $pkgs | tee -a "$own" >> "$fix"
fi

# A range that overlaps another user's maps both users' containers onto the
Expand All @@ -304,7 +378,7 @@ verify_rootless_prereqs() {
first=$(cat /etc/subuid /etc/subgid 2>/dev/null \
| awk -F: '$2 + $3 > m {m = $2 + $3} END {print (m > 100000 ? m : 100000)}')
last=$((first + 65535))
echo "usermod --add-subuids $first-$last --add-subgids $first-$last $me" >> "$fix"
echo "usermod --add-subuids $first-$last --add-subgids $first-$last $me" | tee -a "$own" >> "$fix"
fi

# The profile names the binary by path, so the one a /var/lib install left
Expand Down Expand Up @@ -400,7 +474,19 @@ FIX
if [ -z "$miss" ] && [ -z "$need" ]; then
return 0
fi
if [ -z "$miss" ] && [ "$setup_can" = true ] && sudo -n true 2>/dev/null; then
if [ -z "$miss" ] && [ "$setup_can" = false ] && [ -z "${OWN_MADE:-}" ] \
&& sudo_ready; then
OWN_MADE=true
warn "making what brig-rootless-setup.sh does not, with sudo:"
sed 's/^/ /' "$own" >&2
# A distribution without apt-get fails here, and the host is then one the
# commands below are for. Otherwise the check runs again on the new host.
if sudo -n sh -eu "$own" >&2; then
verify_rootless_prereqs
return 0
fi
fi
if [ -z "$miss" ] && [ "$setup_can" = true ] && sudo_ready; then
# Counted, not assumed: this said "two" whatever it had found, which
# reads as a second thing the reader has missed.
n=$(printf '%b' "$need" | grep -c '^ ')
Expand All @@ -415,10 +501,10 @@ FIX
# shellcheck disable=SC2059
printf "[brig-install] ERROR: this host is not set up for a rootless brig:$miss$need\n\n" >&2
if [ -n "$need" ]; then
if [ "$setup_can" = true ]; then
why="sudo cannot run here without a password"
if [ -n "${OWN_MADE:-}" ]; then
why="making them with sudo failed, above"
else
why="the installer does not install packages or add subuid ranges"
why="sudo cannot run here without a password"
fi
cat >&2 <<MSG
Nothing was downloaded: $why.
Expand All @@ -430,7 +516,7 @@ $(cat "$fix")
BRIG_ROOT

MSG
if [ "$setup_can" = true ]; then
if [ -z "${OWN_MADE:-}" ]; then
echo " With sudo of your own, run 'sudo -v' and then this installer again: it" >&2
echo " makes these itself." >&2
fi
Expand Down
121 changes: 117 additions & 4 deletions tests/install-preflight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,40 @@ T="$(mktemp -d)"
trap 'rm -rf "$T"' EXIT
mkdir -p "$T/stub" "$T/home" "$T/run"

# sudo: only -n true can succeed, and only when the test says sudo works.
# sudo: only -n true can succeed, and only when the test says sudo works. With
# that, -n sh -eu <file> is the installer making packages: it keeps the file,
# and "installs" them as stubs unless the test says the install fails.
cat > "$T/stub/sudo" <<STUB
#!/bin/sh
echo "\$*" >> "$T/sudo.log"
[ "\$*" = "-n true" ] && [ -f "$T/sudo-ok" ] && exit 0
# -v is the password prompt, which the test answers right or not at all.
if [ "\$1" = -v ]; then
[ -f "$T/sudo-pass" ] || exit 1
touch "$T/sudo-ok"; exit 0
fi
[ -f "$T/sudo-ok" ] || exit 1
[ "\$*" = "-n true" ] && exit 0
if [ "\$1 \$2 \$3" = "-n sh -eu" ]; then
cp "\$4" "$T/sudo-ran.sh"
[ -f "$T/apt-fails" ] && exit 100
for b in newuidmap setfacl iptables; do
printf '#!/bin/sh\nexit 0\n' > "$T/stub/\$b"; chmod 0755 "$T/stub/\$b"
done
exit 0
fi
exit 1
STUB

# id -nG names a sudo group only when the test says this user is in one.
cat > "$T/stub/id" <<STUB
#!/bin/sh
if [ "\$1" = -nG ]; then
if [ -f "$T/in-sudo" ]; then echo "$user sudo"; else echo "$user"; fi
exit 0
fi
exec /usr/bin/id "\$@"
STUB

# getfacl and sysctl answer from files the test sets.
cat > "$T/stub/getfacl" <<STUB
#!/bin/sh
Expand All @@ -56,7 +82,7 @@ STUB
# modinfo: no module is missing but loadable, so the result does not depend on
# what this kernel has loaded.
printf '#!/bin/sh\nexit 1\n' > "$T/stub/modinfo"
for b in newuidmap setfacl systemctl; do
for b in newuidmap setfacl iptables systemctl; do
printf '#!/bin/sh\nexit 0\n' > "$T/stub/$b"
done
chmod 0755 "$T/stub"/*
Expand All @@ -66,7 +92,7 @@ run_install() {
set +e
env -u XDG_CONFIG_HOME -u XDG_DATA_HOME \
HOME="$T/home" XDG_RUNTIME_DIR="$T/run" PATH="$T/stub:$PATH" \
INSTALL_BRIG_BUNDLE="$T/no-such-bundle.tar.gz" \
INSTALL_BRIG_BUNDLE="$T/no-such-bundle.tar.gz" INSTALL_BRIG_TTY="$T/tty" \
sh "$here/install.sh" > "$T/out.log" 2>&1
rc=$?
set -e
Expand Down Expand Up @@ -110,6 +136,93 @@ grep -q "no-such-bundle" "$T/out.log" || { cat "$T/out.log" >&2; fail "the insta
ok "with sudo the install goes on to the download"
rm -f "$T/sudo-ok"

# 2b. A fresh Ubuntu cloud image has neither uidmap nor acl. Without sudo: stop,
# with the packages in the root commands and the way to let it use sudo.
if command -v newuidmap >/dev/null 2>&1 \
|| command -v setfacl >/dev/null 2>&1; then
echo "SKIP: this host has uidmap or acl, so their absence cannot be staged"
else
rm -f "$T/stub/newuidmap" "$T/stub/setfacl"
run_install
[ "$rc" -ne 0 ] || fail "the install went on without uidmap and acl"
grep -q "apt-get install -y uidmap acl" "$T/out.log" || { cat "$T/out.log" >&2; fail "the packages are not in the root commands"; }
grep -q "sudo -v" "$T/out.log" || { cat "$T/out.log" >&2; fail "no word that sudo -v lets the installer do it"; }
ok "without sudo, missing packages stop the install with the root commands"

# With sudo: install them before the download, check again, and go on.
touch "$T/sudo-ok"
run_install
grep -q "apt-get install -y uidmap acl" "$T/sudo-ran.sh" 2>/dev/null \
|| { cat "$T/out.log" >&2; fail "the packages were not installed with sudo"; }
if grep -q "^usermod\|udev\|apparmor" "$T/sudo-ran.sh"; then
cat "$T/sudo-ran.sh" >&2
fail "the early sudo ran what brig-rootless-setup.sh makes"
fi
grep -q "no-such-bundle" "$T/out.log" || { cat "$T/out.log" >&2; fail "the install did not go on after the packages"; }
ok "with sudo, missing packages are installed and the install goes on"

# With sudo, and an install that fails: stop, and say so.
rm -f "$T/stub/newuidmap" "$T/stub/setfacl" "$T/sudo-ran.sh"
touch "$T/apt-fails"
run_install
[ "$rc" -ne 0 ] || fail "the install went on after the packages failed"
grep -q "making them with sudo failed" "$T/out.log" || { cat "$T/out.log" >&2; fail "no word that sudo failed"; }
if grep -q "sudo -v" "$T/out.log"; then
fail "a user whose sudo works was told to run sudo -v"
fi
ok "a failed package install stops the install"
rm -f "$T/apt-fails" "$T/sudo-ok"

# sudo with a password, from a terminal, for a user in a sudo group: asked
# once, and then the same as sudo without one.
rm -f "$T/stub/newuidmap" "$T/stub/setfacl" "$T/sudo-ran.sh"
: > "$T/tty"
touch "$T/in-sudo" "$T/sudo-pass"
run_install
[ "$(grep -c '^-v$' "$T/sudo.log")" = 1 ] || { cat "$T/sudo.log" >&2; fail "sudo was not asked for the password once"; }
grep -q "apt-get install -y uidmap acl" "$T/sudo-ran.sh" 2>/dev/null \
|| { cat "$T/out.log" >&2; fail "the packages were not installed after the password"; }
grep -q "no-such-bundle" "$T/out.log" || { cat "$T/out.log" >&2; fail "the install did not go on after the password"; }
ok "a user in a sudo group is asked for the password and the install goes on"

# No terminal, or not in a sudo group: no prompt, and the stop as before.
rm -f "$T/stub/newuidmap" "$T/stub/setfacl" "$T/sudo-ok" "$T/tty"
run_install
if grep -q '^-v$' "$T/sudo.log"; then fail "sudo was asked for a password with no terminal"; fi
grep -q "Nothing was downloaded" "$T/out.log" || { cat "$T/out.log" >&2; fail "no stop without a terminal"; }
: > "$T/tty"
rm -f "$T/in-sudo"
run_install
if grep -q '^-v$' "$T/sudo.log"; then fail "a user in no sudo group was asked for a password"; fi
ok "no terminal, or no sudo group, means no password prompt"
rm -f "$T/sudo-pass" "$T/tty"
for b in newuidmap setfacl iptables; do
printf '#!/bin/sh\nexit 0\n' > "$T/stub/$b"; chmod 0755 "$T/stub/$b"
done
fi

# 2c. The packages come from the package manager the host has, by its names.
sed -n '/^install_tools_cmd()/,/^}/p' "$here/install.sh" > "$T/tools-fn.sh"
for pm in apt-get dnf yum zypper pacman none; do
mkdir -p "$T/pm-$pm"
[ "$pm" = none ] || { printf '#!/bin/sh\n' > "$T/pm-$pm/$pm"; chmod 0755 "$T/pm-$pm/$pm"; }
got="$(PATH="$T/pm-$pm" /bin/sh -c ". '$T/tools-fn.sh'; install_tools_cmd newuidmap setfacl")"
case "$pm" in
apt-get) want="DEBIAN_FRONTEND=noninteractive apt-get install -y uidmap acl </dev/null" ;;
dnf) want="dnf install -y shadow-utils acl" ;;
yum) want="yum install -y shadow-utils acl" ;;
zypper) want="zypper --non-interactive install shadow acl" ;;
pacman) want="pacman -S --noconfirm --needed shadow acl" ;;
none) want="# install shadow acl with this host's package manager: none that this installer knows is here" ;;
esac
[ "$got" = "$want" ] || fail "with $pm: got '$got', want '$want'"
done
got="$(PATH="$T/pm-dnf" /bin/sh -c ". '$T/tools-fn.sh'; install_tools_cmd iptables")"
[ "$got" = "dnf install -y iptables-nft" ] || fail "iptables on dnf: got '$got'"
got="$(PATH="$T/pm-zypper" /bin/sh -c ". '$T/tools-fn.sh'; install_tools_cmd iptables")"
[ "$got" = "zypper --non-interactive install iptables" ] || fail "iptables on zypper: got '$got'"
ok "the packages come from the host's own package manager"

# 3. Prepared by an admin, and no sudo: go on without asking for it.
if [ -d /sys/module/vhost_vsock ] && ! grep -qsx vhost_vsock /etc/modules-load.d/*.conf /etc/modules; then
skip "vhost_vsock is loaded here but not at boot, which a prepared host has"
Expand Down
2 changes: 1 addition & 1 deletion tests/install-record.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ STUB
printf '#!/bin/sh\necho 0\n' > "$T/stub/sysctl"
printf '#!/bin/sh\n[ "$*" = "-n true" ]\n' > "$T/stub/sudo"
printf '#!/bin/sh\nexit 1\n' > "$T/stub/modinfo"
for b in newuidmap setfacl systemctl cosign; do
for b in newuidmap setfacl iptables systemctl cosign; do
printf '#!/bin/sh\nexit 0\n' > "$T/stub/$b"
done
chmod 0755 "$T/stub"/*
Expand Down
2 changes: 1 addition & 1 deletion tests/install-summary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ STUB
printf '#!/bin/sh\necho 0\n' > "$T/stub/sysctl"
printf '#!/bin/sh\n[ "$*" = "-n true" ]\n' > "$T/stub/sudo"
printf '#!/bin/sh\nexit 1\n' > "$T/stub/modinfo"
for b in newuidmap setfacl systemctl; do
for b in newuidmap setfacl iptables systemctl; do
printf '#!/bin/sh\nexit 0\n' > "$T/stub/$b"
done
chmod 0755 "$T/stub"/*
Expand Down