Create runner image through a pinned file descriptor
This commit is contained in:
1 parent
4110aa54c9
commit
433c3059b9
9 files changed
+153
-41
No files matched your search
@@ -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
@@ -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`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user