From ca0528852adca23415f16f154299d5d032644f42 Mon Sep 17 00:00:00 2001 From: mlevans0 Date: Tue, 5 May 2026 14:31:14 +0100 Subject: [PATCH 1/6] feat: add podman support --- bin/doctor | 157 +++++- bin/podman-setup | 615 +++++++++++++++++++++ lib/clsi-profile.json | 856 ++++++++++++++++++++++++++++++ lib/docker-compose.base.yml | 2 +- lib/docker-compose.git-bridge.yml | 4 - lib/docker-compose.mongo.yml | 5 +- lib/docker-compose.redis.yml | 5 +- lib/podman_socket_clsi.te | 17 + lib/shared-functions.sh | 167 ++++++ 9 files changed, 1794 insertions(+), 34 deletions(-) create mode 100755 bin/podman-setup create mode 100644 lib/clsi-profile.json create mode 100644 lib/podman_socket_clsi.te diff --git a/bin/doctor b/bin/doctor index 00e6693c..e583978e 100755 --- a/bin/doctor +++ b/bin/doctor @@ -138,29 +138,142 @@ 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" -lt 23 ]]; 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" && -n "$socket_path" ]]; 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 [[ ! -S "$socket_path" ]]; then + add_warning "Configured socket path '$socket_path' does not exist or is not a socket" + else + if [[ "$using_podman" == true ]] && command -v getenforce &>/dev/null; then + local enforce + enforce=$(getenforce 2>/dev/null || echo "Disabled") + print_point 1 "SELinux: $enforce" + + if [[ "$enforce" != "Disabled" ]]; then + check_selinux_module "sudo -n" + if [[ "$SELINUX_MODULE_LOADED" == true ]]; then + print_point 1 "SELinux module: present (podman_socket_clsi)" + check_selinux_rules "sudo -n" + if [[ "$SELINUX_RULES_OK" == false ]]; then + for rule in "${SELINUX_MISSING_RULES[@]}"; do + print_point 2 "rule MISSING: $rule" + done + add_warning "SELinux policy module podman_socket_clsi is missing required rules. Container cannot connect to Podman socket." + fi + else + print_point 1 "SELinux module (podman_socket_clsi): not found" + add_warning "SELinux is Enforcing but the podman_socket_clsi module is not loaded. The sharelatex container will be blocked from connecting to the Podman socket." + fi + fi + fi + + 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 + + if [[ "$using_podman" == true ]]; then + 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 + fi } function print_warnings() { @@ -222,18 +335,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." diff --git a/bin/podman-setup b/bin/podman-setup new file mode 100755 index 00000000..d79c4e1c --- /dev/null +++ b/bin/podman-setup @@ -0,0 +1,615 @@ +#! /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" + +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 + +USER_NAME="$(id -un)" +USER_UID="$(id -u)" +DRY_RUN=true + +declare -i CHECK_PASS=0 CHECK_WARN=0 CHECK_FAIL=0 +SUMMARY="" + +REQUIRED_PKGS=(podman podman-docker git curl checkpolicy policycoreutils) + +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" + local desc="lingering enabled for $USER_NAME" + + if loginctl show-user "$USER_NAME" -p Linger --value 2>/dev/null | grep -q yes; then + report ok "$desc" + return 0 + fi + + if is_dry_run; then + report warn "$desc missing" + return 1 + fi + + if run_sudo loginctl enable-linger "$USER_NAME"; then + report fix "$desc configured" + else + report fail "$desc" + 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 missing" + 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 "dnf install" + 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 "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 missing" + 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 "download compose" + rc=1 + elif ! curl -sL "${base_url}/${binary}.sha256" -o "$tmp_sum"; then + report fail "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 linked" + elif is_dry_run; then + report warn "Compose plugin symlink missing" + 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 "containers.conf incomplete" + 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 exists" + elif is_dry_run; then + report warn "nodocker missing" + else + if run_sudo mkdir -p /etc/containers && run_sudo touch /etc/containers/nodocker; then + report fix "created nodocker" + else + report fail "nodocker" + 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 missing" + 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 "quay.io credentials valid" + return 0 + fi + + report warn "quay.io auth missing or invalid" + is_apply && printf "\n Run 'podman login quay.io' to authenticate.\n\n" +} + +check_loaded_selinux_policy() { + local sudo_cmd="$1" + # shellcheck disable=SC2153 # SELINUX_* set by lib/shared-functions.sh + check_selinux_module "$sudo_cmd" + if [[ "$SELINUX_MODULE_LOADED" == true ]]; then + report ok "SELinux module loaded" + + check_selinux_rules "$sudo_cmd" + if [[ "$SELINUX_RULES_OK" == true ]]; then + report ok "SELinux rules verified" + elif is_dry_run; then + report warn "SELinux rules incomplete" + else + for rule in "${SELINUX_MISSING_RULES[@]}"; do + report fail "Rule missing: $rule" + done + fi + return 0 + fi + return 1 +} + +ensure_selinux_policy() { + heading "SELinux Policy" + + if is_dry_run; then + check_loaded_selinux_policy "sudo -n" || report warn "SELinux module status unknown (requires sudo)" + return 0 + fi + + check_loaded_selinux_policy "run_sudo" && return 0 + + # Install SELinux module + local dir="$HOME/.overleaf/selinux" + mkdir -p "$dir" + + cp "$TOOLKIT_ROOT/lib/podman_socket_clsi.te" "$dir/podman_socket_clsi.te" || { report fail "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 "compile/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 "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 "sharelatex not running (skip container socket test)" + else + report fail "Container -> socket failed ($SOCKET_PING_CONTAINER_OUTPUT)" + fi +} + +ensure_seccomp() { + heading "Seccomp" + local seccomp_path + seccomp_path="$(get_seccomp_expected_path)" + local bundled="$TOOLKIT_ROOT/lib/clsi-profile.json" + + if [ -f "$seccomp_path" ]; then + report ok "Seccomp profile exists" + elif is_dry_run; then + report warn "Seccomp profile not installed" + elif [ ! -f "$bundled" ]; then + report fail "bundled seccomp profile not found" + else + mkdir -p "$HOME/.overleaf/seccomp" + if cp "$bundled" "$seccomp_path"; then + report fix "copied seccomp profile" + else + report fail "copy seccomp profile" + fi + 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}" + + for entry in "mongo:${mongo_data}:999" "redis:${redis_data}:999" "overleaf:${overleaf_data}:33"; 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 "$name data dir permissions" + 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 +} + +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. + +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 + +echo "=== Podman Setup ===" + +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/lib/clsi-profile.json b/lib/clsi-profile.json new file mode 100644 index 00000000..c78027b1 --- /dev/null +++ b/lib/clsi-profile.json @@ -0,0 +1,856 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32" + ], + "syscalls": [ + { + "name": "getrandom", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "access", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "arch_prctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "brk", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "chmod", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_getres", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_gettime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clock_nanosleep", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "clone", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2080505856, + "valueTwo": 0, + "op": "SCMP_CMP_MASKED_EQ" + } + ] + }, + { + "name": "clone3", + "action": "SCMP_ACT_ERRNO", + "errnoRet": 38 + }, + { + "name": "close", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "copy_file_range", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "creat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "dup3", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "execve", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "execveat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "exit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "exit_group", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "faccessat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fadvise64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fadvise64_64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fallocate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchmod", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchmodat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fcntl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fcntl64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fdatasync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fork", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fstatfs64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fsync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ftruncate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ftruncate64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "futex", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "futimesat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getcpu", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getcwd", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getdents", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getdents64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getegid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getegid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "geteuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "geteuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgroups", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getgroups32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpgrp", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getppid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getpriority", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresgid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getresuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getrlimit", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "get_robust_list", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getrusage", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getsid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "gettid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "getuid32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "ioctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "kill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "_llseek", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lseek", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lstat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "lstat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "madvise", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mkdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mkdirat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mmap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mmap2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mprotect", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "mremap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "munmap", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "newfstatat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "open", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "openat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "openat2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pause", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pipe", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pipe2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "prctl", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pread64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "preadv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "prlimit64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pwrite64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pwritev", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "read", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readlinkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "readv", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rename", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "renameat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "renameat2", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "restart_syscall", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rmdir", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigaction", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigpending", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigprocmask", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigqueueinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigreturn", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigsuspend", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_sigtimedwait", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "rt_tgsigqueueinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getaffinity", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getparam", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_get_priority_max", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_get_priority_min", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_getscheduler", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_rr_get_interval", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sched_yield", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendfile", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sendfile64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgroups", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgroups32", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_robust_list", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "set_tid_address", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sigaltstack", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "stat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "statx", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "stat64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "statfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "statfs64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sync", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sync_file_range", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "syncfs", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "sysinfo", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "tgkill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_create", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_delete", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_getoverrun", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_gettime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "timer_settime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "times", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "tkill", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "truncate", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "truncate64", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "umask", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "uname", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "unlink", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "unlinkat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utime", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utimensat", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "utimes", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "vfork", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "vhangup", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "wait4", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "waitid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "write", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "writev", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "pread", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setgid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "setuid", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "capget", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "capset", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "fchown", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "gettimeofday", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, { + "name": "epoll_pwait", + "action": "SCMP_ACT_ALLOW", + "args": [] + } + ] +} 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..71604c67 100644 --- a/lib/docker-compose.git-bridge.yml +++ b/lib/docker-compose.git-bridge.yml @@ -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/podman_socket_clsi.te b/lib/podman_socket_clsi.te new file mode 100644 index 00000000..b5c56d2d --- /dev/null +++ b/lib/podman_socket_clsi.te @@ -0,0 +1,17 @@ +module podman_socket_clsi 1.1; + +# This module grants the sharelatex container permission to +# interact with the rootless Podman socket under SELinux enforcing. +# The socket lives at /run/user/$UID/podman/podman.sock with context user_tmp_t. + +require { + type container_t; + type user_tmp_t; + type container_runtime_t; + class sock_file { write getattr }; + class unix_stream_socket connectto; +} + +allow container_t user_tmp_t:sock_file write; +allow container_t user_tmp_t:sock_file getattr; +allow container_t container_runtime_t:unix_stream_socket connectto; diff --git a/lib/shared-functions.sh b/lib/shared-functions.sh index a35d17c7..f86d6195 100644 --- a/lib/shared-functions.sh +++ b/lib/shared-functions.sh @@ -225,3 +225,170 @@ 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() { + echo "$HOME/.overleaf/seccomp/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 +} + +# Check if SELinux module is loaded. +SELINUX_MODULE_NAME="podman_socket_clsi" +SELINUX_RULES=( + "container_t container_runtime_t unix_stream_socket connectto" + "container_t user_tmp_t sock_file write" + "container_t user_tmp_t sock_file getattr" +) + +# Check if SELinux module is loaded. +# Args: $1 = sudo command (e.g. "sudo -n" or "run_sudo") +# Sets: SELINUX_MODULE_LOADED (true/false) +check_selinux_module() { + local sudo_cmd="${1:-sudo -n}" + SELINUX_MODULE_LOADED=false + + if $sudo_cmd semodule -l 2>/dev/null | grep -q "^${SELINUX_MODULE_NAME}"; then + SELINUX_MODULE_LOADED=true + fi +} + +# Verify SELinux module rules with sesearch. +# Args: $1 = sudo command (e.g. "sudo -n" or "run_sudo") +# Sets: SELINUX_RULES_OK (true/false), SELINUX_MISSING_RULES (array) +check_selinux_rules() { + local sudo_cmd="${1:-sudo -n}" + SELINUX_RULES_OK=true + SELINUX_MISSING_RULES=() + + if ! command -v sesearch &>/dev/null; then + return + fi + + local rule + for rule in "${SELINUX_RULES[@]}"; do + read -r src target class perm <<< "$rule" + if ! $sudo_cmd sesearch --allow -s "$src" -t "$target" -c "$class" -p "$perm" 2>/dev/null | grep -q "allow"; then + SELINUX_RULES_OK=false + SELINUX_MISSING_RULES+=("$rule") + fi + done +} \ No newline at end of file From 425f5f220c297b11acafd84cf21c2b73f3005cc5 Mon Sep 17 00:00:00 2001 From: Mathew Evans Date: Tue, 18 Aug 2026 15:06:50 +0100 Subject: [PATCH 2/6] Update supported Docker version check + doc examples, move clsi-profile and selinux module to config/ --- bin/doctor | 2 +- bin/podman-setup | 133 ++++-- doc/quick-start-guide.md | 2 +- doc/the-doctor.md | 9 +- lib/clsi-profile.json | 856 --------------------------------------- lib/shared-functions.sh | 6 +- 6 files changed, 106 insertions(+), 902 deletions(-) delete mode 100644 lib/clsi-profile.json diff --git a/bin/doctor b/bin/doctor index e583978e..9315da00 100755 --- a/bin/doctor +++ b/bin/doctor @@ -157,7 +157,7 @@ function check_docker_daemon() { if [[ "$docker_server_version" =~ ^([0-9]+)\.([0-9]+) ]]; then local major="${BASH_REMATCH[1]}" local minor="${BASH_REMATCH[2]}" - if [[ "$major" -lt 23 ]]; then + 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 diff --git a/bin/podman-setup b/bin/podman-setup index d79c4e1c..21def1d8 100755 --- a/bin/podman-setup +++ b/bin/podman-setup @@ -29,6 +29,34 @@ 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_os() { [[ -f /etc/os-release ]] || { echo "ERROR: /etc/os-release missing"; exit 1; } @@ -47,14 +75,32 @@ validate_os() { 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 " Sandboxed Compiles are a Server Pro feature." + 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@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)" -DRY_RUN=true 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" @@ -383,8 +429,7 @@ ensure_selinux_policy() { check_loaded_selinux_policy "run_sudo" && return 0 - # Install SELinux module - local dir="$HOME/.overleaf/selinux" + local dir="$TOOLKIT_ROOT/config/selinux" mkdir -p "$dir" cp "$TOOLKIT_ROOT/lib/podman_socket_clsi.te" "$dir/podman_socket_clsi.te" || { report fail "copy SELinux policy"; return 1; } @@ -424,25 +469,58 @@ ensure_socket_connectivity() { 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 "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 "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)" - local bundled="$TOOLKIT_ROOT/lib/clsi-profile.json" if [ -f "$seccomp_path" ]; then - report ok "Seccomp profile exists" + report ok "Seccomp profile installed for $IMAGE_VERSION" elif is_dry_run; then - report warn "Seccomp profile not installed" - elif [ ! -f "$bundled" ]; then - report fail "bundled seccomp profile not found" - else - mkdir -p "$HOME/.overleaf/seccomp" - if cp "$bundled" "$seccomp_path"; then - report fix "copied seccomp profile" - else - report fail "copy seccomp profile" - fi + report warn "Seccomp profile is not currently installed 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 @@ -565,31 +643,6 @@ print_summary() { fi } -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. - -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 - echo "=== Podman Setup ===" run_checks 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/clsi-profile.json b/lib/clsi-profile.json deleted file mode 100644 index c78027b1..00000000 --- a/lib/clsi-profile.json +++ /dev/null @@ -1,856 +0,0 @@ -{ - "defaultAction": "SCMP_ACT_ERRNO", - "architectures": [ - "SCMP_ARCH_X86_64", - "SCMP_ARCH_X86", - "SCMP_ARCH_X32" - ], - "syscalls": [ - { - "name": "getrandom", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "access", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "arch_prctl", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "brk", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "chdir", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "chmod", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "clock_getres", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "clock_gettime", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "clock_nanosleep", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "clone", - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 2080505856, - "valueTwo": 0, - "op": "SCMP_CMP_MASKED_EQ" - } - ] - }, - { - "name": "clone3", - "action": "SCMP_ACT_ERRNO", - "errnoRet": 38 - }, - { - "name": "close", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "copy_file_range", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "creat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "dup", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "dup2", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "dup3", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "execve", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "execveat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "exit", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "exit_group", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "faccessat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fadvise64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fadvise64_64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fallocate", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fchdir", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fchmod", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fchmodat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fcntl", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fcntl64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fdatasync", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fork", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fstat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fstat64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fstatat64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fstatfs", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fstatfs64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fsync", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "ftruncate", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "ftruncate64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "futex", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "futimesat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getcpu", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getcwd", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getdents", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getdents64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getegid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getegid32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "geteuid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "geteuid32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getgid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getgid32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getgroups", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getgroups32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getpgid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getpgrp", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getpid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getppid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getpriority", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getresgid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getresgid32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getresuid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getresuid32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getrlimit", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "get_robust_list", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getrusage", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getsid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "gettid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getuid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "getuid32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "ioctl", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "kill", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "_llseek", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "lseek", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "lstat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "lstat64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "madvise", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "mkdir", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "mkdirat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "mmap", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "mmap2", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "mprotect", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "mremap", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "munmap", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "newfstatat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "open", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "openat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "openat2", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "pause", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "pipe", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "pipe2", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "prctl", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "pread64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "preadv", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "prlimit64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "pwrite64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "pwritev", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "read", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "readlink", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "readlinkat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "readv", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rename", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "renameat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "renameat2", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "restart_syscall", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rmdir", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_sigaction", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_sigpending", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_sigprocmask", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_sigqueueinfo", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_sigreturn", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_sigsuspend", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_sigtimedwait", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "rt_tgsigqueueinfo", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sched_getaffinity", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sched_getparam", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sched_get_priority_max", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sched_get_priority_min", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sched_getscheduler", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sched_rr_get_interval", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sched_yield", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sendfile", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sendfile64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "setgroups", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "setgroups32", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "set_robust_list", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "set_tid_address", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sigaltstack", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "stat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "statx", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "stat64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "statfs", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "statfs64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sync", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sync_file_range", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "syncfs", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "sysinfo", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "tgkill", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "timer_create", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "timer_delete", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "timer_getoverrun", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "timer_gettime", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "timer_settime", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "times", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "tkill", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "truncate", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "truncate64", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "umask", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "uname", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "unlink", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "unlinkat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "utime", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "utimensat", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "utimes", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "vfork", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "vhangup", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "wait4", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "waitid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "write", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "writev", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "pread", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "setgid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "setuid", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "capget", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "capset", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "fchown", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, - { - "name": "gettimeofday", - "action": "SCMP_ACT_ALLOW", - "args": [] - }, { - "name": "epoll_pwait", - "action": "SCMP_ACT_ALLOW", - "args": [] - } - ] -} diff --git a/lib/shared-functions.sh b/lib/shared-functions.sh index f86d6195..b4c33849 100644 --- a/lib/shared-functions.sh +++ b/lib/shared-functions.sh @@ -327,7 +327,11 @@ test_socket_ping_container() { } get_seccomp_expected_path() { - echo "$HOME/.overleaf/seccomp/clsi-profile.json" + 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() { From 95862e62814b64023f759b600e96c8c89c2a2472 Mon Sep 17 00:00:00 2001 From: Mathew Evans Date: Wed, 19 Aug 2026 01:35:19 +0100 Subject: [PATCH 3/6] Align check results between podman-setup and doctor scripts, add missing git-bridge support/related checks and tune SELinux module. --- bin/docker-compose | 8 ++ bin/doctor | 28 +++--- bin/podman-setup | 152 +++++++++++++++++++----------- lib/docker-compose.git-bridge.yml | 2 +- lib/docker-compose.selinux.yml | 4 + lib/podman_socket_clsi.te | 32 +++++-- lib/shared-functions.sh | 81 +++++++++++----- 7 files changed, 212 insertions(+), 95 deletions(-) create mode 100644 lib/docker-compose.selinux.yml diff --git a/bin/docker-compose b/bin/docker-compose index 4c9951b5..68e658db 100755 --- a/bin/docker-compose +++ b/bin/docker-compose @@ -31,6 +31,9 @@ function build_environment() { if [[ "$SIBLING_CONTAINERS_ENABLED" == "true" ]]; then if [[ $SERVER_PRO == "true" ]]; then set_sibling_containers_vars + if use_selinux_label; then + set_selinux_vars + fi else if [[ ${SKIP_WARNINGS:-null} != "true" ]]; then echo "WARNING: SIBLING_CONTAINERS_ENABLED=true is not supported in Overleaf Community Edition." >&2 @@ -152,6 +155,11 @@ function set_sibling_containers_vars() { export OVERLEAF_DATA_PATH } +# Set environment variables for docker-compose.selinux.yml +function set_selinux_vars() { + DOCKER_COMPOSE_FLAGS+=(-f "$TOOLKIT_ROOT/lib/docker-compose.selinux.yml") +} + # 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 9315da00..67e8e3e8 100755 --- a/bin/doctor +++ b/bin/doctor @@ -216,19 +216,25 @@ function check_docker_daemon() { print_point 1 "SELinux: $enforce" if [[ "$enforce" != "Disabled" ]]; then - check_selinux_module "sudo -n" + check_selinux_rules + if [[ "$SELINUX_RULES_CHECKED" == false ]]; then + print_point 1 "SELinux rules: unknown" + elif [[ "$SELINUX_RULES_OK" == true ]]; then + print_point 1 "SELinux rules: present" + else + for rule in "${SELINUX_MISSING_RULES[@]}"; do + print_point 2 "rule $rule not present" + done + add_warning "SELinux is Enforcing 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 + + check_selinux_module if [[ "$SELINUX_MODULE_LOADED" == true ]]; then - print_point 1 "SELinux module: present (podman_socket_clsi)" - check_selinux_rules "sudo -n" - if [[ "$SELINUX_RULES_OK" == false ]]; then - for rule in "${SELINUX_MISSING_RULES[@]}"; do - print_point 2 "rule MISSING: $rule" - done - add_warning "SELinux policy module podman_socket_clsi is missing required rules. Container cannot connect to Podman socket." - fi + print_point 1 "SELinux module: loaded (podman_socket_clsi)" + elif [[ "$SELINUX_MODULE_CHECKED" == false ]]; then + print_point 1 "SELinux module: unknown" else - print_point 1 "SELinux module (podman_socket_clsi): not found" - add_warning "SELinux is Enforcing but the podman_socket_clsi module is not loaded. The sharelatex container will be blocked from connecting to the Podman socket." + print_point 1 "SELinux module: not loaded" fi fi fi diff --git a/bin/podman-setup b/bin/podman-setup index 21def1d8..a703e479 100755 --- a/bin/podman-setup +++ b/bin/podman-setup @@ -57,6 +57,21 @@ HELP 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; } @@ -81,9 +96,9 @@ validate_server_pro() { [[ "$SERVER_PRO" == "true" ]] && return 0 echo "ERROR: SERVER_PRO=true is required in config/overleaf.rc (found '$SERVER_PRO')." - echo " Sandboxed Compiles are a Server Pro feature." + 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@overleaf.com if you need assistance." + echo " Or, if you already have a license, contact support+serverpro@overleaf.com if you need assistance." exit 1 } @@ -135,22 +150,21 @@ get_podman_socket_path() { ensure_lingering() { heading "Lingering" - local desc="lingering enabled for $USER_NAME" if loginctl show-user "$USER_NAME" -p Linger --value 2>/dev/null | grep -q yes; then - report ok "$desc" + report ok "lingering enabled for $USER_NAME" return 0 fi if is_dry_run; then - report warn "$desc missing" + report warn "lingering not enabled for $USER_NAME" return 1 fi if run_sudo loginctl enable-linger "$USER_NAME"; then - report fix "$desc configured" + report fix "lingering enabled for $USER_NAME" else - report fail "$desc" + report fail "failed to enable lingering for $USER_NAME" return 1 fi } @@ -163,7 +177,7 @@ ensure_packages() { if rpm -q "$pkg" &>/dev/null; then report ok "$pkg installed" else - report fail "$pkg missing" + report fail "$pkg not installed" missing+=("$pkg") fi done @@ -172,7 +186,7 @@ ensure_packages() { if run_sudo dnf install -y "${missing[@]}"; then report fix "installed ${missing[*]}" else - report fail "dnf install" + report fail "failed to install ${missing[*]}" fi fi } @@ -193,7 +207,7 @@ ensure_podman_socket() { if systemctl --user enable --now podman.socket; then report fix "enabled podman.socket" else - report fail "podman.socket" + report fail "failed to enable podman.socket" fi } @@ -275,7 +289,7 @@ ensure_docker_compose() { 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 missing" + report fail "Docker Compose not installed" return 1 else mkdir -p "$HOME/.local/bin" "$HOME/.docker/cli-plugins" @@ -287,10 +301,10 @@ ensure_docker_compose() { tmp_sum="$(mktemp)" if ! curl -sL "${base_url}/${binary}" -o "$tmp_bin"; then - report fail "download compose" + report fail "failed to download compose" rc=1 elif ! curl -sL "${base_url}/${binary}.sha256" -o "$tmp_sum"; then - report fail "download compose checksum" + 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" @@ -305,9 +319,9 @@ ensure_docker_compose() { fi if [ -L "$compose_plugin" ]; then - report ok "Compose plugin linked" + report ok "Compose plugin symlink present" elif is_dry_run; then - report warn "Compose plugin symlink missing" + report warn "Compose plugin symlink not present" else mkdir -p "$HOME/.docker/cli-plugins" ln -sf "$compose_bin" "$compose_plugin" @@ -324,7 +338,7 @@ ensure_containers_config() { fi if is_dry_run; then - report warn "containers.conf incomplete" + report warn "compose provider not configured" return 1 fi @@ -342,14 +356,14 @@ EOF ensure_nodocker_file() { if [ -f /etc/containers/nodocker ]; then - report ok "nodocker exists" + report ok "nodocker file present" elif is_dry_run; then - report warn "nodocker missing" + 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" + report fix "created nodocker file" else - report fail "nodocker" + report fail "failed to create nodocker file" fi fi } @@ -359,7 +373,7 @@ ensure_registries_config() { 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 missing" + report warn "registries.conf.d drop-in not configured" else mkdir -p "$(dirname "$reg_conf")" cat > "$reg_conf" << EOF @@ -389,57 +403,80 @@ ensure_registry_auth() { check_quay_login if [[ "$QUAY_LOGIN_STATUS" == true ]]; then - report ok "quay.io credentials valid" + report ok "logged in to quay.io" return 0 fi - report warn "quay.io auth missing or invalid" + 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() { - local sudo_cmd="$1" # shellcheck disable=SC2153 # SELINUX_* set by lib/shared-functions.sh - check_selinux_module "$sudo_cmd" + check_selinux_module + if [[ "$SELINUX_MODULE_LOADED" == true ]]; then - report ok "SELinux module loaded" + report ok "SELinux module loaded (podman_socket_clsi)" + elif [[ "$SELINUX_MODULE_CHECKED" == false ]]; then + report skip "SELinux module unknown" + else + report skip "SELinux module not loaded" + fi - check_selinux_rules "$sudo_cmd" - if [[ "$SELINUX_RULES_OK" == true ]]; then - report ok "SELinux rules verified" - elif is_dry_run; then - report warn "SELinux rules incomplete" - else - for rule in "${SELINUX_MISSING_RULES[@]}"; do - report fail "Rule missing: $rule" - done - fi + check_selinux_rules + + if [[ "$SELINUX_RULES_CHECKED" == false ]]; then + report skip "SELinux rules unknown" + [[ "$SELINUX_MODULE_LOADED" == true ]] && return 0 + return 1 + fi + + if [[ "$SELINUX_RULES_OK" == true ]]; then + report ok "SELinux rules present" return 0 fi + + if is_apply; then + for rule in "${SELINUX_MISSING_RULES[@]}"; do + report skip "rule $rule not present" + done + fi return 1 } ensure_selinux_policy() { heading "SELinux Policy" - if is_dry_run; then - check_loaded_selinux_policy "sudo -n" || report warn "SELinux module status unknown (requires sudo)" + local enforce="Disabled" + command -v getenforce &>/dev/null && enforce="$(getenforce 2>/dev/null || echo Disabled)" + + if [[ "$enforce" == "Disabled" ]]; then + report skip "SELinux disabled" return 0 fi - check_loaded_selinux_policy "run_sudo" && return 0 + check_loaded_selinux_policy && return 0 + + if is_dry_run; then + if [[ "$SELINUX_RULES_CHECKED" == false ]]; then + report warn "SELinux rules unknown, policy module not loaded (use --apply to install it)" + else + report warn "SELinux rules not present (use --apply to install the policy module)" + fi + 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 "copy SELinux policy"; return 1; } + 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 "compile/load SELinux module" + report fail "failed to compile or load SELinux module" return 1 fi } @@ -452,20 +489,20 @@ ensure_socket_connectivity() { # 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" + report ok "host → socket OK" elif [[ "$SOCKET_PING_HOST_OUTPUT" == "socket not found" ]]; then - report warn "socket not found, is podman.socket running?" + report warn "host → socket: socket not found, is podman.socket running?" else - report fail "Host -> socket failed ($SOCKET_PING_HOST_OUTPUT)" + 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" + report ok "container → socket OK" elif [[ "$SOCKET_PING_CONTAINER_OUTPUT" == "sharelatex not running" ]]; then - report warn "sharelatex not running (skip container socket test)" + report warn "container → socket skipped (sharelatex not running)" else - report fail "Container -> socket failed ($SOCKET_PING_CONTAINER_OUTPUT)" + report fail "container → socket failed ($SOCKET_PING_CONTAINER_OUTPUT)" fi } @@ -483,7 +520,7 @@ ensure_image_pulled() { if podman pull "$IMAGE"; then report fix "pulled $IMAGE" else - report fail "pull $IMAGE" + report fail "failed to pull $IMAGE" return 1 fi } @@ -493,7 +530,7 @@ extract_seccomp_profile() { local cid rc=0 if ! cid="$(podman create "$IMAGE" 2>/dev/null)"; then - report fail "create container from $IMAGE" + report fail "failed to create container from $IMAGE" return 1 fi @@ -515,9 +552,9 @@ ensure_seccomp() { seccomp_path="$(get_seccomp_expected_path)" if [ -f "$seccomp_path" ]; then - report ok "Seccomp profile installed for $IMAGE_VERSION" + report ok "Seccomp profile present for $IMAGE_VERSION" elif is_dry_run; then - report warn "Seccomp profile is not currently installed for $IMAGE_VERSION" + report warn "Seccomp profile not present for $IMAGE_VERSION" ensure_image_pulled || true elif ensure_image_pulled; then extract_seccomp_profile "$seccomp_path" || true @@ -552,7 +589,16 @@ ensure_data_permissions() { overleaf_data="$(read_configuration "OVERLEAF_DATA_PATH")" overleaf_data="${overleaf_data:-data/sharelatex}" - for entry in "mongo:${mongo_data}:999" "redis:${redis_data}:999" "overleaf:${overleaf_data}:33"; do + 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" @@ -572,7 +618,7 @@ ensure_data_permissions() { if podman unshare chown -R "${uid}:${uid}" "$full_path" 2>/dev/null; then report fix "$name data dir ownership corrected" else - report fail "$name data dir permissions" + report fail "failed to correct $name data dir ownership" fi fi done diff --git a/lib/docker-compose.git-bridge.yml b/lib/docker-compose.git-bridge.yml index 71604c67..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" diff --git a/lib/docker-compose.selinux.yml b/lib/docker-compose.selinux.yml new file mode 100644 index 00000000..87901173 --- /dev/null +++ b/lib/docker-compose.selinux.yml @@ -0,0 +1,4 @@ +services: + sharelatex: + security_opt: + - "label=type:sharelatex_t" diff --git a/lib/podman_socket_clsi.te b/lib/podman_socket_clsi.te index b5c56d2d..d648a68a 100644 --- a/lib/podman_socket_clsi.te +++ b/lib/podman_socket_clsi.te @@ -1,17 +1,31 @@ -module podman_socket_clsi 1.1; +module podman_socket_clsi 2.0; -# This module grants the sharelatex container permission to -# interact with the rootless Podman socket under SELinux enforcing. -# The socket lives at /run/user/$UID/podman/podman.sock with context user_tmp_t. +# 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 { - type container_t; - type user_tmp_t; + 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; } -allow container_t user_tmp_t:sock_file write; -allow container_t user_tmp_t:sock_file getattr; -allow container_t container_runtime_t: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 b4c33849..633749f1 100644 --- a/lib/shared-functions.sh +++ b/lib/shared-functions.sh @@ -355,44 +355,83 @@ check_seccomp_config() { fi } -# Check if SELinux module is loaded. SELINUX_MODULE_NAME="podman_socket_clsi" SELINUX_RULES=( - "container_t container_runtime_t unix_stream_socket connectto" - "container_t user_tmp_t sock_file write" - "container_t user_tmp_t sock_file getattr" + "sharelatex_t container_runtime_t unix_stream_socket connectto" + "sharelatex_t user_tmp_t sock_file write" + "sharelatex_t container_runtime_t fifo_file setattr" ) -# Check if SELinux module is loaded. -# Args: $1 = sudo command (e.g. "sudo -n" or "run_sudo") -# Sets: SELINUX_MODULE_LOADED (true/false) check_selinux_module() { - local sudo_cmd="${1:-sudo -n}" + local sudo_cmd="${1:-sudo}" + local modules SELINUX_MODULE_LOADED=false + SELINUX_MODULE_CHECKED=true - if $sudo_cmd semodule -l 2>/dev/null | grep -q "^${SELINUX_MODULE_NAME}"; then + if ! modules=$($sudo_cmd semodule -l 2>/dev/null); then + SELINUX_MODULE_CHECKED=false + return 0 + fi + + if grep -q "^${SELINUX_MODULE_NAME}" <<< "$modules"; then SELINUX_MODULE_LOADED=true fi } -# Verify SELinux module rules with sesearch. -# Args: $1 = sudo command (e.g. "sudo -n" or "run_sudo") -# Sets: SELINUX_RULES_OK (true/false), SELINUX_MISSING_RULES (array) +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 +} + +# Returns 0 if 'label=type:sharelatex_t' can safely be applied: a Server Pro +# podman deployment with sibling containers, and the podman_socket_clsi module loaded +use_selinux_label() { + [[ "${SERVER_PRO:-false}" == "true" ]] || return 1 + [[ "${SIBLING_CONTAINERS_ENABLED:-false}" == "true" ]] || return 1 + is_podman || return 1 + + check_selinux_rules + [[ "$SELINUX_RULES_CHECKED" == true && "$SELINUX_RULES_OK" == true ]] +} + check_selinux_rules() { - local sudo_cmd="${1:-sudo -n}" SELINUX_RULES_OK=true + SELINUX_RULES_CHECKED=true SELINUX_MISSING_RULES=() - if ! command -v sesearch &>/dev/null; then - return - fi - - local rule + local rule src target class perm status for rule in "${SELINUX_RULES[@]}"; do read -r src target class perm <<< "$rule" - if ! $sudo_cmd sesearch --allow -s "$src" -t "$target" -c "$class" -p "$perm" 2>/dev/null | grep -q "allow"; then - SELINUX_RULES_OK=false - SELINUX_MISSING_RULES+=("$rule") + + selinux_access_allowed "$src" "$target" "$class" "$perm" && continue + status=$? + + if [[ "$status" -eq 2 ]]; then + SELINUX_RULES_CHECKED=false + return 0 fi + + SELINUX_RULES_OK=false + SELINUX_MISSING_RULES+=("$rule") done + + return 0 } \ No newline at end of file From 0c523142256926eab3b0ba573730dc6be5427a53 Mon Sep 17 00:00:00 2001 From: Mathew Evans Date: Wed, 19 Aug 2026 01:52:43 +0100 Subject: [PATCH 4/6] Add warning about experimental Podman support in setup scripts --- bin/podman-setup | 6 ++++++ bin/up | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/bin/podman-setup b/bin/podman-setup index a703e479..0bb7851e 100755 --- a/bin/podman-setup +++ b/bin/podman-setup @@ -690,6 +690,12 @@ print_summary() { } 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 diff --git a/bin/up b/bin/up index 087e1d90..ccdd0f56 100755 --- a/bin/up +++ b/bin/up @@ -76,6 +76,17 @@ 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_not_using_detach_mode() { local detached=false for arg in "$@"; do @@ -115,6 +126,7 @@ function __main__() { pull_sandboxed_compiles fi + notify_about_podman_limited_support notify_about_not_using_detach_mode "$@" exec "$TOOLKIT_ROOT/bin/docker-compose" up "$@" } From 848c724dc1dcd3b413177204e7a981b13190d2cc Mon Sep 17 00:00:00 2001 From: Mathew Evans Date: Wed, 19 Aug 2026 10:02:40 +0100 Subject: [PATCH 5/6] Add SELinux rule for getattr permission on user_tmp_t socket files --- lib/shared-functions.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/shared-functions.sh b/lib/shared-functions.sh index 633749f1..f6d3454f 100644 --- a/lib/shared-functions.sh +++ b/lib/shared-functions.sh @@ -359,6 +359,7 @@ SELINUX_MODULE_NAME="podman_socket_clsi" SELINUX_RULES=( "sharelatex_t container_runtime_t unix_stream_socket connectto" "sharelatex_t user_tmp_t sock_file write" + "sharelatex_t user_tmp_t sock_file getattr" "sharelatex_t container_runtime_t fifo_file setattr" ) From 629529fd071eb329828da87d3773cb063e905ec7 Mon Sep 17 00:00:00 2001 From: Mathew Evans Date: Wed, 19 Aug 2026 23:38:59 +0100 Subject: [PATCH 6/6] Add support for configuring a managed, custom, disable or none label for SELinux --- bin/docker-compose | 16 ++++- bin/doctor | 119 ++++++++++++++++++------------- bin/podman-setup | 52 ++++++++------ bin/up | 21 ++++++ doc/overleaf-rc.md | 21 ++++++ lib/default.rc | 8 +++ lib/docker-compose.selinux.yml | 2 +- lib/shared-functions.sh | 123 ++++++++++++++++++++++++++------- 8 files changed, 260 insertions(+), 102 deletions(-) diff --git a/bin/docker-compose b/bin/docker-compose index 68e658db..d413494e 100755 --- a/bin/docker-compose +++ b/bin/docker-compose @@ -31,9 +31,7 @@ function build_environment() { if [[ "$SIBLING_CONTAINERS_ENABLED" == "true" ]]; then if [[ $SERVER_PRO == "true" ]]; then set_sibling_containers_vars - if use_selinux_label; then - set_selinux_vars - fi + set_selinux_vars else if [[ ${SKIP_WARNINGS:-null} != "true" ]]; then echo "WARNING: SIBLING_CONTAINERS_ENABLED=true is not supported in Overleaf Community Edition." >&2 @@ -157,7 +155,19 @@ function set_sibling_containers_vars() { # 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 diff --git a/bin/doctor b/bin/doctor index 67e8e3e8..6aa792b7 100755 --- a/bin/doctor +++ b/bin/doctor @@ -189,7 +189,7 @@ function check_docker_daemon() { print_point 1 "socket: not found" fi - if [[ "${SERVER_PRO:-false}" == "true" && "${SIBLING_CONTAINERS_ENABLED:-false}" == "true" && -n "$socket_path" ]]; then + 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" @@ -207,38 +207,9 @@ function check_docker_daemon() { fi fi - if [[ ! -S "$socket_path" ]]; then + if [[ -n "$socket_path" ]] && [[ ! -S "$socket_path" ]]; then add_warning "Configured socket path '$socket_path' does not exist or is not a socket" - else - if [[ "$using_podman" == true ]] && command -v getenforce &>/dev/null; then - local enforce - enforce=$(getenforce 2>/dev/null || echo "Disabled") - print_point 1 "SELinux: $enforce" - - if [[ "$enforce" != "Disabled" ]]; then - check_selinux_rules - if [[ "$SELINUX_RULES_CHECKED" == false ]]; then - print_point 1 "SELinux rules: unknown" - elif [[ "$SELINUX_RULES_OK" == true ]]; then - print_point 1 "SELinux rules: present" - else - for rule in "${SELINUX_MISSING_RULES[@]}"; do - print_point 2 "rule $rule not present" - done - add_warning "SELinux is Enforcing 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 - - check_selinux_module - if [[ "$SELINUX_MODULE_LOADED" == true ]]; then - print_point 1 "SELinux module: loaded (podman_socket_clsi)" - elif [[ "$SELINUX_MODULE_CHECKED" == false ]]; then - print_point 1 "SELinux module: unknown" - else - print_point 1 "SELinux module: not loaded" - fi - fi - fi - + elif [[ -S "$socket_path" ]]; then test_socket_ping_host "$socket_path" if [[ "$SOCKET_PING_HOST_OK" == true ]]; then print_point 1 "host → socket: OK" @@ -258,28 +229,77 @@ function check_docker_daemon() { fi fi fi +} - if [[ "$using_podman" == true ]]; then - local seccomp_path - seccomp_path="$(get_seccomp_expected_path)" - check_seccomp_config +function check_security() { + if ! is_podman; then + return + fi - 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'" + 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() { @@ -603,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 index 0bb7851e..53d1e91c 100755 --- a/bin/podman-setup +++ b/bin/podman-setup @@ -417,52 +417,58 @@ check_loaded_selinux_policy() { if [[ "$SELINUX_MODULE_LOADED" == true ]]; then report ok "SELinux module loaded (podman_socket_clsi)" - elif [[ "$SELINUX_MODULE_CHECKED" == false ]]; then - report skip "SELinux module unknown" else report skip "SELinux module not loaded" fi - check_selinux_rules - - if [[ "$SELINUX_RULES_CHECKED" == false ]]; then - report skip "SELinux rules unknown" - [[ "$SELINUX_MODULE_LOADED" == true ]] && return 0 - return 1 - fi + check_selinux_rules "$SELINUX_LABEL" if [[ "$SELINUX_RULES_OK" == true ]]; then report ok "SELinux rules present" return 0 fi - if is_apply; then - for rule in "${SELINUX_MISSING_RULES[@]}"; do - report skip "rule $rule not present" - done - 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" - local enforce="Disabled" - command -v getenforce &>/dev/null && enforce="$(getenforce 2>/dev/null || echo Disabled)" + # 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 - if [[ "$enforce" == "Disabled" ]]; then - report skip "SELinux disabled" + 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 - if [[ "$SELINUX_RULES_CHECKED" == false ]]; then - report warn "SELinux rules unknown, policy module not loaded (use --apply to install it)" - else - report warn "SELinux rules not present (use --apply to install the policy module)" - fi + report warn "SELinux rules not present (use --apply to install the policy module)" return 0 fi diff --git a/bin/up b/bin/up index ccdd0f56..90fee442 100755 --- a/bin/up +++ b/bin/up @@ -87,6 +87,26 @@ function notify_about_podman_limited_support() { 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 @@ -127,6 +147,7 @@ function __main__() { 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/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.selinux.yml b/lib/docker-compose.selinux.yml index 87901173..89a77f75 100644 --- a/lib/docker-compose.selinux.yml +++ b/lib/docker-compose.selinux.yml @@ -1,4 +1,4 @@ services: sharelatex: security_opt: - - "label=type:sharelatex_t" + - "${SELINUX_SECURITY_OPT}" diff --git a/lib/shared-functions.sh b/lib/shared-functions.sh index f6d3454f..b13c498c 100644 --- a/lib/shared-functions.sh +++ b/lib/shared-functions.sh @@ -356,25 +356,29 @@ check_seccomp_config() { } SELINUX_MODULE_NAME="podman_socket_clsi" +SELINUX_MANAGED_LABEL="sharelatex_t" +SELINUX_MODES=(managed custom disable none) SELINUX_RULES=( - "sharelatex_t container_runtime_t unix_stream_socket connectto" - "sharelatex_t user_tmp_t sock_file write" - "sharelatex_t user_tmp_t sock_file getattr" - "sharelatex_t container_runtime_t fifo_file setattr" + "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 - SELINUX_MODULE_CHECKED=true - if ! modules=$($sudo_cmd semodule -l 2>/dev/null); then - SELINUX_MODULE_CHECKED=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 - if grep -q "^${SELINUX_MODULE_NAME}" <<< "$modules"; then + # listing modules needs privileges, the type the module defines is equivalent + if selinux_label_valid "$SELINUX_MANAGED_LABEL"; then SELINUX_MODULE_LOADED=true fi } @@ -402,36 +406,103 @@ selinux_access_allowed() { return 2 } -# Returns 0 if 'label=type:sharelatex_t' can safely be applied: a Server Pro -# podman deployment with sibling containers, and the podman_socket_clsi module loaded -use_selinux_label() { - [[ "${SERVER_PRO:-false}" == "true" ]] || return 1 - [[ "${SIBLING_CONTAINERS_ENABLED:-false}" == "true" ]] || return 1 - is_podman || return 1 +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" - check_selinux_rules - [[ "$SELINUX_RULES_CHECKED" == true && "$SELINUX_RULES_OK" == true ]] + [[ "$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_RULES_CHECKED=true SELINUX_MISSING_RULES=() - local rule src target class perm status + 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 src target class perm <<< "$rule" + read -r target class perm <<< "$rule" selinux_access_allowed "$src" "$target" "$class" "$perm" && continue - status=$? - - if [[ "$status" -eq 2 ]]; then - SELINUX_RULES_CHECKED=false - return 0 - fi SELINUX_RULES_OK=false - SELINUX_MISSING_RULES+=("$rule") + SELINUX_MISSING_RULES+=("$src $rule") done return 0