From 00922e6c09d975c639dc75eea0a8efdb352cdb8e Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 12 Oct 2026 22:18:51 +0300 Subject: [PATCH 1/3] fix: make rootless Docker home writable --- docs/operations/CI_RUNNER.md | 5 +++++ docs/operations/CURRENT_STATE.md | 4 ++++ infra/runner/act-runner-healthcheck.sh | 5 ++++- infra/runner/install-runner.sh | 19 ++++++++++++++----- infra/runner/test-runner-config.sh | 13 ++++++++++++- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/operations/CI_RUNNER.md b/docs/operations/CI_RUNNER.md index 7f0e311..38890e4 100644 --- a/docs/operations/CI_RUNNER.md +++ b/docs/operations/CI_RUNNER.md @@ -20,6 +20,11 @@ dedicated ext4 loop-backed Docker data filesystem. The runner account's home directory is root-owned mode `0711`, so CI cannot rename the image or mountpoint entries. Docker config remains under `.config`; registration state and the runner workspace live in `.config/act-runner`, which is runner-owned. The +rootless Docker setup and user service use `.config/act-runner` as writable +`HOME`, `.config` as `XDG_CONFIG_HOME`, and +`.config/act-runner/.local/share` as `XDG_DATA_HOME`. This keeps Docker config +and its systemd user unit in the existing `.config` tree without granting +write access to the protected home. rootless Docker daemon and runner user are separate from production Docker; `codex` is not added to sudo or Docker groups. Jobs do not receive the rootless Docker socket, the diff --git a/docs/operations/CURRENT_STATE.md b/docs/operations/CURRENT_STATE.md index de49f06..5e0c047 100644 --- a/docs/operations/CURRENT_STATE.md +++ b/docs/operations/CURRENT_STATE.md @@ -95,6 +95,10 @@ UFW rule details, and protected file metadata remain unverified. and runtime state remain in runner-owned subdirectories. It reuses legacy `0755` config directories, creates the image through a pinned file descriptor, rejects path replacement, and requires mount options `nosuid,nodev`. + The reported installation blocker is that rootless setup receives this + protected directory as `HOME`; the fix prepared in this PR assigns writable + runner-owned HOME/XDG paths and checks them before setup. It has not been + merged or run on the VPS. - 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 diff --git a/infra/runner/act-runner-healthcheck.sh b/infra/runner/act-runner-healthcheck.sh index 3c0b687..4c7da97 100644 --- a/infra/runner/act-runner-healthcheck.sh +++ b/infra/runner/act-runner-healthcheck.sh @@ -2,6 +2,9 @@ set -Eeuo pipefail systemctl is-active --quiet akyldash-act-runner.service runner_uid=$(id -u akyldash-runner) -runuser -u akyldash-runner -- env HOME=/var/lib/akyldash-runner \ +runuser -u akyldash-runner -- env \ + HOME=/var/lib/akyldash-runner/.config/act-runner \ + XDG_CONFIG_HOME=/var/lib/akyldash-runner/.config \ + XDG_DATA_HOME=/var/lib/akyldash-runner/.config/act-runner/.local/share \ DOCKER_HOST="unix:///run/user/$runner_uid/docker.sock" \ /usr/bin/docker info --format '{{.ServerVersion}}' >/dev/null diff --git a/infra/runner/install-runner.sh b/infra/runner/install-runner.sh index f170144..bae4922 100644 --- a/infra/runner/install-runner.sh +++ b/infra/runner/install-runner.sh @@ -4,6 +4,8 @@ set -Eeuo pipefail readonly runner_user=akyldash-runner readonly runner_home=/var/lib/akyldash-runner readonly runner_runtime_home="$runner_home/.config/act-runner" +readonly runner_xdg_config_home="$runner_home/.config" +readonly runner_xdg_data_home="$runner_runtime_home/.local/share" readonly runner_store="$runner_home/docker-data" readonly runner_image="$runner_home/docker-store.img" readonly runner_mount_unit_name="$(systemd-escape --path --suffix=mount /var/lib/akyldash-runner/docker-data)" @@ -92,6 +94,8 @@ runner_ensure_private_directory "$runner_home/.config/systemd" "$runner_user" "$ 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' runner_ensure_private_directory "$runner_runtime_home" "$runner_user" "$runner_user" || die 'runner runtime home failed directory validation' +runner_ensure_private_directory "$runner_runtime_home/.local" "$runner_user" "$runner_user" || die 'runner XDG data parent failed directory validation' +runner_ensure_private_directory "$runner_xdg_data_home" "$runner_user" "$runner_user" || die 'runner XDG data directory failed directory validation' if [[ -e $runner_home/.runner || -L $runner_home/.runner ]]; then [[ -f $runner_home/.runner && ! -L $runner_home/.runner ]] || die 'legacy runner registration state is not a regular file' if [[ ! -e $runner_runtime_home/.runner && ! -L $runner_runtime_home/.runner ]]; then @@ -154,6 +158,8 @@ Slice=user-$runner_uid.slice User=$runner_user Group=$runner_user Environment=HOME=$runner_runtime_home +Environment=XDG_CONFIG_HOME=$runner_xdg_config_home +Environment=XDG_DATA_HOME=$runner_xdg_data_home WorkingDirectory=$runner_runtime_home Environment=XDG_RUNTIME_DIR=/run/user/$runner_uid Environment=DOCKER_HOST=unix:///run/user/$runner_uid/docker.sock @@ -172,7 +178,7 @@ ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes RestrictSUIDSGID=yes -ReadWritePaths=$runner_runtime_home /var/log/akyldash-runner /run/user/$runner_uid +ReadWritePaths=$runner_runtime_home $runner_xdg_config_home /var/log/akyldash-runner /run/user/$runner_uid StandardOutput=append:/var/log/akyldash-runner/act_runner.log StandardError=append:/var/log/akyldash-runner/act_runner.log @@ -205,14 +211,17 @@ runner_install_file "$(dirname "$0")/akyldash-runner-restart.service" /etc/syste loginctl enable-linger "$runner_user" systemctl daemon-reload systemctl start "user@$runner_uid.service" -rootless_docker_unit="$runner_home/.config/systemd/user/docker.service" +rootless_docker_unit="$runner_xdg_config_home/systemd/user/docker.service" +rootless_env=(HOME="$runner_runtime_home" XDG_CONFIG_HOME="$runner_xdg_config_home" XDG_DATA_HOME="$runner_xdg_data_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus") +runuser -u "$runner_user" -- env "${rootless_env[@]}" sh -c 'test -w "$HOME" && test -w "$XDG_CONFIG_HOME" && test -w "$XDG_DATA_HOME"' || die 'rootless Docker HOME and XDG directories must be writable by the runner' +runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user set-environment "HOME=$runner_runtime_home" "XDG_CONFIG_HOME=$runner_xdg_config_home" "XDG_DATA_HOME=$runner_xdg_data_home" if [[ -e $rootless_docker_unit || -L $rootless_docker_unit ]]; then [[ -f $rootless_docker_unit && ! -L $rootless_docker_unit && $(stat -c %U "$rootless_docker_unit") == "$runner_user" ]] || die 'existing rootless Docker unit has an unexpected type or owner' grep -Fq 'dockerd-rootless.sh' "$rootless_docker_unit" || die 'existing rootless Docker unit differs from the expected service' else - runuser -u "$runner_user" -- env HOME="$runner_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus" dockerd-rootless-setuptool.sh install + runuser -u "$runner_user" -- env "${rootless_env[@]}" dockerd-rootless-setuptool.sh install fi -runuser -u "$runner_user" -- env HOME="$runner_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus" systemctl --user daemon-reload -runuser -u "$runner_user" -- env HOME="$runner_home" XDG_RUNTIME_DIR="/run/user/$runner_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$runner_uid/bus" systemctl --user enable --now docker.service +runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user daemon-reload +runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user enable --now docker.service printf '\nRunner runtime installed. Register it interactively, then enable its services as described in docs/operations/CI_RUNNER.md.\n' diff --git a/infra/runner/test-runner-config.sh b/infra/runner/test-runner-config.sh index 268d259..1d2256a 100644 --- a/infra/runner/test-runner-config.sh +++ b/infra/runner/test-runner-config.sh @@ -19,8 +19,19 @@ assert_absent 'var/run/docker.sock|metrics:|health_check:|production.env|review_ grep -Fq 'docker-ce-rootless-extras' "$installer" grep -Fq 'runner_ensure_image "$runner_image" "$runner_image_bytes" 0 711' "$installer" grep -Fq 'runner_lock_home "$runner_home"' "$installer" +grep -Fq 'test -w "$HOME" && test -w "$XDG_CONFIG_HOME" && test -w "$XDG_DATA_HOME"' "$installer" +grep -Fq 'rootless_env=(HOME="$runner_runtime_home" XDG_CONFIG_HOME="$runner_xdg_config_home" XDG_DATA_HOME="$runner_xdg_data_home"' "$installer" +grep -Fq 'Environment=XDG_CONFIG_HOME=$runner_xdg_config_home' "$installer" +grep -Fq 'Environment=XDG_DATA_HOME=$runner_xdg_data_home' "$installer" +grep -Fq 'HOME=/var/lib/akyldash-runner/.config/act-runner' infra/runner/act-runner-healthcheck.sh +grep -Fq 'XDG_CONFIG_HOME=/var/lib/akyldash-runner/.config' infra/runner/act-runner-healthcheck.sh +grep -Fq 'XDG_DATA_HOME=/var/lib/akyldash-runner/.config/act-runner/.local/share' infra/runner/act-runner-healthcheck.sh +if grep -Fq 'env HOME="$runner_home"' "$installer"; then + echo 'Protected runner home must not be passed as rootless Docker HOME' >&2 + exit 1 +fi grep -Fq 'die '\''stop akyldash-act-runner.service after all jobs finish before rerunning this installer' "$installer" -grep -Fq 'ReadWritePaths=$runner_runtime_home /var/log/akyldash-runner /run/user/$runner_uid' "$installer" +grep -Fq 'ReadWritePaths=$runner_runtime_home $runner_xdg_config_home /var/log/akyldash-runner /run/user/$runner_uid' "$installer" grep -Fq 'os.O_NOFOLLOW | os.O_CLOEXEC' infra/runner/secure-runner-image.py grep -Fq 'os.posix_fallocate(image_fd, 0, int(bytes_text))' infra/runner/secure-runner-image.py grep -Fq 'pass_fds=(image_fd,)' infra/runner/secure-runner-image.py -- 2.54.0 From 03c5809d6986fa28937aa0ff0f16b2fc62bdd03a Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 14 Oct 2026 11:48:57 +0300 Subject: [PATCH 2/3] fix: persist rootless Docker unit environment --- CHANGELOG.md | 1 + docs/operations/CURRENT_STATE.md | 7 ++++--- infra/runner/install-runner.sh | 11 ++++++++++- infra/runner/test-runner-config.sh | 7 +++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd63b3..83c6eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Не выпущено +- Runner installer сохраняет `HOME` и XDG-пути в user-unit `docker.service` и проверяет применённое окружение после запуска. - Runner installer защищает image и mountpoint root-owned home-каталогом, оставляет регистрацию и workspace в runner-owned каталоге, безопасно форматирует образ по открытому FD и проверяет `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`: на планшетной ширине экран поиска переходит в одну колонку, чтобы фильтры, результаты и документ помещались в окно. diff --git a/docs/operations/CURRENT_STATE.md b/docs/operations/CURRENT_STATE.md index 5e0c047..dd0120d 100644 --- a/docs/operations/CURRENT_STATE.md +++ b/docs/operations/CURRENT_STATE.md @@ -96,9 +96,10 @@ UFW rule details, and protected file metadata remain unverified. `0755` config directories, creates the image through a pinned file descriptor, rejects path replacement, and requires mount options `nosuid,nodev`. The reported installation blocker is that rootless setup receives this - protected directory as `HOME`; the fix prepared in this PR assigns writable - runner-owned HOME/XDG paths and checks them before setup. It has not been - merged or run on the VPS. + protected directory as `HOME`; the installer assigns writable runner-owned + HOME/XDG paths, persists them in the `docker.service` user-unit drop-in, and + checks the effective unit environment after startup. Local runner guards pass; + the change has not been run on the VPS. - 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 diff --git a/infra/runner/install-runner.sh b/infra/runner/install-runner.sh index bae4922..59ffe2e 100644 --- a/infra/runner/install-runner.sh +++ b/infra/runner/install-runner.sh @@ -145,6 +145,12 @@ TasksMax=1024 Restart=always RestartSec=5 EOF +runner_install_user_content "$runner_home/.config/systemd/user/docker.service.d/environment.conf" "$runner_user" "$runner_user" 0644 <&2 + exit 1 +fi grep -Fq 'HOME=/var/lib/akyldash-runner/.config/act-runner' infra/runner/act-runner-healthcheck.sh grep -Fq 'XDG_CONFIG_HOME=/var/lib/akyldash-runner/.config' infra/runner/act-runner-healthcheck.sh grep -Fq 'XDG_DATA_HOME=/var/lib/akyldash-runner/.config/act-runner/.local/share' infra/runner/act-runner-healthcheck.sh -- 2.54.0 From 9ec03200fb7d70865ba7a38600a85baada159ac0 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 14 Oct 2026 17:41:24 +0300 Subject: [PATCH 3/3] fix: restart rootless Docker with updated environment --- CHANGELOG.md | 2 +- docs/operations/CURRENT_STATE.md | 3 ++- infra/runner/install-runner.sh | 13 ++++++++++--- infra/runner/test-runner-config.sh | 5 ++++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c6eb7..b8f1394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Не выпущено -- Runner installer сохраняет `HOME` и XDG-пути в user-unit `docker.service` и проверяет применённое окружение после запуска. +- Runner installer сохраняет `HOME` и XDG-пути в user-unit `docker.service`, перезапускает активный daemon и проверяет окружение его процесса. - Runner installer защищает image и mountpoint root-owned home-каталогом, оставляет регистрацию и workspace в runner-owned каталоге, безопасно форматирует образ по открытому FD и проверяет `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`: на планшетной ширине экран поиска переходит в одну колонку, чтобы фильтры, результаты и документ помещались в окно. diff --git a/docs/operations/CURRENT_STATE.md b/docs/operations/CURRENT_STATE.md index dd0120d..e65ef8e 100644 --- a/docs/operations/CURRENT_STATE.md +++ b/docs/operations/CURRENT_STATE.md @@ -98,7 +98,8 @@ UFW rule details, and protected file metadata remain unverified. The reported installation blocker is that rootless setup receives this protected directory as `HOME`; the installer assigns writable runner-owned HOME/XDG paths, persists them in the `docker.service` user-unit drop-in, and - checks the effective unit environment after startup. Local runner guards pass; + restarts an active daemon and checks the running process environment. Local + runner guards pass; the change has not been run on the VPS. - Gitea repository runner inventory could not be read by the current API principal (403). The owner has not supplied runner state, and no runner diff --git a/infra/runner/install-runner.sh b/infra/runner/install-runner.sh index 59ffe2e..e8fe971 100644 --- a/infra/runner/install-runner.sh +++ b/infra/runner/install-runner.sh @@ -227,10 +227,17 @@ else runuser -u "$runner_user" -- env "${rootless_env[@]}" dockerd-rootless-setuptool.sh install fi runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user daemon-reload -runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user enable --now docker.service -docker_environment=$(runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user show docker.service --property=Environment --value) +runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user enable docker.service +if runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user is-active --quiet docker.service; then + runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user restart docker.service +else + runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user start docker.service +fi +docker_pid=$(runuser -u "$runner_user" -- env "${rootless_env[@]}" systemctl --user show docker.service --property=MainPID --value) +[[ $docker_pid =~ ^[1-9][0-9]*$ && -r /proc/$docker_pid/environ ]] || die 'rootless Docker process environment is unavailable' +docker_environment=$(tr '\0' '\n' <"/proc/$docker_pid/environ") || die 'could not read rootless Docker process environment' for expected in "HOME=$runner_runtime_home" "XDG_CONFIG_HOME=$runner_xdg_config_home" "XDG_DATA_HOME=$runner_xdg_data_home"; do - [[ " $docker_environment " == *" $expected "* ]] || die "rootless Docker unit is missing $expected" + grep -Fqx -- "$expected" <<<"$docker_environment" || die "running rootless Docker process is missing $expected" done printf '\nRunner runtime installed. Register it interactively, then enable its services as described in docs/operations/CI_RUNNER.md.\n' diff --git a/infra/runner/test-runner-config.sh b/infra/runner/test-runner-config.sh index b561e4d..b1267aa 100644 --- a/infra/runner/test-runner-config.sh +++ b/infra/runner/test-runner-config.sh @@ -25,7 +25,10 @@ grep -Fq 'Environment=XDG_CONFIG_HOME=$runner_xdg_config_home' "$installer" grep -Fq 'Environment=XDG_DATA_HOME=$runner_xdg_data_home' "$installer" grep -Fq 'runner_home/.config/systemd/user/docker.service.d/environment.conf' "$installer" grep -Fq 'Environment=HOME=$runner_runtime_home' "$installer" -grep -Fq 'systemctl --user show docker.service --property=Environment --value' "$installer" +grep -Fq 'systemctl --user restart docker.service' "$installer" +grep -Fq 'systemctl --user show docker.service --property=MainPID --value' "$installer" +grep -Fq 'tr '\''\0'\'' '\''\n'\'' <"/proc/$docker_pid/environ"' "$installer" +grep -Fq 'grep -Fqx -- "$expected"' "$installer" if grep -Fq 'systemctl --user set-environment' "$installer"; then echo 'Rootless Docker environment must persist in its user unit drop-in' >&2 exit 1 -- 2.54.0