moby/contrib/dockerd-rootless-setuptool.sh
Akihiro Suda b32cfc3b3a
dockerd-rootless-setuptool.sh: check RootlessKit functionality
RootlessKit will print hints if something is still unsatisfied.

e.g., `kernel.apparmor_restrict_unprivileged_userns` constraint
rootless-containers/rootlesskit@33c3e7ca6c

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
2024-03-06 07:43:00 +09:00

529 lines
14 KiB
Bash
Executable file

#!/bin/sh
# dockerd-rootless-setuptool.sh: setup tool for dockerd-rootless.sh
# Needs to be executed as a non-root user.
#
# Typical usage: dockerd-rootless-setuptool.sh install --force
#
# Documentation: https://docs.docker.com/go/rootless/
set -eu
# utility functions
INFO() {
/bin/echo -e "\e[104m\e[97m[INFO]\e[49m\e[39m $@"
}
WARNING() {
/bin/echo >&2 -e "\e[101m\e[97m[WARNING]\e[49m\e[39m $@"
}
ERROR() {
/bin/echo >&2 -e "\e[101m\e[97m[ERROR]\e[49m\e[39m $@"
}
# constants
DOCKERD_ROOTLESS_SH="dockerd-rootless.sh"
SYSTEMD_UNIT="docker.service"
CLI_CONTEXT="rootless"
# CLI opt: --force
OPT_FORCE=""
# CLI opt: --skip-iptables
OPT_SKIP_IPTABLES=""
# global vars
ARG0="$0"
DOCKERD_ROOTLESS_SH_FLAGS=""
BIN=""
SYSTEMD=""
CFG_DIR=""
XDG_RUNTIME_DIR_CREATED=""
USERNAME=""
USERNAME_ESCAPED=""
# run checks and also initialize global vars
init() {
# OS verification: Linux only
case "$(uname)" in
Linux) ;;
*)
ERROR "Rootless Docker cannot be installed on $(uname)"
exit 1
;;
esac
# User verification: deny running as root
if [ "$(id -u)" = "0" ]; then
ERROR "Refusing to install rootless Docker as the root user"
exit 1
fi
# set BIN
if ! BIN="$(command -v "$DOCKERD_ROOTLESS_SH" 2> /dev/null)"; then
ERROR "$DOCKERD_ROOTLESS_SH needs to be present under \$PATH"
exit 1
fi
BIN=$(dirname "$BIN")
# set SYSTEMD
if systemctl --user show-environment > /dev/null 2>&1; then
SYSTEMD=1
fi
# HOME verification
if [ -z "${HOME:-}" ] || [ ! -d "$HOME" ]; then
ERROR "HOME needs to be set"
exit 1
fi
if [ ! -w "$HOME" ]; then
ERROR "HOME needs to be writable"
exit 1
fi
# Set USERNAME from `id -un` and potentially protect backslash
# for windbind/samba domain users
USERNAME=$(id -un)
USERNAME_ESCAPED=$(echo $USERNAME | sed 's/\\/\\\\/g')
# set CFG_DIR
CFG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
# Existing rootful docker verification
if [ -w /var/run/docker.sock ] && [ -z "$OPT_FORCE" ]; then
ERROR "Aborting because rootful Docker (/var/run/docker.sock) is running and accessible. Set --force to ignore."
exit 1
fi
# Validate XDG_RUNTIME_DIR and set XDG_RUNTIME_DIR_CREATED
if [ -z "${XDG_RUNTIME_DIR:-}" ] || [ ! -w "$XDG_RUNTIME_DIR" ]; then
if [ -n "$SYSTEMD" ]; then
ERROR "Aborting because systemd was detected but XDG_RUNTIME_DIR (\"$XDG_RUNTIME_DIR\") is not set, does not exist, or is not writable"
ERROR "Hint: this could happen if you changed users with 'su' or 'sudo'. To work around this:"
ERROR "- try again by first running with root privileges 'loginctl enable-linger <user>' where <user> is the unprivileged user and export XDG_RUNTIME_DIR to the value of RuntimePath as shown by 'loginctl show-user <user>'"
ERROR "- or simply log back in as the desired unprivileged user (ssh works for remote machines, machinectl shell works for local machines)"
exit 1
fi
export XDG_RUNTIME_DIR="$HOME/.docker/run"
mkdir -p -m 700 "$XDG_RUNTIME_DIR"
XDG_RUNTIME_DIR_CREATED=1
fi
instructions=""
# instruction: uidmap dependency check
if ! command -v newuidmap > /dev/null 2>&1; then
if command -v apt-get > /dev/null 2>&1; then
instructions=$(
cat <<- EOI
${instructions}
# Install newuidmap & newgidmap binaries
apt-get install -y uidmap
EOI
)
elif command -v dnf > /dev/null 2>&1; then
instructions=$(
cat <<- EOI
${instructions}
# Install newuidmap & newgidmap binaries
dnf install -y shadow-utils
EOI
)
elif command -v yum > /dev/null 2>&1; then
instructions=$(
cat <<- EOI
${instructions}
# Install newuidmap & newgidmap binaries
yum install -y shadow-utils
EOI
)
else
ERROR "newuidmap binary not found. Please install with a package manager."
exit 1
fi
fi
# instruction: iptables dependency check
faced_iptables_error=""
if ! command -v iptables > /dev/null 2>&1 && [ ! -f /sbin/iptables ] && [ ! -f /usr/sbin/iptables ]; then
faced_iptables_error=1
if [ -z "$OPT_SKIP_IPTABLES" ]; then
if command -v apt-get > /dev/null 2>&1; then
instructions=$(
cat <<- EOI
${instructions}
# Install iptables
apt-get install -y iptables
EOI
)
elif command -v dnf > /dev/null 2>&1; then
instructions=$(
cat <<- EOI
${instructions}
# Install iptables
dnf install -y iptables
EOI
)
elif command -v yum > /dev/null 2>&1; then
instructions=$(
cat <<- EOI
${instructions}
# Install iptables
yum install -y iptables
EOI
)
else
ERROR "iptables binary not found. Please install with a package manager."
exit 1
fi
fi
fi
# instruction: ip_tables module dependency check
if ! grep -q ip_tables /proc/modules 2> /dev/null && ! grep -q ip_tables /lib/modules/$(uname -r)/modules.builtin 2> /dev/null; then
faced_iptables_error=1
if [ -z "$OPT_SKIP_IPTABLES" ]; then
instructions=$(
cat <<- EOI
${instructions}
# Load ip_tables module
modprobe ip_tables
EOI
)
fi
fi
# set DOCKERD_ROOTLESS_SH_FLAGS
if [ -n "$faced_iptables_error" ] && [ -n "$OPT_SKIP_IPTABLES" ]; then
DOCKERD_ROOTLESS_SH_FLAGS="${DOCKERD_ROOTLESS_SH_FLAGS} --iptables=false"
fi
# instruction: Debian and Arch require setting unprivileged_userns_clone
if [ -f /proc/sys/kernel/unprivileged_userns_clone ]; then
if [ "1" != "$(cat /proc/sys/kernel/unprivileged_userns_clone)" ]; then
instructions=$(
cat <<- EOI
${instructions}
# Set kernel.unprivileged_userns_clone
cat <<EOT > /etc/sysctl.d/50-rootless.conf
kernel.unprivileged_userns_clone = 1
EOT
sysctl --system
EOI
)
fi
fi
# instruction: RHEL/CentOS 7 requires setting max_user_namespaces
if [ -f /proc/sys/user/max_user_namespaces ]; then
if [ "0" = "$(cat /proc/sys/user/max_user_namespaces)" ]; then
instructions=$(
cat <<- EOI
${instructions}
# Set user.max_user_namespaces
cat <<EOT > /etc/sysctl.d/51-rootless.conf
user.max_user_namespaces = 28633
EOT
sysctl --system
EOI
)
fi
fi
# instructions: validate subuid/subgid files for current user
if ! grep -q "^$USERNAME_ESCAPED:\|^$(id -u):" /etc/subuid 2> /dev/null; then
instructions=$(
cat <<- EOI
${instructions}
# Add subuid entry for ${USERNAME}
echo "${USERNAME}:100000:65536" >> /etc/subuid
EOI
)
fi
if ! grep -q "^$USERNAME_ESCAPED:\|^$(id -u):" /etc/subgid 2> /dev/null; then
instructions=$(
cat <<- EOI
${instructions}
# Add subgid entry for ${USERNAME}
echo "${USERNAME}:100000:65536" >> /etc/subgid
EOI
)
fi
# fail with instructions if requirements are not satisfied.
if [ -n "$instructions" ]; then
ERROR "Missing system requirements. Run the following commands to"
ERROR "install the requirements and run this tool again."
if [ -n "$faced_iptables_error" ] && [ -z "$OPT_SKIP_IPTABLES" ]; then
ERROR "Alternatively iptables checks can be disabled with --skip-iptables ."
fi
echo
echo "########## BEGIN ##########"
echo "sudo sh -eux <<EOF"
echo "$instructions" | sed -e '/^$/d'
echo "EOF"
echo "########## END ##########"
echo
exit 1
fi
# TODO: support printing non-essential but recommended instructions:
# - sysctl: "net.ipv4.ping_group_range"
# - sysctl: "net.ipv4.ip_unprivileged_port_start"
# - external binary: slirp4netns
# - external binary: fuse-overlayfs
# check RootlessKit functionality. RootlessKit will print hints if something is still unsatisfied.
# (e.g., `kernel.apparmor_restrict_unprivileged_userns` constraint)
if ! rootlesskit true; then
ERROR "RootlessKit failed, see the error messages and https://rootlesscontaine.rs/getting-started/common/ ."
exit 1
fi
}
# CLI subcommand: "check"
cmd_entrypoint_check() {
init
# requirements are already checked in init()
INFO "Requirements are satisfied"
}
# CLI subcommand: "nsenter"
cmd_entrypoint_nsenter() {
# No need to call init()
pid=$(cat "$XDG_RUNTIME_DIR/dockerd-rootless/child_pid")
exec nsenter --no-fork --wd="$(pwd)" --preserve-credentials -m -n -U -t "$pid" -- "$@"
}
show_systemd_error() {
n="20"
ERROR "Failed to start ${SYSTEMD_UNIT}. Run \`journalctl -n ${n} --no-pager --user --unit ${SYSTEMD_UNIT}\` to show the error log."
ERROR "Before retrying installation, you might need to uninstall the current setup: \`$0 uninstall -f ; ${BIN}/rootlesskit rm -rf ${HOME}/.local/share/docker\`"
if journalctl -q -n ${n} --user --unit ${SYSTEMD_UNIT} | grep -qF "/run/xtables.lock: Permission denied"; then
ERROR "Failure likely related to https://github.com/moby/moby/issues/41230"
ERROR "This may work as a workaround: \`sudo dnf install -y policycoreutils-python-utils && sudo semanage permissive -a iptables_t\`"
fi
}
# install (systemd)
install_systemd() {
mkdir -p "${CFG_DIR}/systemd/user"
unit_file="${CFG_DIR}/systemd/user/${SYSTEMD_UNIT}"
if [ -f "${unit_file}" ]; then
WARNING "File already exists, skipping: ${unit_file}"
else
INFO "Creating ${unit_file}"
cat <<- EOT > "${unit_file}"
[Unit]
Description=Docker Application Container Engine (Rootless)
Documentation=https://docs.docker.com/go/rootless/
[Service]
Environment=PATH=$BIN:/sbin:/usr/sbin:$PATH
ExecStart=$BIN/dockerd-rootless.sh $DOCKERD_ROOTLESS_SH_FLAGS
ExecReload=/bin/kill -s HUP \$MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
StartLimitBurst=3
StartLimitInterval=60s
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
Delegate=yes
Type=notify
NotifyAccess=all
KillMode=mixed
[Install]
WantedBy=default.target
EOT
systemctl --user daemon-reload
fi
if ! systemctl --user --no-pager status "${SYSTEMD_UNIT}" > /dev/null 2>&1; then
INFO "starting systemd service ${SYSTEMD_UNIT}"
(
set -x
if ! systemctl --user start "${SYSTEMD_UNIT}"; then
set +x
show_systemd_error
exit 1
fi
sleep 3
)
fi
(
set -x
if ! systemctl --user --no-pager --full status "${SYSTEMD_UNIT}"; then
set +x
show_systemd_error
exit 1
fi
DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock" $BIN/docker version
systemctl --user enable "${SYSTEMD_UNIT}"
)
INFO "Installed ${SYSTEMD_UNIT} successfully."
INFO "To control ${SYSTEMD_UNIT}, run: \`systemctl --user (start|stop|restart) ${SYSTEMD_UNIT}\`"
INFO "To run ${SYSTEMD_UNIT} on system startup, run: \`sudo loginctl enable-linger ${USERNAME}\`"
echo
}
# install (non-systemd)
install_nonsystemd() {
INFO "systemd not detected, ${DOCKERD_ROOTLESS_SH} needs to be started manually:"
echo
echo "PATH=$BIN:/sbin:/usr/sbin:\$PATH ${DOCKERD_ROOTLESS_SH} ${DOCKERD_ROOTLESS_SH_FLAGS}"
echo
}
cli_ctx_exists() {
name="$1"
"${BIN}/docker" --context=default context inspect -f "{{.Name}}" "${name}" > /dev/null 2>&1
}
cli_ctx_create() {
name="$1"
host="$2"
description="$3"
"${BIN}/docker" --context=default context create "${name}" --docker "host=${host}" --description "${description}" > /dev/null
}
cli_ctx_use() {
name="$1"
"${BIN}/docker" --context=default context use "${name}" > /dev/null
}
cli_ctx_rm() {
name="$1"
"${BIN}/docker" --context=default context rm -f "${name}" > /dev/null
}
# CLI subcommand: "install"
cmd_entrypoint_install() {
init
# requirements are already checked in init()
if [ -z "$SYSTEMD" ]; then
install_nonsystemd
else
install_systemd
fi
if cli_ctx_exists "${CLI_CONTEXT}"; then
INFO "CLI context \"${CLI_CONTEXT}\" already exists"
else
INFO "Creating CLI context \"${CLI_CONTEXT}\""
cli_ctx_create "${CLI_CONTEXT}" "unix://${XDG_RUNTIME_DIR}/docker.sock" "Rootless mode"
fi
INFO "Using CLI context \"${CLI_CONTEXT}\""
cli_ctx_use "${CLI_CONTEXT}"
echo
INFO "Make sure the following environment variable(s) are set (or add them to ~/.bashrc):"
if [ -n "$XDG_RUNTIME_DIR_CREATED" ]; then
echo "# WARNING: systemd not found. You have to remove XDG_RUNTIME_DIR manually on every logout."
echo "export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}"
fi
echo "export PATH=${BIN}:\$PATH"
echo
INFO "Some applications may require the following environment variable too:"
echo "export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/docker.sock"
echo
}
# CLI subcommand: "uninstall"
cmd_entrypoint_uninstall() {
init
# requirements are already checked in init()
if [ -z "$SYSTEMD" ]; then
INFO "systemd not detected, ${DOCKERD_ROOTLESS_SH} needs to be stopped manually:"
else
unit_file="${CFG_DIR}/systemd/user/${SYSTEMD_UNIT}"
(
set -x
systemctl --user stop "${SYSTEMD_UNIT}"
) || :
(
set -x
systemctl --user disable "${SYSTEMD_UNIT}"
) || :
rm -f "${unit_file}"
INFO "Uninstalled ${SYSTEMD_UNIT}"
fi
if cli_ctx_exists "${CLI_CONTEXT}"; then
cli_ctx_rm "${CLI_CONTEXT}"
INFO "Deleted CLI context \"${CLI_CONTEXT}\""
fi
unset DOCKER_HOST
unset DOCKER_CONTEXT
cli_ctx_use "default"
INFO 'Configured CLI to use the "default" context.'
INFO
INFO 'Make sure to unset or update the environment PATH, DOCKER_HOST, and DOCKER_CONTEXT environment variables if you have added them to `~/.bashrc`.'
INFO "This uninstallation tool does NOT remove Docker binaries and data."
INFO "To remove data, run: \`$BIN/rootlesskit rm -rf $HOME/.local/share/docker\`"
}
# text for --help
usage() {
echo "Usage: ${ARG0} [OPTIONS] COMMAND"
echo
echo "A setup tool for Rootless Docker (${DOCKERD_ROOTLESS_SH})."
echo
echo "Documentation: https://docs.docker.com/go/rootless/"
echo
echo "Options:"
echo " -f, --force Ignore rootful Docker (/var/run/docker.sock)"
echo " --skip-iptables Ignore missing iptables"
echo
echo "Commands:"
echo " check Check prerequisites"
echo " nsenter Enter into RootlessKit namespaces (mostly for debugging)"
echo " install Install systemd unit (if systemd is available) and show how to manage the service"
echo " uninstall Uninstall systemd unit"
}
# parse CLI args
if ! args="$(getopt -o hf --long help,force,skip-iptables -n "$ARG0" -- "$@")"; then
usage
exit 1
fi
eval set -- "$args"
while [ "$#" -gt 0 ]; do
arg="$1"
shift
case "$arg" in
-h | --help)
usage
exit 0
;;
-f | --force)
OPT_FORCE=1
;;
--skip-iptables)
OPT_SKIP_IPTABLES=1
;;
--)
break
;;
*)
# XXX this means we missed something in our "getopt" arguments above!
ERROR "Scripting error, unknown argument '$arg' when parsing script arguments."
exit 1
;;
esac
done
command="${1:-}"
if [ -z "$command" ]; then
ERROR "No command was specified. Run with --help to see the usage. Maybe you want to run \`$ARG0 install\`?"
exit 1
fi
if ! command -v "cmd_entrypoint_${command}" > /dev/null 2>&1; then
ERROR "Unknown command: ${command}. Run with --help to see the usage."
exit 1
fi
# main
"cmd_entrypoint_${command}" "$@"