diff --git a/bin/docker-compose b/bin/docker-compose index 4c9951b5..d413494e 100755 --- a/bin/docker-compose +++ b/bin/docker-compose @@ -31,6 +31,7 @@ function build_environment() { if [[ "$SIBLING_CONTAINERS_ENABLED" == "true" ]]; then if [[ $SERVER_PRO == "true" ]]; then set_sibling_containers_vars + set_selinux_vars else if [[ ${SKIP_WARNINGS:-null} != "true" ]]; then echo "WARNING: SIBLING_CONTAINERS_ENABLED=true is not supported in Overleaf Community Edition." >&2 @@ -152,6 +153,23 @@ function set_sibling_containers_vars() { export OVERLEAF_DATA_PATH } +# Set environment variables for docker-compose.selinux.yml +function set_selinux_vars() { + check_selinux_config + + if [[ -n "$SELINUX_CONFIG_ERROR" ]]; then + echo "ERROR: $SELINUX_CONFIG_ERROR" >&2 + exit 1 + fi + + if [[ -z "$SELINUX_SECURITY_OPT" ]]; then + return + fi + + DOCKER_COMPOSE_FLAGS+=(-f "$TOOLKIT_ROOT/lib/docker-compose.selinux.yml") + export SELINUX_SECURITY_OPT +} + # Set environment variables for docker-compose.logging.yml function set_logging_vars() { DOCKER_COMPOSE_FLAGS+=(-f "$TOOLKIT_ROOT/lib/docker-compose.logging.yml") diff --git a/bin/doctor b/bin/doctor index 00e6693c..6aa792b7 100755 --- a/bin/doctor +++ b/bin/doctor @@ -138,29 +138,168 @@ function check_dependencies() { } function check_docker_daemon() { - print_point 0 "Docker Daemon" - if docker ps &>/dev/null; then + print_point 0 "Container Daemon" + + local using_podman=false + if is_podman; then + using_podman=true + fi + + if docker ps &>/dev/null; then print_point 1 "status: up" + print_point 1 "runtime: $([[ "$using_podman" == true ]] && echo "podman" || echo "docker")" - local docker_server_version=$(docker version -f '{{.Server.Version}}') + local docker_server_version + docker_server_version=$(docker version -f '{{.Server.Version}}' 2>/dev/null || echo "unknown") print_point 1 "server version: $docker_server_version" - if [[ "$docker_server_version" =~ ^([0-9]+)\.([0-9]+) ]]; then - local major="${BASH_REMATCH[1]}" - local minor="${BASH_REMATCH[2]}" - if [[ "$major" -lt 23 ]]; then - add_warning "Docker v$major.$minor has reached its End Of Life. We recommend upgrading to a supported version." + + if [[ "$using_podman" == false ]]; then + if [[ "$docker_server_version" =~ ^([0-9]+)\.([0-9]+) ]]; then + local major="${BASH_REMATCH[1]}" + local minor="${BASH_REMATCH[2]}" + if [[ "$major" -ne 25 ]] && [[ "$major" -lt 29 ]]; then + add_warning "Docker v$major.$minor has reached its End Of Life. We recommend upgrading to a supported version." + fi + else + add_warning "Docker server version unknown ($docker_server_version)" fi - else - add_warning "Docker server version unknown ($docker_server_version)" fi - if docker info | grep -q -e '/var/snap/docker/common/var-lib-docker'; then + if docker info 2>/dev/null | grep -q -e '/var/snap/docker/common/var-lib-docker'; then add_warning "Installing Docker via snap is not supported. The sandboxed compiles feature may not be available. Please follow the steps for installing Docker CE on https://docs.docker.com/engine/install/." fi else print_point 1 "status: DOWN !" add_warning "Docker daemon is not running" fi + + local socket_path="" + + if [[ -f "$TOOLKIT_ROOT/config/overleaf.rc" ]]; then + # shellcheck disable=SC1090 + source "$TOOLKIT_ROOT/config/overleaf.rc" + fi + + resolve_socket_path + socket_path="$RESOLVED_SOCKET_PATH" + + if [[ -n "$socket_path" ]]; then + print_point 1 "socket: $socket_path" + else + print_point 1 "socket: not found" + fi + + if [[ "${SERVER_PRO:-false}" == "true" && "${SIBLING_CONTAINERS_ENABLED:-false}" == "true" ]]; then + if [[ "$using_podman" == true ]]; then + if [[ -n "${DOCKER_HOST:-}" ]]; then + print_point 1 "DOCKER_HOST: $DOCKER_HOST" + elif [[ -f "$TOOLKIT_ROOT/config/overleaf.rc" ]] && grep -q "^DOCKER_HOST=" "$TOOLKIT_ROOT/config/overleaf.rc" 2>/dev/null; then + local rc_docker_host + rc_docker_host=$(grep "^DOCKER_HOST=" "$TOOLKIT_ROOT/config/overleaf.rc" | head -1 | sed 's/^DOCKER_HOST=//' | sed 's/["'"'"']//g') + print_point 1 "DOCKER_HOST: $rc_docker_host" + else + print_point 1 "DOCKER_HOST: not set" + add_warning "DOCKER_HOST not set — ./bin/up will look for the default Docker socket" + fi + + if [[ -f "$TOOLKIT_ROOT/config/variables.env" ]] && grep -q "DOCKER_HOST" "$TOOLKIT_ROOT/config/variables.env" 2>/dev/null; then + add_warning "DOCKER_HOST found in variables.env — it needs to be set in overleaf.rc instead" + fi + fi + + if [[ -n "$socket_path" ]] && [[ ! -S "$socket_path" ]]; then + add_warning "Configured socket path '$socket_path' does not exist or is not a socket" + elif [[ -S "$socket_path" ]]; then + test_socket_ping_host "$socket_path" + if [[ "$SOCKET_PING_HOST_OK" == true ]]; then + print_point 1 "host → socket: OK" + else + print_point 1 "host → socket: FAILED ($SOCKET_PING_HOST_OUTPUT)" + add_warning "Cannot connect to Docker/Podman socket from host" + fi + + test_socket_ping_container + if [[ "$SOCKET_PING_CONTAINER_OK" == true ]]; then + print_point 1 "container → socket: OK" + elif [[ "$SOCKET_PING_CONTAINER_OUTPUT" == "sharelatex not running" ]]; then + print_point 1 "container → socket: skipped (sharelatex not running)" + else + print_point 1 "container → socket: FAILED ($SOCKET_PING_CONTAINER_OUTPUT)" + add_warning "sharelatex container cannot connect to the Docker/Podman socket at /var/run/docker.sock" + fi + fi + fi +} + +function check_security() { + if ! is_podman; then + return + fi + + print_point 0 "Security" + + if [[ -f "$TOOLKIT_ROOT/config/overleaf.rc" ]]; then + # shellcheck disable=SC1090 + source "$TOOLKIT_ROOT/config/overleaf.rc" + fi + + if [[ "${SERVER_PRO:-false}" == "true" && "${SIBLING_CONTAINERS_ENABLED:-false}" == "true" ]]; then + check_selinux_config + print_point 1 "SELinux: $SELINUX_ENFORCE_MODE" + + if [[ -n "$SELINUX_CONFIG_ERROR" ]]; then + add_warning "$SELINUX_CONFIG_ERROR" + elif [[ "$SELINUX_ENFORCE_MODE" != "Disabled" ]]; then + print_point 1 "SELINUX_MODE: $SELINUX_MODE" + print_point 1 "SELINUX_LABEL: ${SELINUX_LABEL:-not set}" + + if [[ -z "$SELINUX_LABEL" ]]; then + print_point 1 "SELinux rules: not required" + else + check_selinux_module + if [[ "$SELINUX_MODULE_LOADED" == true ]]; then + print_point 1 "SELinux module: loaded (podman_socket_clsi)" + else + print_point 1 "SELinux module: not loaded" + fi + + check_selinux_rules "$SELINUX_LABEL" + if [[ "$SELINUX_RULES_OK" == true ]]; then + print_point 1 "SELinux rules: present" + else + print_point 1 "SELinux rules: not present" + for rule in "${SELINUX_MISSING_RULES[@]}"; do + print_point 2 "rule $rule not present" + done + if [[ "$SELINUX_MODE" == "custom" ]]; then + add_warning "SELinux is $SELINUX_ENFORCE_MODE but the rules the sharelatex container needs for the type '$SELINUX_LABEL' are not present — sandboxed compiles will not work. Add the missing rules to your own policy module." + else + add_warning "SELinux is $SELINUX_ENFORCE_MODE but the rules the sharelatex container needs are not present — the container will be blocked from connecting to the Podman socket. Run './bin/podman-setup --apply'." + fi + fi + fi + fi + fi + + local seccomp_path + seccomp_path="$(get_seccomp_expected_path)" + check_seccomp_config + + if [[ "$SECCOMP_FILE_EXISTS" == true && "$SECCOMP_ENV_MATCHES" == true ]]; then + print_point 1 "Seccomp profile: present" + else + if [[ "$SECCOMP_FILE_EXISTS" == false ]]; then + print_point 1 "Seccomp profile: MISSING (file not found at $seccomp_path)" + add_warning "Seccomp profile not installed at $seccomp_path" + fi + if [[ -z "$SECCOMP_ENV_VALUE" ]]; then + print_point 1 "SECCOMP_PROFILE: not set" + add_warning "SECCOMP_PROFILE not set in variables.env" + elif [[ "$SECCOMP_ENV_MATCHES" == false ]]; then + print_point 1 "SECCOMP_PROFILE: '$SECCOMP_ENV_VALUE' (expected '$seccomp_path')" + add_warning "SECCOMP_PROFILE should be '$seccomp_path'" + fi + fi } function print_warnings() { @@ -222,18 +361,16 @@ function check_config_files() { add_warning "Detected SIBLING_CONTAINERS_ENABLED=false. When not using Sibling containers, users have full read and write access to the 'sharelatex' container resources (filesystem, network, environment variables) when running LaTeX compiles. Only use this mode in environments where all users are trusted and no isolation of users is required." fi if [[ "${SERVER_PRO:-null}" == "true" ]]; then - local logged_in - logged_in="$(grep -q quay.io ~/.docker/config.json && echo 'true' || echo 'false')" - print_point 3 "logged in to quay.io: $logged_in" - if [[ "${logged_in}" == "false" ]]; then - local warning_message=( - "Server Pro enabled, but not logged in to quay.io repository." - "These credentials are supplied by Overleaf with a Server Pro" - "license. See https://www.overleaf.com/for/enterprises/features" - "for more details about Server Pro, or contact support@overleaf.com" - "if you have any questions." - ) - add_warning "${warning_message[@]}" + check_quay_login + if [[ "$QUAY_LOGIN_STATUS" == true ]]; then + if [[ -n "$QUAY_LOGIN_USER" ]]; then + print_point 3 "logged in to quay.io: true ($QUAY_LOGIN_USER)" + else + print_point 3 "logged in to quay.io: true" + fi + else + print_point 3 "logged in to quay.io: false" + add_warning "Server Pro enabled, but not logged in to quay.io repository. These credentials are supplied by Overleaf with a Server Pro license. See https://www.overleaf.com/for/enterprises/features for more details about Server Pro, or contact support@overleaf.com if you have any questions." fi elif [[ "${SIBLING_CONTAINERS_ENABLED:-null}" == "true" ]]; then add_warning "Sibling containers are not available in Community Edition, which is intended for use in environments where all users are trusted. Community Edition is not appropriate for scenarios where isolation of users is required. Sibling containers are offered as part of our Server Pro offering and you can read more about the differences at https://www.overleaf.com/for/enterprises/features. Set SIBLING_CONTAINERS_ENABLED=false in config/overleaf.rc to continue using insecure in-container compiles." @@ -486,6 +623,7 @@ function __main__() { check_host_information check_dependencies check_docker_daemon + check_security check_config_files print_warnings print_section_separator "End" diff --git a/bin/podman-setup b/bin/podman-setup new file mode 100755 index 00000000..53d1e91c --- /dev/null +++ b/bin/podman-setup @@ -0,0 +1,726 @@ +#! /usr/bin/env bash +# shellcheck source-path=.. +# Overleaf Server Pro -- Podman Setup +# Validates and auto-fixes a RHEL-based system for use with Overleaf Server Pro +# rootless Podman, sibling containers and SELinux enforcing. +# +# Part of the overleaf-toolkit. Invoked via: +# ./bin/podman-setup [--apply] +# Exit codes: 0 = all clear, 1 = warnings, 2 = failures + +set -euo pipefail + +#### Detect Toolkit Project Root #### +command -v realpath >/dev/null 2>&1 || realpath() { + [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" +} +SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")" +TOOLKIT_ROOT="$(realpath "$(dirname "$SCRIPT_PATH")/..")" + +if [[ ! -d "$TOOLKIT_ROOT/bin" ]] || [[ ! -d "$TOOLKIT_ROOT/config" ]]; then + echo "ERROR: could not find root of overleaf-toolkit project" + exit 1 +fi + +if [[ ! -f "$TOOLKIT_ROOT/config/overleaf.rc" ]] || [[ ! -f "$TOOLKIT_ROOT/config/variables.env" ]] || [[ ! -f "$TOOLKIT_ROOT/config/version" ]]; then + echo "ERROR: Toolkit not initialized. Run 'bin/init' first." + exit 1 +fi + +source "$TOOLKIT_ROOT/lib/shared-functions.sh" + +DRY_RUN=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --apply) DRY_RUN=false; shift ;; + help|--help) + cat <<'HELP' +Usage: bin/podman-setup [OPTIONS] + +Validates a RHEL-based system for Overleaf Server Pro +with rootless Podman, sibling containers and SELinux enforcing. + +Requires an initialised toolkit (bin/init) with SERVER_PRO=true +set in config/overleaf.rc. + +By default, this script checks for issues without fixing them. +Use --apply to automatically fix detected issues. + +Options: + --apply Fix detected issues (default: dry-run only) + --help Show this help and exit +HELP + exit + ;; + *) echo "Usage: $0 [--apply]" >&2; exit 2 ;; + esac +done + +validate_not_root() { + [[ "$EUID" -ne 0 ]] && return 0 + + local args="" + [[ "$DRY_RUN" == false ]] && args=" --apply" + + echo "ERROR: do not run this script as root or with sudo." + echo " Run it as the user that will own the rootless Podman setup:" + echo " ./bin/podman-setup${args}" + echo " It will prompt for a sudo password when a step needs one." + exit 1 +} + +validate_not_root + +validate_os() { + [[ -f /etc/os-release ]] || { echo "ERROR: /etc/os-release missing"; exit 1; } + + local id version_id + # shellcheck disable=SC1091 + read -r id version_id < <(. /etc/os-release && echo "${ID} ${VERSION_ID%%.*}") + + case "$id" in + centos|rhel|rocky|almalinux) + [[ "$version_id" -ge 9 ]] && return 0 + ;; + esac + echo "ERROR: unsupported OS '${id} ${version_id}'. RHEL 9+ required." + exit 1 +} + +validate_os + +# Sandboxed Compiles are a Server Pro only feature. With SERVER_PRO=false the image +# name resolves to Community Edition, which includes neither the seccomp profile nor sandboxed compiles. +validate_server_pro() { + [[ "$SERVER_PRO" == "true" ]] && return 0 + + echo "ERROR: SERVER_PRO=true is required in config/overleaf.rc (found '$SERVER_PRO')." + echo " This script is for Server Pro setups." + echo " See https://www.overleaf.com/for/enterprises/features for more details about Server Pro and how to buy a license." + echo " Or, if you already have a license, contact support+serverpro@overleaf.com if you need assistance." + exit 1 +} + +read_image_version +read_config +validate_server_pro + +set_server_pro_image_name "$IMAGE_VERSION" + +USER_NAME="$(id -un)" +USER_UID="$(id -u)" + +declare -i CHECK_PASS=0 CHECK_WARN=0 CHECK_FAIL=0 +SUMMARY="" + +REQUIRED_PKGS=(podman podman-docker git curl checkpolicy policycoreutils) +SECCOMP_IMAGE_PATH=/overleaf/services/clsi/seccomp/clsi-profile.json + +report() { + local level="$1" msg="$2" + case "$level" in + ok) CHECK_PASS+=1; printf "[✓] %s\n" "$msg" ;; + warn) CHECK_WARN+=1; SUMMARY+="[!] $msg"$'\n'; printf "[!] %s\n" "$msg" >&2 ;; + fail) CHECK_FAIL+=1; SUMMARY+="[✗] $msg"$'\n'; printf "[✗] %s\n" "$msg" >&2 ;; + fix) printf "[*] %s\n" "$msg" ;; + skip) printf "[-] %s\n" "$msg" ;; + esac +} + +heading() { printf "\n====== %s ======\n" "$1"; } + +is_dry_run() { [ "$DRY_RUN" = true ]; } +is_apply() { [ "$DRY_RUN" = false ]; } + +run_sudo() { + if sudo -n true 2>/dev/null; then + sudo "$@" + elif is_dry_run; then + report warn "Cannot run 'sudo $*' (password required, use --apply)" + return 1 + else + sudo "$@" + fi +} + +get_podman_socket_path() { + echo "/run/user/${USER_UID}/podman/podman.sock" +} + +ensure_lingering() { + heading "Lingering" + + if loginctl show-user "$USER_NAME" -p Linger --value 2>/dev/null | grep -q yes; then + report ok "lingering enabled for $USER_NAME" + return 0 + fi + + if is_dry_run; then + report warn "lingering not enabled for $USER_NAME" + return 1 + fi + + if run_sudo loginctl enable-linger "$USER_NAME"; then + report fix "lingering enabled for $USER_NAME" + else + report fail "failed to enable lingering for $USER_NAME" + return 1 + fi +} + +ensure_packages() { + heading "Packages" + local missing=() + + for pkg in "${REQUIRED_PKGS[@]}"; do + if rpm -q "$pkg" &>/dev/null; then + report ok "$pkg installed" + else + report fail "$pkg not installed" + missing+=("$pkg") + fi + done + + if is_apply && [ ${#missing[@]} -gt 0 ]; then + if run_sudo dnf install -y "${missing[@]}"; then + report fix "installed ${missing[*]}" + else + report fail "failed to install ${missing[*]}" + fi + fi +} + +ensure_podman_socket() { + heading "Services" + + if systemctl --user is-active podman.socket &>/dev/null; then + report ok "podman.socket active" + return 0 + fi + + if is_dry_run; then + report fail "podman.socket inactive" + return 1 + fi + + if systemctl --user enable --now podman.socket; then + report fix "enabled podman.socket" + else + report fail "failed to enable podman.socket" + fi +} + + +read_config_var() { + local var_name="$1" file="$2" + grep "^${var_name}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" | tr -d ' ' || true +} + +set_config_var() { + local var_name="$1" expected="$2" file="$3" + if grep -q "^${var_name}=" "$file" 2>/dev/null; then + sed -i "s|^${var_name}=.*|${var_name}=${expected}|" "$file" + else + echo "${var_name}=${expected}" >> "$file" + fi +} + +remove_config_var() { + local var_name="$1" file="$2" + sed -i "/^${var_name}=/d" "$file" 2>/dev/null || true +} + +ensure_config_var() { + local var_name="$1" expected="$2" rc_file="$3" wrong_file="${4:-}" + + local current + current="$(read_config_var "$var_name" "$rc_file")" + + if [ "$current" = "$expected" ]; then + report ok "${var_name} correct" + elif [ -n "$current" ]; then + if is_dry_run; then + report warn "${var_name} is '$current', expected '$expected'" + else + set_config_var "$var_name" "$expected" "$rc_file" + report fix "${var_name} corrected" + fi + else + if is_dry_run; then + report warn "${var_name} not set" + else + set_config_var "$var_name" "$expected" "$rc_file" + report fix "added ${var_name}" + fi + fi + + if [ -n "$wrong_file" ] && grep -q "^${var_name}=" "$wrong_file" 2>/dev/null; then + if is_dry_run; then + report fail "${var_name} must not be in $(basename "$wrong_file")" + else + remove_config_var "$var_name" "$wrong_file" + report fix "removed ${var_name} from $(basename "$wrong_file")" + fi + fi +} + +ensure_docker_host() { + local sock_path expected + sock_path="$(get_podman_socket_path)" + expected="unix://${sock_path}" + ensure_config_var "DOCKER_HOST" "$expected" \ + "$TOOLKIT_ROOT/config/overleaf.rc" "$TOOLKIT_ROOT/config/variables.env" +} + +ensure_docker_socket() { + local sock_path + sock_path="$(get_podman_socket_path)" + ensure_config_var "DOCKER_SOCKET_PATH" "$sock_path" \ + "$TOOLKIT_ROOT/config/overleaf.rc" "$TOOLKIT_ROOT/config/variables.env" +} + +ensure_docker_compose() { + local compose_bin="$HOME/.local/bin/docker-compose" + local compose_plugin="$HOME/.docker/cli-plugins/docker-compose" + + if [ -x "$compose_bin" ]; then + local version + version="$($compose_bin version 2>/dev/null | head -1 || echo "installed")" + report ok "Docker Compose: $version" + elif is_dry_run; then + report fail "Docker Compose not installed" + return 1 + else + mkdir -p "$HOME/.local/bin" "$HOME/.docker/cli-plugins" + + local base_url="https://github.com/docker/compose/releases/latest/download" + local binary="docker-compose-$(uname -s)-$(uname -m)" + local tmp_bin tmp_sum rc=0 + tmp_bin="$(mktemp)" + tmp_sum="$(mktemp)" + + if ! curl -sL "${base_url}/${binary}" -o "$tmp_bin"; then + report fail "failed to download compose" + rc=1 + elif ! curl -sL "${base_url}/${binary}.sha256" -o "$tmp_sum"; then + report fail "failed to download compose checksum" + rc=1 + elif ! (cd "$(dirname "$tmp_bin")" && sed "s|[^ ]*$|$(basename "$tmp_bin")|" "$tmp_sum" | sha256sum --check --status); then + report fail "compose checksum mismatch — binary discarded" + rc=1 + else + install -m 0755 "$tmp_bin" "$compose_bin" + report fix "installed compose (checksum verified)" + fi + + rm -f "$tmp_bin" "$tmp_sum" + [ "$rc" -eq 0 ] || return "$rc" + fi + + if [ -L "$compose_plugin" ]; then + report ok "Compose plugin symlink present" + elif is_dry_run; then + report warn "Compose plugin symlink not present" + else + mkdir -p "$HOME/.docker/cli-plugins" + ln -sf "$compose_bin" "$compose_plugin" + report fix "linked compose plugin" + fi +} + +ensure_containers_config() { + local conf="$HOME/.config/containers/containers.conf" + + if grep -q "compose_providers" "$conf" 2>/dev/null; then + report ok "compose provider configured" + return 0 + fi + + if is_dry_run; then + report warn "compose provider not configured" + return 1 + fi + + local compose_plugin="$HOME/.docker/cli-plugins/docker-compose" + mkdir -p "$(dirname "$conf")" + + [ -f "$conf" ] && echo >> "$conf" + cat >> "$conf" << EOF +[engine] +compose_warning_logs = false +compose_providers = ["$compose_plugin"] +EOF + report fix "configured containers.conf" +} + +ensure_nodocker_file() { + if [ -f /etc/containers/nodocker ]; then + report ok "nodocker file present" + elif is_dry_run; then + report warn "nodocker file not present" + else + if run_sudo mkdir -p /etc/containers && run_sudo touch /etc/containers/nodocker; then + report fix "created nodocker file" + else + report fail "failed to create nodocker file" + fi + fi +} + +ensure_registries_config() { + local reg_conf="$HOME/.config/containers/registries.conf.d/overleaf-toolkit.conf" + if [ -f "$reg_conf" ] && grep -qF '# overleaf-toolkit' "$reg_conf" 2>/dev/null; then + report ok "registries.conf.d drop-in configured" + elif is_dry_run; then + report warn "registries.conf.d drop-in not configured" + else + mkdir -p "$(dirname "$reg_conf")" + cat > "$reg_conf" << EOF +# overleaf-toolkit managed configuration +[aliases] +"mongo" = "docker.io/library/mongo" +"redis" = "docker.io/library/redis" +EOF + report fix "configured registries.conf.d drop-in" + fi +} + +ensure_container_runtime() { + heading "Container Runtime" + + ensure_nodocker_file + ensure_registries_config + ensure_docker_compose + ensure_containers_config + ensure_docker_host + ensure_docker_socket +} + +ensure_registry_auth() { + heading "Registry Auth" + # shellcheck disable=SC2153 # QUAY_LOGIN_STATUS set by lib/shared-functions.sh + check_quay_login + + if [[ "$QUAY_LOGIN_STATUS" == true ]]; then + report ok "logged in to quay.io" + return 0 + fi + + report warn "not logged in to quay.io" + is_apply && printf "\n Run 'podman login quay.io' to authenticate.\n\n" +} + +check_loaded_selinux_policy() { + # shellcheck disable=SC2153 # SELINUX_* set by lib/shared-functions.sh + check_selinux_module + + if [[ "$SELINUX_MODULE_LOADED" == true ]]; then + report ok "SELinux module loaded (podman_socket_clsi)" + else + report skip "SELinux module not loaded" + fi + + check_selinux_rules "$SELINUX_LABEL" + + if [[ "$SELINUX_RULES_OK" == true ]]; then + report ok "SELinux rules present" + return 0 + fi + + report skip "SELinux rules not present" + for rule in "${SELINUX_MISSING_RULES[@]}"; do + report skip "rule $rule not present" + done + return 1 +} + +ensure_selinux_policy() { + heading "SELinux Policy" + + # shellcheck disable=SC2153 # SELINUX_* set by lib/shared-functions.sh + check_selinux_config + + if [[ -n "$SELINUX_CONFIG_ERROR" ]]; then + report fail "$SELINUX_CONFIG_ERROR" + return 1 + fi + + if [[ "$SELINUX_ENFORCE_MODE" == "Disabled" ]]; then + report skip "SELinux: Disabled" + return 0 + fi + + report ok "SELinux: $SELINUX_ENFORCE_MODE" + report ok "SELINUX_MODE: $SELINUX_MODE" + report ok "SELINUX_LABEL: ${SELINUX_LABEL:-not set}" + + if [[ -z "$SELINUX_LABEL" ]]; then + report skip "SELinux rules not required" + return 0 + fi + + check_loaded_selinux_policy && return 0 + + if [[ "$SELINUX_MODE" == "custom" ]]; then + report warn "SELinux rules not present for '$SELINUX_LABEL' (add them to your own policy module)" + return 0 + fi + + if is_dry_run; then + report warn "SELinux rules not present (use --apply to install the policy module)" + return 0 + fi + + local dir="$TOOLKIT_ROOT/config/selinux" + mkdir -p "$dir" + + cp "$TOOLKIT_ROOT/lib/podman_socket_clsi.te" "$dir/podman_socket_clsi.te" || { report fail "failed to copy SELinux policy"; return 1; } + + if checkmodule -M -m -o "$dir/podman_socket_clsi.mod" "$dir/podman_socket_clsi.te" && \ + semodule_package -o "$dir/podman_socket_clsi.pp" -m "$dir/podman_socket_clsi.mod" && \ + run_sudo semodule -i "$dir/podman_socket_clsi.pp" 2>/dev/null; then + report fix "compiled and loaded SELinux module" + else + report fail "failed to compile or load SELinux module" + return 1 + fi +} + +ensure_socket_connectivity() { + heading "Socket Connectivity" + local sock_path + sock_path="$(get_podman_socket_path)" + + # shellcheck disable=SC2153 # SOCKET_PING_* set by lib/shared-functions.sh + test_socket_ping_host "$sock_path" + if [[ "$SOCKET_PING_HOST_OK" == true ]]; then + report ok "host → socket OK" + elif [[ "$SOCKET_PING_HOST_OUTPUT" == "socket not found" ]]; then + report warn "host → socket: socket not found, is podman.socket running?" + else + report fail "host → socket failed ($SOCKET_PING_HOST_OUTPUT)" + fi + + test_socket_ping_container + if [[ "$SOCKET_PING_CONTAINER_OK" == true ]]; then + report ok "container → socket OK" + elif [[ "$SOCKET_PING_CONTAINER_OUTPUT" == "sharelatex not running" ]]; then + report warn "container → socket skipped (sharelatex not running)" + else + report fail "container → socket failed ($SOCKET_PING_CONTAINER_OUTPUT)" + fi +} + +ensure_image_pulled() { + if podman image exists "$IMAGE"; then + report ok "image $IMAGE pulled" + return 0 + fi + + if is_dry_run; then + report warn "image $IMAGE not pulled" + return 1 + fi + + if podman pull "$IMAGE"; then + report fix "pulled $IMAGE" + else + report fail "failed to pull $IMAGE" + return 1 + fi +} + +extract_seccomp_profile() { + local dest="$1" + local cid rc=0 + + if ! cid="$(podman create "$IMAGE" 2>/dev/null)"; then + report fail "failed to create container from $IMAGE" + return 1 + fi + + mkdir -p "$(dirname "$dest")" + if podman cp "$cid:$SECCOMP_IMAGE_PATH" "$dest" 2>/dev/null; then + report fix "extracted seccomp profile from $IMAGE" + else + report fail "seccomp profile not found at $SECCOMP_IMAGE_PATH in $IMAGE" + rc=1 + fi + + podman rm -f "$cid" >/dev/null 2>&1 || true + return "$rc" +} + +ensure_seccomp() { + heading "Seccomp" + local seccomp_path + seccomp_path="$(get_seccomp_expected_path)" + + if [ -f "$seccomp_path" ]; then + report ok "Seccomp profile present for $IMAGE_VERSION" + elif is_dry_run; then + report warn "Seccomp profile not present for $IMAGE_VERSION" + ensure_image_pulled || true + elif ensure_image_pulled; then + extract_seccomp_profile "$seccomp_path" || true + fi + + # shellcheck disable=SC2153 # SECCOMP_ENV_* set by lib/shared-functions.sh + check_seccomp_config + if [[ "$SECCOMP_ENV_MATCHES" == true ]]; then + report ok "SECCOMP_PROFILE correct" + elif is_dry_run; then + if [[ -n "$SECCOMP_ENV_VALUE" ]]; then + report warn "SECCOMP_PROFILE is '$SECCOMP_ENV_VALUE', expected '$seccomp_path'" + else + report warn "SECCOMP_PROFILE not set" + fi + else + set_config_var "SECCOMP_PROFILE" "$seccomp_path" "$TOOLKIT_ROOT/config/variables.env" + report fix "SECCOMP_PROFILE set" + fi +} + +ensure_data_permissions() { + heading "Data Directory Permissions" + + local mongo_data + mongo_data="$(read_configuration "MONGO_DATA_PATH")" + mongo_data="${mongo_data:-data/mongo}" + local redis_data + redis_data="$(read_configuration "REDIS_DATA_PATH")" + redis_data="${redis_data:-data/redis}" + local overleaf_data + overleaf_data="$(read_configuration "OVERLEAF_DATA_PATH")" + overleaf_data="${overleaf_data:-data/sharelatex}" + + local entries=("mongo:${mongo_data}:999" "redis:${redis_data}:999" "overleaf:${overleaf_data}:33") + + if [[ "${GIT_BRIDGE_ENABLED:-false}" == "true" ]]; then + local git_bridge_data + git_bridge_data="$(read_configuration "GIT_BRIDGE_DATA_PATH")" + git_bridge_data="${git_bridge_data:-data/git-bridge}" + entries+=("git-bridge:${git_bridge_data}:1000") + fi + + for entry in "${entries[@]}"; do + IFS=':' read -r name dir uid <<< "$entry" + local full_path="$TOOLKIT_ROOT/$dir" + + if [ ! -d "$full_path" ]; then + report skip "$name data dir not found" + continue + fi + + local actual_uid + actual_uid="$(podman unshare stat -c %u "$full_path" 2>/dev/null || echo "")" + + if [ "$actual_uid" = "$uid" ]; then + report ok "$name data dir owned by $uid" + elif is_dry_run; then + report fail "$name data dir owned by '${actual_uid:-unknown}', expected $uid" + else + if podman unshare chown -R "${uid}:${uid}" "$full_path" 2>/dev/null; then + report fix "$name data dir ownership corrected" + else + report fail "failed to correct $name data dir ownership" + fi + fi + done +} + +reset_results() { + CHECK_PASS=0; CHECK_WARN=0; CHECK_FAIL=0; SUMMARY="" +} + +print_result_counts() { + heading "Summary" + printf "[✓] %d passed [!] %d warnings [✗] %d failures\n" "$CHECK_PASS" "$CHECK_WARN" "$CHECK_FAIL" +} + +run_check() { + local fn="$1" + local pass_before=$CHECK_PASS warn_before=$CHECK_WARN fail_before=$CHECK_FAIL + local rc=0 + "$fn" || rc=$? + if [ "$rc" -ne 0 ] \ + && [ "$CHECK_PASS" -eq "$pass_before" ] \ + && [ "$CHECK_WARN" -eq "$warn_before" ] \ + && [ "$CHECK_FAIL" -eq "$fail_before" ]; then + report fail "$fn aborted (exit $rc, no status reported)" + fi + return 0 +} + +run_common_checks() { + local include_socket_check="${1:-false}" + + run_check ensure_lingering + run_check ensure_packages + run_check ensure_podman_socket + run_check ensure_container_runtime + run_check ensure_registry_auth + run_check ensure_selinux_policy + if [ "$include_socket_check" = true ]; then + run_check ensure_socket_connectivity + fi + run_check ensure_data_permissions + run_check ensure_seccomp +} + +run_checks() { + reset_results + run_common_checks true + + print_result_counts +} + +run_fixes() { + reset_results + run_common_checks false + print_result_counts +} + +is_failure() { [ "$CHECK_FAIL" -gt 0 ]; } +is_warn() { [ "$CHECK_WARN" -gt 0 ]; } + +print_summary() { + local label="${1:-}" + if is_failure; then + printf "[✗] Failures%s:\n%s\n" "$label" "$(printf '%s' "$SUMMARY" | grep '^\[✗\]')" + fi + if is_warn; then + printf "[!] Warnings%s:\n%s\n" "$label" "$(printf '%s' "$SUMMARY" | grep '^\[!\]')" + fi +} + +echo "=== Podman Setup ===" +echo +echo "--------------------------- WARNING ----------------------------" +echo " Podman support is experimental and comes with limited support. " +echo " It is only tested on RHEL 9+ with rootless Podman, Server Pro " +echo " sibling containers and SELinux enforcing. " +echo "--------------------------- WARNING ----------------------------" + +run_checks +print_summary + +rc=0 +is_failure && rc=2 +[ "$rc" -eq 0 ] && is_warn && rc=1 + +if is_apply && [ "$rc" -ne 0 ]; then + printf "\n====== Applying fixes ======\n" + sudo -v || { echo "ERROR: sudo authentication required for --apply" >&2; exit 2; } + run_fixes + printf "\n====== Re-checking ======\n" + run_checks + print_summary " remaining" + rc=0 + is_failure && rc=2 + [ "$rc" -eq 0 ] && is_warn && rc=1 +fi + +[ "$rc" -eq 0 ] && echo "All checks passed!" +exit "$rc" diff --git a/bin/up b/bin/up index 087e1d90..90fee442 100755 --- a/bin/up +++ b/bin/up @@ -76,6 +76,37 @@ function pull_sandboxed_compiles() { done } +function notify_about_podman_limited_support() { + if [[ $SERVER_PRO != "true" ]] || [[ "$SIBLING_CONTAINERS_ENABLED" != "true" ]] || ! is_podman; then + return + fi + echo + echo '---' + echo ' WARNING: Podman support is experimental and comes with limited support. Running Server Pro with sibling containers on Podman is only tested on RHEL 9+ with rootless Podman and SELinux enforcing. Run "bin/podman-setup" to validate the host configuration, and "bin/doctor" to check a running setup.' + echo '---' + echo +} + +function notify_about_missing_selinux_rules() { + check_selinux_config + if [[ -z "$SELINUX_LABEL" ]] || [[ -z "$SELINUX_SECURITY_OPT" ]]; then + return + fi + + check_selinux_rules "$SELINUX_LABEL" + if [[ "$SELINUX_RULES_OK" == "true" ]]; then + return + fi + + local missing + missing=$(printf '%s, ' "${SELINUX_MISSING_RULES[@]}") + + echo '---' + echo " WARNING: The SELinux policy module for the type '$SELINUX_LABEL' is missing rules the sharelatex container needs to reach the Podman socket, so sandboxed compiles will not work. Missing rules: ${missing%, }. With SELINUX_MODE=managed, run \"bin/podman-setup --apply\" to install the Overleaf policy module; with SELINUX_MODE=custom, add these rules to your own policy module." + echo '---' + echo +} + function notify_about_not_using_detach_mode() { local detached=false for arg in "$@"; do @@ -115,6 +146,8 @@ function __main__() { pull_sandboxed_compiles fi + notify_about_podman_limited_support + notify_about_missing_selinux_rules notify_about_not_using_detach_mode "$@" exec "$TOOLKIT_ROOT/bin/docker-compose" up "$@" } diff --git a/doc/overleaf-rc.md b/doc/overleaf-rc.md index f87c2f65..5552f726 100644 --- a/doc/overleaf-rc.md +++ b/doc/overleaf-rc.md @@ -80,6 +80,27 @@ Requires `SIBLING_CONTAINERS_ENABLED=true` - Default: /var/run/docker.sock +### `SELINUX_MODE` + +Controls the SELinux options applied to the `sharelatex` container: + +- `managed`: apply the label of the SELinux policy module installed by `bin/podman-setup --apply` (`sharelatex_t`). `SELINUX_LABEL` is ignored. +- `custom`: apply the label set in `SELINUX_LABEL`. +- `disable`: disable SELinux confinement for the `sharelatex` container. +- `none`: do not set any SELinux options on the `sharelatex` container. + +Only applies when running Server Pro with sibling containers on Podman, with SELinux enabled. + +- Default: disable + +### `SELINUX_LABEL` + +Sets the SELinux type applied to the `sharelatex` container, for example `sharelatex_t`. The type must exist in the loaded policy, otherwise the sharelatex container will not start. If it is missing any of the rules the sharelatex container needs to reach the Podman socket, `bin/up` warns that sandboxed compiles will not work; `bin/doctor` and `bin/podman-setup` list the missing rules. + +Requires `SELINUX_MODE=custom` + +- Default: unset + ### `GIT_BRIDGE_ENABLED` Set to `true` to enable the git-bridge feature (Server Pro only). diff --git a/doc/quick-start-guide.md b/doc/quick-start-guide.md index 61a14965..d8e80d8a 100644 --- a/doc/quick-start-guide.md +++ b/doc/quick-start-guide.md @@ -169,7 +169,7 @@ We should see some output similar to this: - version info: 5.0.17(1)-release - docker - status: present - - version info: Docker version 23.06.6, build 369ce74a3c + - version info: Docker version 29.7.2, build a7dcaa6 - docker compose - status: present - version info: docker compose version v2.17.3 diff --git a/doc/the-doctor.md b/doc/the-doctor.md index 1b6abecb..36d23117 100644 --- a/doc/the-doctor.md +++ b/doc/the-doctor.md @@ -38,7 +38,7 @@ You will see some output like this: - version info: 5.0.17(1)-release - docker - status: present - - version info: Docker version 23.0.6, build 369ce74a3c + - version info: Docker version 29.7.2, build a7dcaa6 - docker compose - status: present - version info: Docker Compose version v2.17.3 @@ -51,8 +51,11 @@ You will see some output like this: - awk - status: present - version info: GNU Awk 5.0.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.2.0) -- Docker Daemon +- Container Daemon - status: up + - runtime: docker + - server version: 29.7.2 + - socket: /var/run/docker.sock ====== Configuration ====== - config/version - status: present @@ -85,7 +88,7 @@ If the tool is present on the system, it will be listed as `status: present`, al ``` - docker - status: present - - version info: Docker version 19.03.6, build 369ce74a3c + - version info: Docker version 29.7.2, build a7dcaa6 ``` However, if the tool is missing, it will be listed as `status: MISSING!`, and a warning will be added to the bottom of the `doctor` output. For example: diff --git a/lib/default.rc b/lib/default.rc index a28489bf..2c47c189 100644 --- a/lib/default.rc +++ b/lib/default.rc @@ -9,6 +9,14 @@ OVERLEAF_PORT=80 SIBLING_CONTAINERS_ENABLED=false DOCKER_SOCKET_PATH=/var/run/docker.sock +# SELinux (Podman, Server Pro sibling containers only) +# managed - apply the toolkit managed label (sharelatex_t), ignoring SELINUX_LABEL +# custom - apply the label set in SELINUX_LABEL +# disable - disable SELinux confinement for the sharelatex container +# none - do not set any SELinux options on the sharelatex container +SELINUX_MODE=disable +SELINUX_LABEL= + # Mongo configuration MONGO_ENABLED=true MONGO_IMAGE=mongo:4.0 diff --git a/lib/docker-compose.base.yml b/lib/docker-compose.base.yml index 4e061bc8..885e5ea4 100644 --- a/lib/docker-compose.base.yml +++ b/lib/docker-compose.base.yml @@ -6,7 +6,7 @@ services: image: "${IMAGE}" container_name: sharelatex volumes: - - "${OVERLEAF_DATA_PATH}:${OVERLEAF_IN_CONTAINER_DATA_PATH}" + - "${OVERLEAF_DATA_PATH}:${OVERLEAF_IN_CONTAINER_DATA_PATH}:z" ports: - "${OVERLEAF_LISTEN_IP:-127.0.0.1}:${OVERLEAF_PORT:-80}:80" environment: diff --git a/lib/docker-compose.git-bridge.yml b/lib/docker-compose.git-bridge.yml index 540cc6fc..56d9a943 100644 --- a/lib/docker-compose.git-bridge.yml +++ b/lib/docker-compose.git-bridge.yml @@ -5,7 +5,7 @@ services: restart: always image: "${GIT_BRIDGE_IMAGE}" volumes: - - "${GIT_BRIDGE_DATA_PATH}:/data/git-bridge" + - "${GIT_BRIDGE_DATA_PATH}:/data/git-bridge:z" container_name: git-bridge expose: - "8000" @@ -19,7 +19,3 @@ services: - ../config/variables.env user: root command: ["/server-pro-start.sh"] - - sharelatex: - links: - - git-bridge diff --git a/lib/docker-compose.mongo.yml b/lib/docker-compose.mongo.yml index 8a2d6dce..403ad89d 100644 --- a/lib/docker-compose.mongo.yml +++ b/lib/docker-compose.mongo.yml @@ -7,7 +7,7 @@ services: command: "${MONGO_ARGS}" container_name: mongo volumes: - - "${MONGO_DATA_PATH}:/data/db" + - "${MONGO_DATA_PATH}:/data/db:z" expose: - 27017 healthcheck: @@ -20,5 +20,4 @@ services: depends_on: mongo: condition: service_healthy - links: - - mongo + diff --git a/lib/docker-compose.redis.yml b/lib/docker-compose.redis.yml index 5c798140..5df805fe 100644 --- a/lib/docker-compose.redis.yml +++ b/lib/docker-compose.redis.yml @@ -5,7 +5,7 @@ services: restart: always image: "${REDIS_IMAGE}" volumes: - - "${REDIS_DATA_PATH}:/data" + - "${REDIS_DATA_PATH}:/data:z" container_name: redis command: ${REDIS_COMMAND} expose: @@ -15,5 +15,4 @@ services: depends_on: redis: condition: service_started - links: - - redis + diff --git a/lib/docker-compose.selinux.yml b/lib/docker-compose.selinux.yml new file mode 100644 index 00000000..89a77f75 --- /dev/null +++ b/lib/docker-compose.selinux.yml @@ -0,0 +1,4 @@ +services: + sharelatex: + security_opt: + - "${SELINUX_SECURITY_OPT}" diff --git a/lib/podman_socket_clsi.te b/lib/podman_socket_clsi.te new file mode 100644 index 00000000..d648a68a --- /dev/null +++ b/lib/podman_socket_clsi.te @@ -0,0 +1,31 @@ +module podman_socket_clsi 2.0; + +# Scopes access to the rootless Podman socket (/run/user/$UID/podman/podman.sock, +# user_tmp_t) to the sharelatex container. +# Applied with 'security_opt: label=type:sharelatex_t'. + +require { + attribute container_domain; + attribute container_net_domain; + attribute domain; + attribute kernel_system_state_reader; + attribute mcs_constrained_type; + attribute process_user_target; + attribute svirt_sandbox_domain; + role system_r; + type container_runtime_t; + type user_tmp_t; + class fifo_file setattr; + class sock_file { write getattr }; + class unix_stream_socket connectto; +} + +type sharelatex_t; +typeattribute sharelatex_t container_domain, container_net_domain, domain, + kernel_system_state_reader, mcs_constrained_type, process_user_target, + svirt_sandbox_domain; +role system_r types sharelatex_t; + +allow sharelatex_t user_tmp_t:sock_file { write getattr }; +allow sharelatex_t container_runtime_t:unix_stream_socket connectto; +allow sharelatex_t container_runtime_t:fifo_file setattr; diff --git a/lib/shared-functions.sh b/lib/shared-functions.sh index a35d17c7..b13c498c 100644 --- a/lib/shared-functions.sh +++ b/lib/shared-functions.sh @@ -225,3 +225,285 @@ function read_configuration() { grep -E "^$name=" "$TOOLKIT_ROOT/config/overleaf.rc" \ | sed -r "s/^$name=([\"']?)(.+)\1\$/\2/" } + +# Returns 0 if podman runtime is available (podman binary or docker shim) +is_podman() { + command -v podman &>/dev/null && return 0 + + if command -v docker &>/dev/null; then + docker version 2>/dev/null | grep -qi podman && return 0 + fi + + return 1 +} + +# Check for quay.io login credentials. +# Sets: QUAY_LOGIN_STATUS ("true" or "false") +# QUAY_LOGIN_USER (username if found, empty otherwise) +check_quay_login() { + QUAY_LOGIN_STATUS=false + QUAY_LOGIN_USER="" + + # Try podman --get-login first + if command -v podman &>/dev/null; then + QUAY_LOGIN_USER=$(podman login quay.io --get-login 2>/dev/null) || true + if [[ -n "$QUAY_LOGIN_USER" ]]; then + QUAY_LOGIN_STATUS=true + return + fi + fi + + # Fall back to checking auth config files + local auth_file + for auth_file in \ + "$HOME/.config/containers/auth.json" \ + "$HOME/.docker/config.json" \ + "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/containers/auth.json"; do + if [[ -f "$auth_file" ]] && grep -q "quay.io" "$auth_file" 2>/dev/null; then + QUAY_LOGIN_STATUS=true + local auth_b64 + auth_b64=$(awk '/"quay.io"/{f=1} f&&/"auth"/{gsub(/.*"auth": *"/,""); gsub(/".*/,""); print; exit}' "$auth_file" 2>/dev/null) || true + if [[ -n "$auth_b64" ]]; then + QUAY_LOGIN_USER=$(printf '%s' "$auth_b64" | base64 -d 2>/dev/null | cut -d: -f1) || true + fi + return + fi + done +} + +resolve_socket_path() { + RESOLVED_SOCKET_PATH="" + + if [[ -f "$TOOLKIT_ROOT/config/overleaf.rc" ]]; then + # shellcheck disable=SC1090 + source "$TOOLKIT_ROOT/config/overleaf.rc" + fi + + if [[ -n "${DOCKER_SOCKET_PATH:-}" ]]; then + RESOLVED_SOCKET_PATH="$DOCKER_SOCKET_PATH" + elif [[ -n "${DOCKER_HOST:-}" ]]; then + RESOLVED_SOCKET_PATH="${DOCKER_HOST#unix://}" + elif [[ -S "/var/run/docker.sock" ]]; then + RESOLVED_SOCKET_PATH="/var/run/docker.sock" + else + local runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + if [[ -S "$runtime_dir/podman/podman.sock" ]]; then + RESOLVED_SOCKET_PATH="$runtime_dir/podman/podman.sock" + fi + fi +} + +test_socket_ping_host() { + local socket_path="$1" + SOCKET_PING_HOST_OK=false + SOCKET_PING_HOST_OUTPUT="socket not found" + + if [[ ! -S "$socket_path" ]]; then + return + fi + + if ! command -v curl &>/dev/null; then + SOCKET_PING_HOST_OUTPUT="curl not available" + return + fi + + SOCKET_PING_HOST_OUTPUT=$(curl -s --max-time 3 --unix-socket "$socket_path" http://localhost/_ping 2>&1) || true + if [[ "$SOCKET_PING_HOST_OUTPUT" == "OK" ]]; then + SOCKET_PING_HOST_OK=true + fi +} + +test_socket_ping_container() { + local docker_cmd="${1:-docker}" + SOCKET_PING_CONTAINER_OK=false + SOCKET_PING_CONTAINER_OUTPUT="sharelatex not running" + + if $docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -qx sharelatex; then + SOCKET_PING_CONTAINER_OUTPUT=$($docker_cmd exec sharelatex sh -c 'curl -s --max-time 3 --unix-socket /var/run/docker.sock http://localhost/_ping 2>&1') || true + if [[ "$SOCKET_PING_CONTAINER_OUTPUT" == "OK" ]]; then + SOCKET_PING_CONTAINER_OK=true + fi + fi +} + +get_seccomp_expected_path() { + local version="${IMAGE_VERSION:-}" + if [[ -z "$version" ]]; then + version="$(head -n 1 "$TOOLKIT_ROOT/config/version" 2>/dev/null)" + fi + echo "$TOOLKIT_ROOT/config/seccomp/$version/clsi-profile.json" +} + +check_seccomp_config() { + local seccomp_path + seccomp_path=$(get_seccomp_expected_path) + local ve="$TOOLKIT_ROOT/config/variables.env" + + SECCOMP_FILE_EXISTS=false + SECCOMP_ENV_MATCHES=false + SECCOMP_ENV_VALUE="" + + if [[ -f "$seccomp_path" ]]; then + SECCOMP_FILE_EXISTS=true + fi + + if [[ -f "$ve" ]]; then + SECCOMP_ENV_VALUE=$(grep "^SECCOMP_PROFILE=" "$ve" 2>/dev/null | tail -1 | sed 's/^SECCOMP_PROFILE=//; s/["'\'']//g') || true + if [[ "$SECCOMP_ENV_VALUE" == "$seccomp_path" ]]; then + SECCOMP_ENV_MATCHES=true + fi + fi +} + +SELINUX_MODULE_NAME="podman_socket_clsi" +SELINUX_MANAGED_LABEL="sharelatex_t" +SELINUX_MODES=(managed custom disable none) +SELINUX_RULES=( + "container_runtime_t unix_stream_socket connectto" + "user_tmp_t sock_file write" + "user_tmp_t sock_file getattr" + "container_runtime_t fifo_file setattr" +) + +check_selinux_module() { + local sudo_cmd="${1:-sudo}" + local modules + SELINUX_MODULE_LOADED=false + + if modules=$($sudo_cmd semodule -l 2>/dev/null); then + if grep -q "^${SELINUX_MODULE_NAME}" <<< "$modules"; then + SELINUX_MODULE_LOADED=true + fi + return 0 + fi + + # listing modules needs privileges, the type the module defines is equivalent + if selinux_label_valid "$SELINUX_MANAGED_LABEL"; then + SELINUX_MODULE_LOADED=true + fi +} + +selinux_access_allowed() { + local src="$1" target="$2" class="$3" perm="$4" + local index bit requested role allowed + + index=$(cat "/sys/fs/selinux/class/${class}/index" 2>/dev/null) || return 2 + bit=$(cat "/sys/fs/selinux/class/${class}/perms/${perm}" 2>/dev/null) || return 2 + requested=$(( 1 << (bit - 1) )) + + for role in system_r object_r; do + exec 3<>/sys/fs/selinux/access || return 2 + if printf 'system_u:system_r:%s:s0 system_u:%s:%s:s0 %s %s' \ + "$src" "$role" "$target" "$index" "$requested" >&3 2>/dev/null; then + read -r allowed _ <&3 + exec 3>&- + (( 0x${allowed:-0} & requested )) && return 0 + return 1 + fi + exec 3>&- + done + + return 2 +} + +selinux_enforce_mode() { + local mode="Disabled" + command -v getenforce &>/dev/null && mode="$(getenforce 2>/dev/null || echo Disabled)" + echo "$mode" +} + +# Returns 0 if the type exists in the loaded policy, 1 if it does not, +# 2 if the policy cannot be queried +selinux_label_valid() { + local label="$1" + + [[ "$label" =~ ^[a-zA-Z0-9_.-]+$ ]] || return 1 + [[ -w /sys/fs/selinux/context ]] || return 2 + + # the kernel rejects a context whose type is not in the loaded policy + printf 'system_u:system_r:%s:s0' "$label" > /sys/fs/selinux/context 2>/dev/null +} + +# Resolves SELINUX_MODE/SELINUX_LABEL, which only apply to a Server Pro podman +# deployment with sibling containers and SELinux enabled. +# Sets: SELINUX_MODE, SELINUX_LABEL (empty unless a label is applied), +# SELINUX_SECURITY_OPT (empty when no security_opt should be applied), +# SELINUX_ENFORCE_MODE, SELINUX_APPLIES, SELINUX_CONFIG_ERROR +check_selinux_config() { + SELINUX_MODE="${SELINUX_MODE:-disable}" + SELINUX_LABEL="${SELINUX_LABEL:-}" + SELINUX_SECURITY_OPT="" + SELINUX_CONFIG_ERROR="" + SELINUX_APPLIES=false + SELINUX_ENFORCE_MODE="$(selinux_enforce_mode)" + + case "$SELINUX_MODE" in + managed) SELINUX_LABEL="$SELINUX_MANAGED_LABEL" ;; + custom) ;; + disable|none) SELINUX_LABEL="" ;; + *) + SELINUX_CONFIG_ERROR="invalid SELINUX_MODE '$SELINUX_MODE' in config/overleaf.rc, expected one of: ${SELINUX_MODES[*]}" + SELINUX_LABEL="" + ;; + esac + + [[ -z "$SELINUX_CONFIG_ERROR" ]] || return 0 + [[ "${SERVER_PRO:-false}" == "true" ]] || return 0 + [[ "${SIBLING_CONTAINERS_ENABLED:-false}" == "true" ]] || return 0 + is_podman || return 0 + [[ "$SELINUX_ENFORCE_MODE" != "Disabled" ]] || return 0 + + SELINUX_APPLIES=true + + local status=0 + case "$SELINUX_MODE" in + managed) + # the managed type only exists once the policy module is installed + selinux_label_valid "$SELINUX_LABEL" || return 0 + SELINUX_SECURITY_OPT="label=type:$SELINUX_LABEL" + ;; + custom) + if [[ -z "$SELINUX_LABEL" ]]; then + SELINUX_CONFIG_ERROR="SELINUX_MODE=custom requires SELINUX_LABEL to be set in config/overleaf.rc" + return 0 + fi + selinux_label_valid "$SELINUX_LABEL" || status=$? + if [[ "$status" -eq 1 ]]; then + SELINUX_CONFIG_ERROR="SELINUX_LABEL '$SELINUX_LABEL' is not a valid SELinux type in the loaded policy" + return 0 + fi + SELINUX_SECURITY_OPT="label=type:$SELINUX_LABEL" + ;; + disable) + SELINUX_SECURITY_OPT="label=disable" + ;; + esac + + return 0 +} + +check_selinux_rules() { + local src="${1:-$SELINUX_MANAGED_LABEL}" + SELINUX_RULES_OK=true + SELINUX_MISSING_RULES=() + + local rule target class perm + + if ! selinux_label_valid "$src"; then + # the type is not in the loaded policy, so none of its rules can be present + SELINUX_RULES_OK=false + SELINUX_MISSING_RULES=("${SELINUX_RULES[@]/#/$src }") + return 0 + fi + + for rule in "${SELINUX_RULES[@]}"; do + read -r target class perm <<< "$rule" + + selinux_access_allowed "$src" "$target" "$class" "$perm" && continue + + SELINUX_RULES_OK=false + SELINUX_MISSING_RULES+=("$src $rule") + done + + return 0 +} \ No newline at end of file