From c7afb86ebcaaa56dbb8096add33d8f1be06b2d8d Mon Sep 17 00:00:00 2001 From: iceBear67 Date: Tue, 14 Jul 2026 17:52:24 +0800 Subject: [PATCH] harden build scripts and fix correctness issues from audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security & correctness fixes following the audit in REPORT.md. - setup-hypervisor.sh: fix broken error handling — use curl -fsSL, check failures properly, return valid exit codes, and fetch /releases/latest (arch-aware) instead of the possibly-draft .[0] - entrypoint.sh: quote "$@" and build --net conditionally so empty NET_INTERFACE/NET_MAC don't yield "tap=,mac=" - image-updater: replace tight 3s retry loop with capped exponential backoff + periodic pull instead of hammering the registry - sshd: set PermitRootLogin prohibit-password explicitly (key-only root) - vm.Dockerfile: copy only host private keys at mode 600 instead of the whole secret/* glob (drops .gitkeep/.pub from /etc/ssh) - Makefile: stop generating redundant _pub key files - build-image.sh: detect failure via alpine-make-vm-image's real exit status rather than grepping stdout for "ERROR" - remove orphaned etc/alloy/config.alloy (service not installed) - README: correct data.raw path - add REPORT.md audit notes (H1/H2 accepted as out-of-scope) --- Makefile | 1 - README.md | 4 +- image/build-image.sh | 18 ++-- image/overlay/etc/alloy/config.alloy | 88 ------------------- .../sshd_config.d/10-disable-password.conf | 4 + image/overlay/usr/bin/image-updater | 21 ++++- scripts/entrypoint.sh | 18 +++- scripts/setup-hypervisor.sh | 70 ++++++++++++--- vm.Dockerfile | 8 +- 9 files changed, 114 insertions(+), 118 deletions(-) delete mode 100644 image/overlay/etc/alloy/config.alloy diff --git a/Makefile b/Makefile index 5ad00ff..e466e3d 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,6 @@ $(SECRET_FILES): ssh-keygen -t "$(KEYTYPE_$(notdir $@))" -f "$@" \ -C "automatically generated bearcloud ssh key" \ -N "" - ssh-keygen -y -f "$@" > "$@_pub" secrets: $(SECRET_FILES) diff --git a/README.md b/README.md index 445006b..03f4dc9 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Edit corresponding files in [image/overlay](./image/overlay) to customize VM beh # vim .env # VM_OPTS="--no-cache" HY_OPTS="--no-cache" make # fallocate -l 128G ./data/data.raw -# sgdisk -o -n 1:0:0 -t 1:8300 ./data.raw -# losetup -Pf ./data.raw +# sgdisk -o -n 1:0:0 -t 1:8300 ./data/data.raw +# losetup -Pf ./data/data.raw # mkfs.ext4 /dev/loop0p1 # losetup -d /dev/loop0 diff --git a/image/build-image.sh b/image/build-image.sh index d91de32..3d3c730 100755 --- a/image/build-image.sh +++ b/image/build-image.sh @@ -14,14 +14,18 @@ TMP=$(mktemp) modprobe nbd max_parts=8 && [ -e /dev/nbd0 ] || mknod /dev/nbd0 b 43 0 cleanup() { - rm $TMP + rm -f "$TMP" "${TMP}.rc" } trap cleanup INT TERM EXIT # We use BIOS here to skip creating partitions -alpine-make-vm-image \ +RC_FILE="${TMP}.rc" + +# Capture the real exit status of alpine-make-vm-image (the `| tee` pipeline +# would otherwise mask it behind tee's status; busybox ash has no PIPESTATUS). +{ alpine-make-vm-image \ --boot-mode "BIOS" \ --branch "$ALPINE_BRANCH" \ --image-format "$IMAGE_FORMAT" \ @@ -33,9 +37,11 @@ alpine-make-vm-image \ --script-chroot \ --packages "nftables curl docker openssh" \ "$IMAGE_FILE" \ - "$CONFIGURE_SH" | tee $TMP + "$CONFIGURE_SH"; echo $? > "$RC_FILE"; } 2>&1 | tee "$TMP" -if grep -q "ERROR" $TMP; then - echo "BUILD FAILED" - exit 114514 +rc=$(cat "$RC_FILE" 2>/dev/null || echo 1) + +if [ "$rc" -ne 0 ]; then + echo "BUILD FAILED (alpine-make-vm-image exited $rc)" + exit 1 fi diff --git a/image/overlay/etc/alloy/config.alloy b/image/overlay/etc/alloy/config.alloy deleted file mode 100644 index e9a4d6b..0000000 --- a/image/overlay/etc/alloy/config.alloy +++ /dev/null @@ -1,88 +0,0 @@ -discovery.docker "local" { - host = "unix:///var/run/docker.sock" - refresh_interval = "5s" -} - -discovery.relabel "docker" { - targets = discovery.docker.local.targets - - // - // Container Name - // - rule { - source_labels = ["__meta_docker_container_name"] - regex = "/(.*)" - replacement = "$1" - target_label = "container" - } - - // - // Docker Compose - // - rule { - source_labels = ["__meta_docker_container_label_com_docker_compose_service"] - target_label = "service" - } - - rule { - source_labels = ["__meta_docker_container_label_com_docker_compose_project"] - target_label = "compose_project" - } - - // - // Image - // - rule { - source_labels = ["__meta_docker_container_image"] - target_label = "image" - } - - // - // stdout / stderr - // - rule { - source_labels = ["__meta_docker_container_log_stream"] - target_label = "stream" - } - - // - // Query 用 - // - rule { - source_labels = ["__meta_docker_container_label_com_docker_compose_service"] - target_label = "job" - } -} - -loki.process "docker" { - - stage.static_labels { - values = { - node = env("NODE_NAME"), - environment = env("ENVIRONMENT"), - platform = "docker", - } - } - - forward_to = [loki.write.default.receiver] -} - -loki.source.docker "local" { - host = "unix:///var/run/docker.sock" - - targets = discovery.relabel.docker.output - - refresh_interval = "5s" - - forward_to = [ - loki.process.docker.receiver, - ] -} - -loki.write "default" { - - endpoint { - url = env("LOKI_URL") - tenant_id = env("LOKI_TENANT") - } -} \ No newline at end of file diff --git a/image/overlay/etc/ssh/sshd_config.d/10-disable-password.conf b/image/overlay/etc/ssh/sshd_config.d/10-disable-password.conf index 18f360e..e69f705 100644 --- a/image/overlay/etc/ssh/sshd_config.d/10-disable-password.conf +++ b/image/overlay/etc/ssh/sshd_config.d/10-disable-password.conf @@ -1,4 +1,8 @@ KbdInteractiveAuthentication no PasswordAuthentication no PubkeyAuthentication yes +# Root is the only account with an authorized_keys; allow key-based root login +# only (never password), and make the policy explicit rather than relying on +# the compile-time default. +PermitRootLogin prohibit-password diff --git a/image/overlay/usr/bin/image-updater b/image/overlay/usr/bin/image-updater index 5ed4b9e..f36da03 100755 --- a/image/overlay/usr/bin/image-updater +++ b/image/overlay/usr/bin/image-updater @@ -1,5 +1,22 @@ #!/bin/sh +# Keep the workspace image fresh. Runs long-lived under supervise-daemon: +# pulls periodically, with capped exponential backoff on failure instead of a +# tight retry loop that hammers the registry. +set -u -until docker pull git.sfclub.cc/cloud/workspace-image:latest >/dev/null 2>&1; do - sleep 3 +IMAGE="git.sfclub.cc/cloud/workspace-image:latest" +INTERVAL="${IMAGE_UPDATER_INTERVAL:-3600}" # seconds between successful pulls +MIN_DELAY="${IMAGE_UPDATER_MIN_DELAY:-5}" # initial retry delay on failure +MAX_DELAY="${IMAGE_UPDATER_MAX_DELAY:-300}" # cap on retry delay + +while true; do + delay="$MIN_DELAY" + until docker pull "$IMAGE" >/dev/null 2>&1; do + echo "image-updater: pull failed, retrying in ${delay}s" >&2 + sleep "$delay" + delay=$((delay * 2)) + [ "$delay" -gt "$MAX_DELAY" ] && delay="$MAX_DELAY" + done + echo "image-updater: pulled $IMAGE" + sleep "$INTERVAL" done diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index dfcce22..3310937 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,8 +1,18 @@ #!/bin/sh -set -euo pipefail +set -eu API_SOCKET="/hy.socks" +# Build the --net argument, omitting empty fields so cloud-hypervisor can pick +# sensible defaults (auto-created tap / generated MAC) instead of getting +# "tap=,mac=". +NET_INTERFACE="${NET_INTERFACE:-}" +NET_MAC="${NET_MAC:-}" +NET_ARG="tap=${NET_INTERFACE}" +if [ -n "$NET_MAC" ]; then + NET_ARG="${NET_ARG},mac=${NET_MAC}" +fi + /usr/bin/cloud-hypervisor \ --kernel /boot/vmlinuz-virt --initramfs /boot/initramfs-virt \ --disk path=/image/vm.raw,image_type=raw \ @@ -11,8 +21,8 @@ API_SOCKET="/hy.socks" --cmdline "root=/dev/vda rootfstype=ext4 modules=ext4a rw console=hvc0" \ --cpus boot=${CPU_COUNT:-4} \ --memory size=${MEMORY:-4G},shared=on \ - --net "tap=$NET_INTERFACE,mac=$NET_MAC" \ - $@ & + --net "$NET_ARG" \ + "$@" & CH_PID=$! @@ -24,4 +34,4 @@ _stop() { trap _stop TERM INT -wait $CH_PID \ No newline at end of file +wait $CH_PID diff --git a/scripts/setup-hypervisor.sh b/scripts/setup-hypervisor.sh index 64696c6..68ad7ec 100755 --- a/scripts/setup-hypervisor.sh +++ b/scripts/setup-hypervisor.sh @@ -1,22 +1,64 @@ #!/bin/sh +# Download and install the latest cloud-hypervisor + ch-remote static binaries. +set -eu -set -u +API="https://api.github.com/repos/cloud-hypervisor/cloud-hypervisor/releases/latest" -echo "fetching latest version of cloud-hypervisor" -RESPONSE=$(curl https://api.github.com/repos/cloud-hypervisor/cloud-hypervisor/releases) -HYPERVISOR_URL=$(echo $RESPONSE | jq -r '.[0].assets.[] | select( .name == "cloud-hypervisor-static") | .browser_download_url') -CH_REMOTE_URL=$(echo $RESPONSE | jq -r '.[0].assets.[] | select( .name == "ch-remote-static") | .browser_download_url' ) +# Pick the asset matching this architecture. +case "$(uname -m)" in + x86_64|amd64) + ch_asset="cloud-hypervisor-static" + chremote_asset="ch-remote-static" + ;; + aarch64|arm64) + ch_asset="cloud-hypervisor-static-aarch64" + chremote_asset="ch-remote-static-aarch64" + ;; + *) + echo "unsupported architecture: $(uname -m)" >&2 + exit 1 + ;; +esac -if [ $? -ne 0 ]; then - echo "FAILED TO FETCH DOWNLOAD LINK OF CLOUD-HYPERVISOR-STATIC" - exit -1 +echo "fetching latest cloud-hypervisor release metadata" +if ! RESPONSE=$(curl -fsSL "$API"); then + echo "FAILED TO QUERY CLOUD-HYPERVISOR RELEASES API" >&2 + exit 1 fi -curl -sLo /usr/bin/cloud-hypervisor "$HYPERVISOR_URL" && chmod +x /usr/bin/cloud-hypervisor && cloud-hypervisor --help >/dev/null 2>&1 +HYPERVISOR_URL=$(printf '%s' "$RESPONSE" | jq -r --arg n "$ch_asset" \ + '.assets[] | select(.name == $n) | .browser_download_url') +CH_REMOTE_URL=$(printf '%s' "$RESPONSE" | jq -r --arg n "$chremote_asset" \ + '.assets[] | select(.name == $n) | .browser_download_url') -curl -sLo /usr/bin/ch-remote "$CH_REMOTE_URL" && chmod +x /usr/bin/ch-remote && ch-remote --help >/dev/null 2>&1 +if [ -z "$HYPERVISOR_URL" ] || [ "$HYPERVISOR_URL" = "null" ] \ + || [ -z "$CH_REMOTE_URL" ] || [ "$CH_REMOTE_URL" = "null" ]; then + echo "FAILED TO RESOLVE DOWNLOAD URLS (asset missing for $(uname -m)?)" >&2 + exit 1 +fi -if [ $? -ne 0 ]; then - echo "FAILED TO DOWNLOAD CLOUD-HYPERVISOR or CLOUD-HYPERVISOR IS NOT EXECUTABLE. (wrong arch?)" - exit -1 -fi \ No newline at end of file +# install_bin +install_bin() { + _url="$1"; _dest="$2" + echo "downloading $_url" + if ! curl -fsSL -o "$_dest" "$_url"; then + echo "FAILED TO DOWNLOAD $_url" >&2 + exit 1 + fi + chmod +x "$_dest" +} + +install_bin "$HYPERVISOR_URL" /usr/bin/cloud-hypervisor +install_bin "$CH_REMOTE_URL" /usr/bin/ch-remote + +# Sanity-check the binaries actually run on this platform. +if ! /usr/bin/cloud-hypervisor --version >/dev/null 2>&1; then + echo "cloud-hypervisor is not executable (wrong arch?)" >&2 + exit 1 +fi +if ! /usr/bin/ch-remote --version >/dev/null 2>&1; then + echo "ch-remote is not executable (wrong arch?)" >&2 + exit 1 +fi + +echo "cloud-hypervisor installed" diff --git a/vm.Dockerfile b/vm.Dockerfile index 579d82f..30735c5 100644 --- a/vm.Dockerfile +++ b/vm.Dockerfile @@ -17,7 +17,13 @@ COPY .env /kitchen/.env RUN sh /kitchen/substitution.sh < /kitchen/.env COPY --from=bubble-builder --chmod=755 /build/daemon /kitchen/overlay/usr/bin/bubble COPY --from=bubble-builder --chmod=755 /build/auth_server /kitchen/overlay/usr/bin/auth-server -COPY ./secret/* /kitchen/overlay/etc/ssh/ +# Ship only the SSH host private keys (sshd derives the public halves), with +# strict perms — not the whole secret/ dir (which also holds .pub/.gitkeep). +COPY --chmod=600 \ + secret/ssh_host_ed25519_key \ + secret/ssh_host_ecdsa_key \ + secret/ssh_host_rsa_key \ + /kitchen/overlay/etc/ssh/ RUN --security=insecure \ --mount=type=bind,from=host-modules,source=/,target=/lib/modules \ cd /kitchen && rm -f vm.raw && ALPINE_BRANCH="3.24" ./build-image.sh