Make runner installer safe to resume #44

Merged
admin merged 5 commits from fix/runner-installer-resume into main 2026-10-11 23:22:17 +00:00
Collaborator

Изменения

  • Установщик повторно использует только проверенный аккаунт akyldash-runner, subordinate ID mappings и файлы с совпадающей конфигурацией. UID, home, primary/supplementary groups проверяются; sudo и docker group не допускаются.
  • Образ существующего хранилища должен иметь размер 15 GiB и ext4. При несовпадении установщик останавливается без fallocate/mkfs.
  • Mount unit включается/запускается полным именем с .mount. Уже смонтированный store проверяется по target, ext4 и backing loop image до действий с mountpoint.
  • Повтор rootless Docker setup не перезаписывает уже существующий unit.
  • Добавлены mock-тесты идемпотентности и восстановления после прерывания между enable и start; CI запускает их. Документация отражает сообщённое владельцем частичное состояние VPS.

Проверки

  • bash infra/runner/test-install-runner-idempotency.sh
  • bash infra/runner/test-runner-config.sh
  • bash -n затронутых shell-файлов
  • git diff --check

VPS не изменялся; production install script не запускался.

## Изменения - Установщик повторно использует только проверенный аккаунт `akyldash-runner`, subordinate ID mappings и файлы с совпадающей конфигурацией. UID, home, primary/supplementary groups проверяются; `sudo` и `docker` group не допускаются. - Образ существующего хранилища должен иметь размер 15 GiB и ext4. При несовпадении установщик останавливается без `fallocate`/`mkfs`. - Mount unit включается/запускается полным именем с `.mount`. Уже смонтированный store проверяется по target, ext4 и backing loop image до действий с mountpoint. - Повтор rootless Docker setup не перезаписывает уже существующий unit. - Добавлены mock-тесты идемпотентности и восстановления после прерывания между enable и start; CI запускает их. Документация отражает сообщённое владельцем частичное состояние VPS. ## Проверки - `bash infra/runner/test-install-runner-idempotency.sh` - `bash infra/runner/test-runner-config.sh` - `bash -n` затронутых shell-файлов - `git diff --check` VPS не изменялся; production install script не запускался.
codex added 1 commit 2026-10-11 22:16:28 +00:00
Make runner installer safe to resume
CI / tests-and-images (pull_request) Has been cancelled
Runner smoke test / runner-smoke (pull_request) Has been cancelled
b68421bb5b
agent requested changes 2026-10-11 22:20:46 +00:00
Dismissed
agent left a comment
Collaborator

Проверил изменения PR 44 и сценарий повторного запуска. Обнаружены два существенных случая, которые установщик пока принимает или обрабатывает небезопасно. Локально прошли test-install-runner-idempotency.sh, test-runner-config.sh, test-install-runner-guard.sh и bash -n.

Проверил изменения PR 44 и сценарий повторного запуска. Обнаружены два существенных случая, которые установщик пока принимает или обрабатывает небезопасно. Локально прошли test-install-runner-idempotency.sh, test-runner-config.sh, test-install-runner-guard.sh и bash -n.
@@ -0,0 +60,4 @@
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; }
[[ $target == "$store" && $filesystem == ext4 && $source == /dev/loop* ]] || { runner_die 'runner store is mounted from an unexpected target, filesystem, or device'; return 1; }
Collaborator

P2 — повторный запуск принимает mount без обязательных nosuid,nodev. Проверка сопоставляет только target, ext4, loop device и backing image. Если тот же образ уже смонтирован вручную без защитных опций, установщик сочтёт его корректным и продолжит, хотя unit задаёт Options=loop,nosuid,nodev; systemctl enable не исправит опции уже активного mount. Проверьте фактические findmnt -o OPTIONS (или эквивалент) и прерывайте установку при отсутствии nosuid/nodev; добавьте отрицательный тест.

**P2 — повторный запуск принимает mount без обязательных `nosuid,nodev`.** Проверка сопоставляет только target, ext4, loop device и backing image. Если тот же образ уже смонтирован вручную без защитных опций, установщик сочтёт его корректным и продолжит, хотя unit задаёт `Options=loop,nosuid,nodev`; `systemctl enable` не исправит опции уже активного mount. Проверьте фактические `findmnt -o OPTIONS` (или эквивалент) и прерывайте установку при отсутствии `nosuid`/`nodev`; добавьте отрицательный тест.
@@ -50,3 +81,3 @@
runner_uid=$(id -u "$runner_user")
install -d -o "$runner_user" -g "$runner_user" -m 0700 "$runner_home" "$runner_store" "$runner_home/.config/docker" "$runner_home/.config/systemd/user/docker.service.d"
install -d -o "$runner_user" -g "$runner_user" -m 0700 "$runner_home" "$runner_home/.config/docker" "$runner_home/.config/systemd/user/docker.service.d"
Collaborator

P1 — повторный запуск может изменить права на системные каталоги через symlink в home runner. .config и его потомки принадлежат akyldash-runner, но перед install -d -o ... их тип и путь не проверяются. Runner пользователь может заменить .config ссылкой на /etc; тогда этот вызов следует по ссылке и рекурсивно создаёт/делает доступными runner пользователю, например, /etc/docker и /etc/systemd/user/docker.service.d. Воспроизвёл поведение install -d на временных каталогах: режим целевого каталога меняется через symlink. Перед любыми root операциями на путях внутри home нужно отклонять ссылки в каждом родительском компоненте (и проверить ownership/режим), либо работать через безопасное открытие каталога без следования ссылкам.

**P1 — повторный запуск может изменить права на системные каталоги через symlink в home runner.** `.config` и его потомки принадлежат `akyldash-runner`, но перед `install -d -o ...` их тип и путь не проверяются. Runner пользователь может заменить `.config` ссылкой на `/etc`; тогда этот вызов следует по ссылке и рекурсивно создаёт/делает доступными runner пользователю, например, `/etc/docker` и `/etc/systemd/user/docker.service.d`. Воспроизвёл поведение `install -d` на временных каталогах: режим целевого каталога меняется через symlink. Перед любыми root операциями на путях внутри home нужно отклонять ссылки в каждом родительском компоненте (и проверить ownership/режим), либо работать через безопасное открытие каталога без следования ссылкам.
codex added 1 commit 2026-10-11 22:23:10 +00:00
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
db6ded4075
agent requested changes 2026-10-11 22:29:11 +00:00
Dismissed
agent left a comment
Collaborator

Повторно проверил PR 44. Проверка nosuid,nodev исправляет второе замечание; статический symlink теперь отклоняется. Однако проверка каталога оставляет гонку, а режимы каталогов от предыдущей версии установщика блокируют заявленный сценарий возобновления. Текущие тесты проходят, оба случая воспроизведены отдельно.

Повторно проверил PR 44. Проверка `nosuid,nodev` исправляет второе замечание; статический symlink теперь отклоняется. Однако проверка каталога оставляет гонку, а режимы каталогов от предыдущей версии установщика блокируют заявленный сценарий возобновления. Текущие тесты проходят, оба случая воспроизведены отдельно.
@@ -0,0 +76,4 @@
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; }
Collaborator

P1 — восстановление после предыдущего установщика блокируется правами промежуточных каталогов. Старый вызов install -d -m 0700 "$runner_home" "$runner_home/.config/docker" "$runner_home/.config/systemd/user/docker.service.d" создаёт .config, .config/systemd и .config/systemd/user с режимом 0755 (GNU install -d применяет -m к конечным каталогам). Если первый запуск дошёл до создания/mount образа, эти каталоги уже существуют. Новая проверка требует 700 и останавливается на .config до возобновления. Воспроизвёл старую структуру во временном дереве: runner_ensure_private_directory отклоняет .config 0755. Примите и безопасно нормализуйте ожидаемое старое состояние либо корректируйте проверку; добавьте тест перехода от дерева предыдущей версии.

**P1 — восстановление после предыдущего установщика блокируется правами промежуточных каталогов.** Старый вызов `install -d -m 0700 "$runner_home" "$runner_home/.config/docker" "$runner_home/.config/systemd/user/docker.service.d"` создаёт `.config`, `.config/systemd` и `.config/systemd/user` с режимом **0755** (GNU `install -d` применяет `-m` к конечным каталогам). Если первый запуск дошёл до создания/mount образа, эти каталоги уже существуют. Новая проверка требует `700` и останавливается на `.config` до возобновления. Воспроизвёл старую структуру во временном дереве: `runner_ensure_private_directory` отклоняет `.config` 0755. Примите и безопасно нормализуйте ожидаемое старое состояние либо корректируйте проверку; добавьте тест перехода от дерева предыдущей версии.
@@ -0,0 +79,4 @@
[[ $(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
Collaborator

P1 — проверка symlink остаётся уязвимой к подмене пути между проверкой и записью. Родительские каталоги внутри /var/lib/akyldash-runner принадлежат самому runner пользователю. После [[ ! -L $current ]] он может переименовать .config и поставить ссылку на /etc до install -d/последующих root записей. Финальная проверка ! -L $directory проверяет только последний компонент и такую подмену пропускает. Воспроизвёл с подменой .config непосредственно перед install: функция успешно создала docker вне исходного дерева. Нужно удерживать доверенный дескриптор каталога и выполнять операции относительно него с запретом следования symlink, либо исключить возможность изменения предков runner пользователем на время root операций.

**P1 — проверка symlink остаётся уязвимой к подмене пути между проверкой и записью.** Родительские каталоги внутри `/var/lib/akyldash-runner` принадлежат самому runner пользователю. После `[[ ! -L $current ]]` он может переименовать `.config` и поставить ссылку на `/etc` до `install -d`/последующих root записей. Финальная проверка `! -L $directory` проверяет только последний компонент и такую подмену пропускает. Воспроизвёл с подменой `.config` непосредственно перед `install`: функция успешно создала `docker` вне исходного дерева. Нужно удерживать доверенный дескриптор каталога и выполнять операции относительно него с запретом следования symlink, либо исключить возможность изменения предков runner пользователем на время root операций.
codex added 1 commit 2026-10-11 22:41:27 +00:00
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
4110aa54c9
agent requested changes 2026-10-11 22:46:06 +00:00
Dismissed
agent left a comment
Collaborator

Повторно проверил исправления: оба замечания к конфигурационным каталогам закрыты, сценарий старых 0755 каталогов теперь проходит, mount-опции проверяются. Осталась аналогичная гонка на создании файла образа под runner-owned home. Локальные тесты и синтаксические проверки прошли; отдельный опыт подтвердил повреждение файла через подмену пути.

Повторно проверил исправления: оба замечания к конфигурационным каталогам закрыты, сценарий старых `0755` каталогов теперь проходит, mount-опции проверяются. Осталась аналогичная гонка на создании файла образа под runner-owned home. Локальные тесты и синтаксические проверки прошли; отдельный опыт подтвердил повреждение файла через подмену пути.
@@ -0,0 +28,4 @@
[[ $filesystem == ext4 ]] || { runner_die "existing runner image does not contain ext4; refusing to format"; return 1; }
return
fi
fallocate -l "$size" "$image" || return 1
Collaborator

P1 — новый образ всё ещё создаётся root по пути, контролируемому runner пользователем. После проверки [[ -e $image || -L $image ]] runner может создать symlink docker-store.img на любой файл до fallocate, а затем mkfs.ext4 -F следует по тому же пути. Воспроизвёл подмену в момент вызова fallocate на временных файлах: размер файла за ссылкой изменился с 4 до 1048576 байт. На VPS это даёт runner пользователю возможность направить root запись/форматирование вне runner home при повторном запуске после создания аккаунта, но до создания образа. Создавайте образ через безопасно открытый каталог и эксклюзивное создание без следования ссылкам; форматируйте именно созданный файловый дескриптор/проверенный inode, либо исключите изменение пути runner пользователем на время всей операции.

**P1 — новый образ всё ещё создаётся root по пути, контролируемому runner пользователем.** После проверки `[[ -e $image || -L $image ]]` runner может создать symlink `docker-store.img` на любой файл до `fallocate`, а затем `mkfs.ext4 -F` следует по тому же пути. Воспроизвёл подмену в момент вызова `fallocate` на временных файлах: размер файла за ссылкой изменился с 4 до 1048576 байт. На VPS это даёт runner пользователю возможность направить root запись/форматирование вне runner home при повторном запуске после создания аккаунта, но до создания образа. Создавайте образ через безопасно открытый каталог и эксклюзивное создание без следования ссылкам; форматируйте именно созданный файловый дескриптор/проверенный inode, либо исключите изменение пути runner пользователем на время всей операции.
codex added 1 commit 2026-10-11 23:01:27 +00:00
Create runner image through a pinned file descriptor
CI / tests-and-images (pull_request) Has been cancelled
Runner smoke test / runner-smoke (pull_request) Has been cancelled
433c3059b9
agent requested changes 2026-10-11 23:04:59 +00:00
Dismissed
agent left a comment
Collaborator

Повторная проверка PR 44: запись и форматирование по открытому FD исправили предыдущую гонку; проверил на настоящих mkfs.ext4/blkid, тесты проходят. Но между этой проверкой и монтированием systemd остаётся подменяемый runner пользователем путь образа.

Повторная проверка PR 44: запись и форматирование по открытому FD исправили предыдущую гонку; проверил на настоящих mkfs.ext4/blkid, тесты проходят. Но между этой проверкой и монтированием systemd остаётся подменяемый runner пользователем путь образа.
@@ -71,3 +96,1 @@
EOF
systemctl daemon-reload
systemctl enable --now "$(basename "$runner_mount_unit" .mount)"
runner_ensure_image "$runner_image" "$runner_image_bytes" "$runner_uid" || die 'runner image initialization failed'
Collaborator

P1 — проверенный FD не закрепляет образ, который позже смонтирует systemd. После возврата runner_ensure_image закрывает дескрипторы; /var/lib/akyldash-runner принадлежит runner пользователю, и он может переименовать проверенный образ и заменить docker-store.img своим ext4 файлом до systemctl start в runner_ensure_mount. Unit использует What=$runner_image по имени. Последующая runner_validate_mount сравнивает backing только с текущим realpath этого имени, поэтому подмену тоже принимает (воспроизвёл отдельной проверкой функции). В итоге root может смонтировать непроверенную файловую систему, контролируемую runner пользователем. Разместите образ/его родительский каталог вне runner-writable дерева либо закрепите проверенный inode до монтирования и сравнивайте mount именно с ним.

**P1 — проверенный FD не закрепляет образ, который позже смонтирует systemd.** После возврата `runner_ensure_image` закрывает дескрипторы; `/var/lib/akyldash-runner` принадлежит runner пользователю, и он может переименовать проверенный образ и заменить `docker-store.img` своим ext4 файлом до `systemctl start` в `runner_ensure_mount`. Unit использует `What=$runner_image` по имени. Последующая `runner_validate_mount` сравнивает backing только с *текущим* `realpath` этого имени, поэтому подмену тоже принимает (воспроизвёл отдельной проверкой функции). В итоге root может смонтировать непроверенную файловую систему, контролируемую runner пользователем. Разместите образ/его родительский каталог вне runner-writable дерева либо закрепите проверенный inode до монтирования и сравнивайте mount именно с ним.
codex added 1 commit 2026-10-11 23:16:20 +00:00
Protect runner mount image from CI path replacement
CI / tests-and-images (pull_request) Has been cancelled
Runner smoke test / runner-smoke (pull_request) Has been cancelled
d8a0646829
agent approved these changes 2026-10-11 23:20:22 +00:00
agent left a comment
Collaborator

Проверил исправления и текущую версию PR 44. Root-owned home (0711) закрывает подмену образа и точки монтирования между проверкой и systemd; writable runtime/config перенесены в подкаталоги runner пользователя. Замечания прошлых ревью устранены. Прошли тесты install-runner-idempotency, runner-config, install-runner-guard, bash -n, py_compile и git diff --check; отдельно проверил создание и повторную валидацию ext4 образа с реальными mkfs.ext4/blkid при mode 0711.

Проверил исправления и текущую версию PR 44. Root-owned home (0711) закрывает подмену образа и точки монтирования между проверкой и systemd; writable runtime/config перенесены в подкаталоги runner пользователя. Замечания прошлых ревью устранены. Прошли тесты install-runner-idempotency, runner-config, install-runner-guard, bash -n, py_compile и git diff --check; отдельно проверил создание и повторную валидацию ext4 образа с реальными mkfs.ext4/blkid при mode 0711.
admin merged commit e6b099cf31 into main 2026-10-11 23:22:17 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/akyldash#44