Harden runner installer path and mount checks
This commit is contained in:
1 parent
b68421bb5b
commit
db6ded4075
6 files changed
+66
-9
No files matched your search
@@ -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`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
Reference in new issue
Block a user