Make runner setup race-safe and resumable
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:41:20 +03:00
1 parent db6ded4075
commit 4110aa54c9
7 files changed
+66 -14

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
## Не выпущено
- Runner installer отклоняет symlink-компоненты в путях конфигурации и повторно использует хранилище только при активных mount-опциях `nosuid,nodev`.
- Runner installer безопасно повторяет настройку каталогов от имени runner, включая восстановление промежуточных каталогов `0755`, и принимает хранилище только с активными 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`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе.
+8 -4
View File
@@ -57,10 +57,14 @@ 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 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 `/`.
`nodev` active. Runner-home directories and user configuration files are
created or normalized as `akyldash-runner`, never by a privileged file-write
operation; this avoids a symlink swap turning setup into a root write outside
the home. The installer rejects static symlink components and validates
ownership and mode after setup. This also normalizes the `0755` intermediate
directories left by the first installer version. 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
+4 -2
View File
@@ -90,8 +90,10 @@ 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. The reviewed installer now rejects symlinks in runner config
directory paths and requires the active store mount to include `nosuid,nodev`.
VPS changes. The reviewed installer now performs runner-home directory and
config writes as `akyldash-runner` (including recovery from legacy `0755`
intermediate directories), and requires active store mounts 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
+21 -5
View File
@@ -72,15 +72,31 @@ runner_ensure_private_directory() {
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; }
[[ $(stat -c '%U:%G' "$current") == "$owner:$group" ]] || { runner_die "runner directory ancestor has unexpected ownership: $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; }
# These paths are runner-writable; make changes as that user so a path swap
# cannot turn a root operation into a write outside its home.
runuser -u "$owner" -- install -d -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_install_user_content() {
local destination=$1 owner=$2 group=$3 mode=${4#0} temporary
temporary=$(mktemp)
cat >"$temporary"
if [[ -e $destination || -L $destination ]]; then
[[ -f $destination && ! -L $destination ]] && cmp -s "$temporary" "$destination" || { rm -f "$temporary"; runner_die "existing runner file differs from reviewed configuration: $destination"; return 1; }
[[ $(stat -c '%U:%G:%a' "$destination") == "$owner:$group:$mode" ]] || { rm -f "$temporary"; runner_die "existing runner file has unexpected ownership or permissions: $destination"; return 1; }
rm -f "$temporary"
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; }
chown "$owner:$group" "$temporary" && chmod "0$mode" "$temporary" || { rm -f "$temporary"; return 1; }
local result=0
runuser -u "$owner" -- install -m "0$mode" "$temporary" "$destination" || result=$?
rm -f "$temporary"
((result == 0)) || return 1
[[ -f $destination && ! -L $destination && $(stat -c '%U:%G:%a' "$destination") == "$owner:$group:$mode" ]] || { runner_die "runner file failed validation: $destination"; return 1; }
}
runner_validate_mount() {
+2 -2
View File
@@ -113,14 +113,14 @@ else
trap - EXIT
fi
runner_install_content "$runner_home/.config/docker/daemon.json" "$runner_user" "$runner_user" 0600 <<EOF
runner_install_user_content "$runner_home/.config/docker/daemon.json" "$runner_user" "$runner_user" 0600 <<EOF
{
"data-root": "$runner_store",
"log-driver": "local",
"log-opts": {"max-size": "10m", "max-file": "2"}
}
EOF
runner_install_content "$runner_home/.config/systemd/user/docker.service.d/limits.conf" "$runner_user" "$runner_user" 0644 <<'EOF'
runner_install_user_content "$runner_home/.config/systemd/user/docker.service.d/limits.conf" "$runner_user" "$runner_user" 0644 <<'EOF'
[Service]
Delegate=yes
CPUQuota=200%
@@ -46,6 +46,12 @@ chmod +x "$tmp/bin/"*
PATH="$tmp/bin:$PATH"
export PATH
source "$runner_dir/install-runner-lib.sh"
runuser() {
shift 2
[[ $1 == -- ]] || return 2
shift
"$@"
}
getent() {
[[ $1 == passwd ]] || return 2
if [[ -n ${RUNNER_TEST_PASSWD:-} ]]; then
@@ -129,6 +135,28 @@ if runner_ensure_private_directory "$private_root/linked/docker" "$runner_owner"
fi
[[ ! -e $tmp/outside/docker ]]
legacy_root=$tmp/legacy
mkdir -p "$legacy_root/.config/systemd/user/docker.service.d"
chmod 0755 "$legacy_root" "$legacy_root/.config" "$legacy_root/.config/systemd" "$legacy_root/.config/systemd/user" "$legacy_root/.config/systemd/user/docker.service.d"
for legacy_dir in "$legacy_root" "$legacy_root/.config" "$legacy_root/.config/systemd" "$legacy_root/.config/systemd/user" "$legacy_root/.config/systemd/user/docker.service.d"; do
runner_ensure_private_directory "$legacy_dir" "$runner_owner" "$runner_group"
[[ $(stat -c %a "$legacy_dir") == 700 ]]
done
user_config=$private_root/daemon.json
runner_install_user_content "$user_config" "$runner_owner" "$runner_group" 0600 <<'EOF'
{"data-root":"/private/runner"}
EOF
runner_install_user_content "$user_config" "$runner_owner" "$runner_group" 0600 <<'EOF'
{"data-root":"/private/runner"}
EOF
if runner_install_user_content "$user_config" "$runner_owner" "$runner_group" 0600 <<'EOF' 2>/dev/null
{"data-root":"/unexpected"}
EOF
then
echo 'Conflicting runner user config was accepted' >&2
exit 1
fi
image=$tmp/docker-store.img
runner_ensure_image "$image" 1M 1048576
runner_ensure_image "$image" 1M 1048576
+2
View File
@@ -22,6 +22,8 @@ grep -Fq 'fallocate -l "$size" "$image"' infra/runner/install-runner-lib.sh
grep -Fq 'mkfs.ext4 -F -q "$image"' infra/runner/install-runner-lib.sh
grep -Fq 'systemctl enable "$unit_name"' infra/runner/install-runner-lib.sh
grep -Fq 'systemctl start "$unit_name"' infra/runner/install-runner-lib.sh
grep -Fq 'runuser -u "$owner" -- install -d -m 0700 "$directory"' infra/runner/install-runner-lib.sh
grep -Fq 'runner_install_user_content "$runner_home/.config/docker/daemon.json"' "$installer"
assert_absent 'basename.*runner_mount_unit|enable --now.*runner_mount_unit' "$installer"
grep -Fq 'groups=$(id -nG "$user")' infra/runner/install-runner-lib.sh
grep -Fq 'id -gn "$user"' infra/runner/install-runner-lib.sh