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

This commit is contained in:
agent committed 2026-10-12 02:01:19 +03:00
1 parent 4110aa54c9
commit 433c3059b9
9 files changed
+153 -41

No files matched your search

+1
View File
@@ -44,6 +44,7 @@ jobs:
infra/runner/install-runner.sh infra/runner/install-runner-lib.sh \
infra/runner/act-runner-healthcheck.sh infra/runner/test-runner-config.sh \
infra/runner/test-install-runner-idempotency.sh infra/runner/test-install-runner-guard.sh
python3 -c "import ast,pathlib; ast.parse(pathlib.Path('infra/runner/secure-runner-image.py').read_text())"
- name: Run rollback helper tests
run: python3 -m unittest discover -s infra/deploy -p 'test_*.py' -v
- name: Validate production Compose and Caddyfile
+1 -1
View File
@@ -2,7 +2,7 @@
## Не выпущено
- Runner installer безопасно повторяет настройку каталогов от имени runner, включая восстановление промежуточных каталогов `0755`, и принимает хранилище только с активными 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`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе.
+4 -1
View File
@@ -64,7 +64,10 @@ 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 `/`.
free on `/`. Image creation opens each parent component without following
symlinks, creates the image exclusively relative to the opened directory, and
passes the open file descriptor to `mkfs.ext4`; validation checks the same inode
and rejects path replacement.
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
@@ -92,7 +92,8 @@ UFW rule details, and protected file metadata remain unverified.
repository registration, and Online state are unverified; this PR makes no
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
intermediate directories). Image initialization uses a safely opened file
descriptor and rejects path replacement; active store mounts must 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
+2 -14
View File
@@ -19,20 +19,8 @@ runner_ensure_account() {
}
runner_ensure_image() {
local image=$1 size=$2 bytes=$3 actual filesystem
if [[ -e $image || -L $image ]]; then
[[ -f $image && ! -L $image ]] || { runner_die "existing runner image is not a regular file: $image"; return 1; }
actual=$(stat -c %s "$image") || return
[[ $actual == "$bytes" ]] || { runner_die "existing runner image has size $actual bytes; expected $bytes; refusing to resize or format"; return 1; }
filesystem=$(blkid -p -s TYPE -o value "$image" 2>/dev/null || true)
[[ $filesystem == ext4 ]] || { runner_die "existing runner image does not contain ext4; refusing to format"; return 1; }
return
fi
fallocate -l "$size" "$image" || return 1
mkfs.ext4 -F -q "$image" || return 1
actual=$(stat -c %s "$image") || return
filesystem=$(blkid -p -s TYPE -o value "$image" 2>/dev/null || true)
[[ $actual == "$bytes" && $filesystem == ext4 ]] || { runner_die 'new runner image failed size or ext4 verification'; return 1; }
local image=$1 bytes=$2 parent_uid=$3
python3 "$(dirname "${BASH_SOURCE[0]}")/secure-runner-image.py" "$image" "$bytes" "$parent_uid" || { runner_die "runner image validation or initialization failed: $image"; return 1; }
}
runner_install_file() {
+2 -2
View File
@@ -38,7 +38,7 @@ else
[[ ! -e $runner_home && ! -L $runner_home ]] || die 'runner home exists without its account; inspect it before continuing'
fi
if [[ -e $runner_image || -L $runner_image ]]; then
runner_ensure_image "$runner_image" 15G "$runner_image_bytes" || die 'runner image validation failed'
runner_ensure_image "$runner_image" "$runner_image_bytes" "$(id -u "$runner_user")" || die 'runner image validation failed'
else
[[ $(df -Pk / | awk 'NR==2 {print $4}') -ge 25000000 ]] || die 'need at least 25 GiB free before reserving 15 GiB for runner data'
fi
@@ -93,7 +93,7 @@ fi
install -d -o root -g "$runner_user" -m 0750 /etc/akyldash-runner
install -d -o "$runner_user" -g "$runner_user" -m 0750 /var/log/akyldash-runner
runner_ensure_image "$runner_image" 15G "$runner_image_bytes" || die 'runner image initialization failed'
runner_ensure_image "$runner_image" "$runner_image_bytes" "$runner_uid" || die 'runner image initialization failed'
runner_ensure_mount "$runner_image" "$runner_store" "$runner_mount_unit_name" "$runner_mount_unit" || die 'runner mount activation failed'
chown "$runner_user:$runner_user" "$runner_store"
chmod 0700 "$runner_store"
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Create or validate the runner image without following replaceable paths."""
import os
import stat
import subprocess
import sys
def open_directory(path: str) -> int:
current = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
try:
for component in path.strip("/").split("/"):
if not component:
continue
next_fd = os.open(
component,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=current,
)
os.close(current)
current = next_fd
return current
except BaseException:
os.close(current)
raise
def filesystem_type(fd: int) -> str:
result = subprocess.run(
["blkid", "-p", "-s", "TYPE", "-o", "value", f"/proc/self/fd/{fd}"],
check=False,
capture_output=True,
text=True,
pass_fds=(fd,),
)
return result.stdout.strip() if result.returncode == 0 else ""
def main() -> int:
image, bytes_text, parent_uid_text = sys.argv[1:]
image = os.path.abspath(image)
parent_path, name = os.path.split(image)
if not name or name in {".", ".."}:
raise ValueError("invalid image filename")
parent_fd = open_directory(parent_path)
image_fd = -1
try:
parent_stat = os.fstat(parent_fd)
if parent_stat.st_uid != int(parent_uid_text) or stat.S_IMODE(parent_stat.st_mode) != 0o700:
raise RuntimeError("image parent must be the runner-owned mode 0700 directory")
created = False
try:
image_fd = os.open(
name,
os.O_RDWR | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=parent_fd,
)
except FileNotFoundError:
image_fd = os.open(
name,
os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC,
0o600,
dir_fd=parent_fd,
)
created = True
image_stat = os.fstat(image_fd)
if not stat.S_ISREG(image_stat.st_mode) or image_stat.st_uid != os.geteuid() or image_stat.st_nlink != 1:
raise RuntimeError("image must be a singly-linked regular file owned by the installer")
if not created and image_stat.st_mode & 0o022:
raise RuntimeError("existing image is writable by group or other users")
if created:
os.posix_fallocate(image_fd, 0, int(bytes_text))
subprocess.run(
["mkfs.ext4", "-F", "-q", f"/proc/self/fd/{image_fd}"],
check=True,
pass_fds=(image_fd,),
)
image_stat = os.fstat(image_fd)
if image_stat.st_size != int(bytes_text):
raise RuntimeError(f"image size is {image_stat.st_size} bytes, expected {bytes_text}; refusing to resize or format")
if filesystem_type(image_fd) != "ext4":
raise RuntimeError("image does not contain ext4; refusing to format")
path_stat = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
if not stat.S_ISREG(path_stat.st_mode) or (path_stat.st_dev, path_stat.st_ino) != (image_stat.st_dev, image_stat.st_ino):
raise RuntimeError("image path changed during validation")
return 0
finally:
if image_fd >= 0:
os.close(image_fd)
os.close(parent_fd)
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, RuntimeError, ValueError, subprocess.CalledProcessError) as error:
print(f"ERROR: {error}", file=sys.stderr)
raise SystemExit(1)
+33 -19
View File
@@ -8,19 +8,17 @@ mkdir -p "$tmp/bin" "$tmp/store" "$tmp/partial-store"
log=$tmp/commands.log
export RUNNER_TEST_LOG=$log
cat >"$tmp/bin/fallocate" <<'EOF'
#!/usr/bin/env bash
printf 'fallocate\n' >>"$RUNNER_TEST_LOG"
truncate -s 1048576 "$3"
EOF
cat >"$tmp/bin/mkfs.ext4" <<'EOF'
#!/usr/bin/env bash
printf 'mkfs\n' >>"$RUNNER_TEST_LOG"
printf 'ext4\n' >"$3.fs"
if [[ -n ${RUNNER_TEST_REPLACE_IMAGE:-} ]]; then
rm -f "$RUNNER_TEST_IMAGE"
ln -s "$RUNNER_TEST_REPLACE_IMAGE" "$RUNNER_TEST_IMAGE"
fi
EOF
cat >"$tmp/bin/blkid" <<'EOF'
#!/usr/bin/env bash
cat "${@: -1}.fs"
printf '%s\n' "${RUNNER_TEST_FS:-ext4}"
EOF
cat >"$tmp/bin/systemctl" <<'EOF'
#!/usr/bin/env bash
@@ -158,26 +156,42 @@ then
fi
image=$tmp/docker-store.img
runner_ensure_image "$image" 1M 1048576
runner_ensure_image "$image" 1M 1048576
[[ $(grep -c '^fallocate$' "$log") == 1 ]]
runner_ensure_image "$image" 1048576 "$(command id -u)"
runner_ensure_image "$image" 1048576 "$(command id -u)"
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
printf 'ext4\n' >"$image.fs"
truncate -s 1048575 "$image"
if runner_ensure_image "$image" 1M 1048576 2>/dev/null; then
if runner_ensure_image "$image" 1048576 "$(command id -u)" 2>/dev/null; then
echo 'Mismatched image size was accepted' >&2
exit 1
fi
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
truncate -s 1048576 "$image"
printf 'ext3\n' >"$image.fs"
if runner_ensure_image "$image" 1M 1048576 2>/dev/null; then
if RUNNER_TEST_FS=ext3 runner_ensure_image "$image" 1048576 "$(command id -u)" 2>/dev/null; then
echo 'Existing non-ext4 image was accepted' >&2
exit 1
fi
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
printf 'ext4\n' >"$image.fs"
external=$tmp/external-file
printf 'preserve this file\n' >"$external"
race_image=$tmp/race.img
ln -s "$external" "$race_image"
if runner_ensure_image "$race_image" 1048576 "$(command id -u)" 2>/dev/null; then
echo 'Symlink runner image was accepted' >&2
exit 1
fi
[[ $(cat "$external") == 'preserve this file' ]]
rm -f "$race_image"
race_image=$tmp/race-during-format.img
export RUNNER_TEST_IMAGE="$race_image" RUNNER_TEST_REPLACE_IMAGE="$external"
if runner_ensure_image "$race_image" 1048576 "$(command id -u)" 2>/dev/null; then
echo 'Image path replacement during formatting was accepted' >&2
exit 1
fi
[[ $(cat "$external") == 'preserve this file' ]]
rm -f "$race_image"
unset RUNNER_TEST_IMAGE RUNNER_TEST_REPLACE_IMAGE
[[ $(grep -c '^mkfs$' "$log") == 2 ]]
unit_name='var-lib-akyldash\x2drunner-docker\x2ddata.mount'
unit_file=$tmp/systemd/$unit_name
@@ -190,9 +204,9 @@ grep -Fq "enable $unit_name" "$log"
grep -Fq "start $unit_name" "$log"
# A second run with the image mounted must reuse it and avoid another start.
runner_ensure_image "$image" 1M 1048576
runner_ensure_image "$image" 1048576 "$(command id -u)"
runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$unit_file"
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
[[ $(grep -c '^mkfs$' "$log") == 2 ]]
[[ $(grep -c '^start ' "$log") == 1 ]]
chmod 0664 "$unit_file"
if runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$unit_file" 2>/dev/null; then
@@ -217,9 +231,9 @@ if runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$tmp/systemd/
exit 1
fi
export RUNNER_TEST_FAIL_START=0
runner_ensure_image "$image" 1M 1048576
runner_ensure_image "$image" 1048576 "$(command id -u)"
runner_ensure_mount "$image" "$RUNNER_TEST_STORE" "$unit_name" "$tmp/systemd/partial.mount"
[[ $(grep -c '^mkfs$' "$log") == 1 ]]
[[ $(grep -c '^mkfs$' "$log") == 2 ]]
[[ $(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
+4 -3
View File
@@ -17,9 +17,10 @@ grep -Eq '^ valid_volumes: \[\]$' "$config"
grep -Fq -- '--cpus=2 --memory=3g --pids-limit=512' "$config"
assert_absent 'var/run/docker.sock|metrics:|health_check:|production.env|review_secret' "$config"
grep -Fq 'docker-ce-rootless-extras' "$installer"
grep -Fq 'runner_ensure_image "$runner_image" 15G "$runner_image_bytes"' "$installer"
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 'runner_ensure_image "$runner_image" "$runner_image_bytes" "$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
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