Harden runner installer path and mount checks
CI / tests-and-images (pull_request) Has been cancelled
Runner smoke test / runner-smoke (pull_request) Has been cancelled

This commit is contained in:
agent committed 2026-10-12 01:23:03 +03:00
1 parent b68421bb5b
commit db6ded4075
6 files changed
+66 -9

No files matched your search

+1
View File
@@ -2,6 +2,7 @@
## Не выпущено
- Runner installer отклоняет symlink-компоненты в путях конфигурации и повторно использует хранилище только при активных mount-опциях `nosuid,nodev`.
- Backend `0.11.3`: ключ подписи поисковых снимков читается из Docker secret-файла, а не из аргументов процесса. Добавлена базовая инфраструктура VPS, Caddy HTTPS proxy с разделёнными Compose-сетями и CI-проверка конфигурации; публикация образов и production-деплой остаются выключены до проверки runner, registry, backup/restore и ограниченного deploy-helper.
- Frontend `0.1.2`: на планшетной ширине экран поиска переходит в одну колонку, чтобы фильтры, результаты и документ помещались в окно.
- Backend `0.11.2`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе.
+5 -3
View File
@@ -56,9 +56,11 @@ installer without resizing or formatting it. The runner account must retain
a non-root system UID below `UID_MIN`, its expected home, primary group, and
sole supplementary group. The mount
is started only when it is not already active, and an active mount must be the
expected ext4 loop device backed by the runner image. Conflicting files or
state stop the installer for manual inspection. A new image requires at least
25 GiB free on `/`.
expected ext4 loop device backed by the runner image with both `nosuid` and
`nodev` active. Before root creates runner configuration directories, it checks
every path component for symlinks and validates the existing runner-owned
directories' ownership and mode. Conflicting files or state stop the installer
for manual inspection. A new image requires at least 25 GiB free on `/`.
If the reviewed checkout is not present at the inventoried path, first verify
that the target is absent and clone the reviewed `main` there. Do not overwrite
+2 -1
View File
@@ -90,7 +90,8 @@ UFW rule details, and protected file metadata remain unverified.
loop-backed store is active on `/dev/loop0` after manually enabling and
starting its mount unit when the installer stopped. Remaining components,
repository registration, and Online state are unverified; this PR makes no
VPS changes.
VPS changes. The reviewed installer now rejects symlinks in runner config
directory paths and requires the active store mount to include `nosuid,nodev`.
- Gitea repository runner inventory could not be read by the current API
principal (403). The owner has not supplied runner state, and no runner
Online status or runner-hosted workflow is claimed. The manual
+31 -3
View File
@@ -56,11 +56,39 @@ runner_install_content() {
rm -f "$temporary"
}
runner_ensure_private_directory() {
local directory=$1 owner=$2 group=$3 current= component index
local -a components
[[ $directory == /* ]] || { runner_die "runner directory path is not absolute: $directory"; return 1; }
IFS=/ read -r -a components <<<"${directory#/}"
for index in "${!components[@]}"; do
component=${components[index]}
[[ -n $component && $component != . && $component != .. ]] || { runner_die "runner directory path has an unsafe component: $directory"; return 1; }
current+="/$component"
[[ ! -L $current ]] || { runner_die "runner directory path contains a symbolic link: $current"; return 1; }
if [[ ! -e $current ]]; then
((index == ${#components[@]} - 1)) || { runner_die "runner directory parent is missing: $current"; return 1; }
break
fi
[[ -d $current ]] || { runner_die "runner directory path component is not a directory: $current"; return 1; }
if [[ $current == /var/lib/akyldash-runner || $current == /var/lib/akyldash-runner/* ]]; then
[[ $(stat -c '%U:%G:%a' "$current") == "$owner:$group:700" ]] || { runner_die "runner directory ancestor has unexpected ownership or permissions: $current"; return 1; }
fi
done
if [[ -e $directory ]]; then
[[ $(stat -c '%U:%G:%a' "$directory") == "$owner:$group:700" ]] || { runner_die "runner directory has unexpected ownership or permissions: $directory"; return 1; }
return
fi
install -d -o "$owner" -g "$group" -m 0700 "$directory" || return 1
[[ -d $directory && ! -L $directory && $(stat -c '%U:%G:%a' "$directory") == "$owner:$group:700" ]] || { runner_die "runner directory failed validation: $directory"; return 1; }
}
runner_validate_mount() {
local image=$1 store=$2 target filesystem source backing mount_info
mount_info=$(findmnt --noheadings --output TARGET,FSTYPE,SOURCE --mountpoint "$store") || { runner_die 'runner store is not mounted'; return 1; }
read -r target filesystem source <<<"$mount_info" || { runner_die 'runner store mount information is empty'; return 1; }
local image=$1 store=$2 target filesystem source options backing mount_info
mount_info=$(findmnt --noheadings --output TARGET,FSTYPE,SOURCE,OPTIONS --mountpoint "$store") || { runner_die 'runner store is not mounted'; return 1; }
read -r target filesystem source options <<<"$mount_info" || { runner_die 'runner store mount information is empty'; return 1; }
[[ $target == "$store" && $filesystem == ext4 && $source == /dev/loop* ]] || { runner_die 'runner store is mounted from an unexpected target, filesystem, or device'; return 1; }
case ",$options," in *,nosuid,*nodev,*|*,nodev,*nosuid,*) ;; *) runner_die 'runner store mount is missing nosuid or nodev'; return 1 ;; esac
backing=$(losetup --noheadings --output BACK-FILE "$source") || { runner_die 'cannot identify the runner loop backing file'; return 1; }
[[ $(realpath -e "$backing") == "$(realpath -e "$image")" ]] || { runner_die 'runner store is backed by a different image'; return 1; }
}
+6 -1
View File
@@ -80,7 +80,12 @@ if ((${#subid_args[@]})); then
fi
runner_uid=$(id -u "$runner_user")
install -d -o "$runner_user" -g "$runner_user" -m 0700 "$runner_home" "$runner_home/.config/docker" "$runner_home/.config/systemd/user/docker.service.d"
runner_ensure_private_directory "$runner_home" "$runner_user" "$runner_user" || die 'runner home failed directory validation'
runner_ensure_private_directory "$runner_home/.config" "$runner_user" "$runner_user" || die 'runner config directory failed validation'
runner_ensure_private_directory "$runner_home/.config/docker" "$runner_user" "$runner_user" || die 'runner Docker config directory failed validation'
runner_ensure_private_directory "$runner_home/.config/systemd" "$runner_user" "$runner_user" || die 'runner systemd config directory failed validation'
runner_ensure_private_directory "$runner_home/.config/systemd/user" "$runner_user" "$runner_user" || die 'runner user systemd directory failed validation'
runner_ensure_private_directory "$runner_home/.config/systemd/user/docker.service.d" "$runner_user" "$runner_user" || die 'runner Docker override directory failed validation'
if [[ ! -e $runner_store && ! -L $runner_store ]]; then
install -d -o root -g root -m 0755 "$runner_store"
fi
@@ -36,7 +36,7 @@ cat >"$tmp/bin/mountpoint" <<'EOF'
EOF
cat >"$tmp/bin/findmnt" <<'EOF'
#!/usr/bin/env bash
printf '%s ext4 /dev/loop0\n' "$RUNNER_TEST_STORE"
printf '%s ext4 /dev/loop0 %s\n' "$RUNNER_TEST_STORE" "${RUNNER_TEST_OPTIONS:-rw,nosuid,nodev,relatime}"
EOF
cat >"$tmp/bin/losetup" <<'EOF'
#!/usr/bin/env bash
@@ -115,6 +115,20 @@ runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME"
runner_ensure_account akyldash-runner "$RUNNER_TEST_HOME"
[[ $(grep -c '^useradd$' "$log") == 1 ]]
private_root=$tmp/private
mkdir -m 0700 "$private_root"
runner_owner=$(command id -un)
runner_group=$(command id -gn)
runner_ensure_private_directory "$private_root/config" "$runner_owner" "$runner_group"
runner_ensure_private_directory "$private_root/config" "$runner_owner" "$runner_group"
mkdir -m 0700 "$tmp/outside"
ln -s "$tmp/outside" "$private_root/linked"
if runner_ensure_private_directory "$private_root/linked/docker" "$runner_owner" "$runner_group" 2>/dev/null; then
echo 'Runner config symlink was accepted' >&2
exit 1
fi
[[ ! -e $tmp/outside/docker ]]
image=$tmp/docker-store.img
runner_ensure_image "$image" 1M 1048576
runner_ensure_image "$image" 1M 1048576
@@ -179,4 +193,10 @@ runner_ensure_image "$image" 1M 1048576
runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$tmp/systemd/partial.mount"
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
[[ $(grep -c '^start ' "$log") == 3 ]]
export RUNNER_TEST_STORE="$tmp/store" RUNNER_TEST_MOUNTED="$tmp/mounted" RUNNER_TEST_OPTIONS=rw,relatime
if runner_validate_mount "$image" "$RUNNER_TEST_STORE" 2>/dev/null; then
echo 'Runner mount missing nosuid/nodev was accepted' >&2
exit 1
fi
unset RUNNER_TEST_OPTIONS
echo 'runner installer idempotency checks passed'