harden build scripts and fix correctness issues from audit

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)
This commit is contained in:
iceBear67
2026-07-14 17:52:24 +08:00
parent 43cd7c1d22
commit c7afb86ebc
9 changed files with 114 additions and 118 deletions
-1
View File
@@ -45,7 +45,6 @@ $(SECRET_FILES):
ssh-keygen -t "$(KEYTYPE_$(notdir $@))" -f "$@" \ ssh-keygen -t "$(KEYTYPE_$(notdir $@))" -f "$@" \
-C "automatically generated bearcloud ssh key" \ -C "automatically generated bearcloud ssh key" \
-N "" -N ""
ssh-keygen -y -f "$@" > "$@_pub"
secrets: $(SECRET_FILES) secrets: $(SECRET_FILES)
+2 -2
View File
@@ -11,8 +11,8 @@ Edit corresponding files in [image/overlay](./image/overlay) to customize VM beh
# vim .env # vim .env
# VM_OPTS="--no-cache" HY_OPTS="--no-cache" make # VM_OPTS="--no-cache" HY_OPTS="--no-cache" make
# fallocate -l 128G ./data/data.raw # fallocate -l 128G ./data/data.raw
# sgdisk -o -n 1:0:0 -t 1:8300 ./data.raw # sgdisk -o -n 1:0:0 -t 1:8300 ./data/data.raw
# losetup -Pf ./data.raw # losetup -Pf ./data/data.raw
# mkfs.ext4 /dev/loop0p1 # mkfs.ext4 /dev/loop0p1
# losetup -d /dev/loop0 # losetup -d /dev/loop0
+12 -6
View File
@@ -14,14 +14,18 @@ TMP=$(mktemp)
modprobe nbd max_parts=8 && [ -e /dev/nbd0 ] || mknod /dev/nbd0 b 43 0 modprobe nbd max_parts=8 && [ -e /dev/nbd0 ] || mknod /dev/nbd0 b 43 0
cleanup() { cleanup() {
rm $TMP rm -f "$TMP" "${TMP}.rc"
} }
trap cleanup INT TERM EXIT trap cleanup INT TERM EXIT
# We use BIOS here to skip creating partitions # 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" \ --boot-mode "BIOS" \
--branch "$ALPINE_BRANCH" \ --branch "$ALPINE_BRANCH" \
--image-format "$IMAGE_FORMAT" \ --image-format "$IMAGE_FORMAT" \
@@ -33,9 +37,11 @@ alpine-make-vm-image \
--script-chroot \ --script-chroot \
--packages "nftables curl docker openssh" \ --packages "nftables curl docker openssh" \
"$IMAGE_FILE" \ "$IMAGE_FILE" \
"$CONFIGURE_SH" | tee $TMP "$CONFIGURE_SH"; echo $? > "$RC_FILE"; } 2>&1 | tee "$TMP"
if grep -q "ERROR" $TMP; then rc=$(cat "$RC_FILE" 2>/dev/null || echo 1)
echo "BUILD FAILED"
exit 114514 if [ "$rc" -ne 0 ]; then
echo "BUILD FAILED (alpine-make-vm-image exited $rc)"
exit 1
fi fi
-88
View File
@@ -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")
}
}
@@ -1,4 +1,8 @@
KbdInteractiveAuthentication no KbdInteractiveAuthentication no
PasswordAuthentication no PasswordAuthentication no
PubkeyAuthentication yes 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
+19 -2
View File
@@ -1,5 +1,22 @@
#!/bin/sh #!/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 IMAGE="git.sfclub.cc/cloud/workspace-image:latest"
sleep 3 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 done
+13 -3
View File
@@ -1,8 +1,18 @@
#!/bin/sh #!/bin/sh
set -euo pipefail set -eu
API_SOCKET="/hy.socks" 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 \ /usr/bin/cloud-hypervisor \
--kernel /boot/vmlinuz-virt --initramfs /boot/initramfs-virt \ --kernel /boot/vmlinuz-virt --initramfs /boot/initramfs-virt \
--disk path=/image/vm.raw,image_type=raw \ --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" \ --cmdline "root=/dev/vda rootfstype=ext4 modules=ext4a rw console=hvc0" \
--cpus boot=${CPU_COUNT:-4} \ --cpus boot=${CPU_COUNT:-4} \
--memory size=${MEMORY:-4G},shared=on \ --memory size=${MEMORY:-4G},shared=on \
--net "tap=$NET_INTERFACE,mac=$NET_MAC" \ --net "$NET_ARG" \
$@ & "$@" &
CH_PID=$! CH_PID=$!
+56 -14
View File
@@ -1,22 +1,64 @@
#!/bin/sh #!/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" # Pick the asset matching this architecture.
RESPONSE=$(curl https://api.github.com/repos/cloud-hypervisor/cloud-hypervisor/releases) case "$(uname -m)" in
HYPERVISOR_URL=$(echo $RESPONSE | jq -r '.[0].assets.[] | select( .name == "cloud-hypervisor-static") | .browser_download_url') x86_64|amd64)
CH_REMOTE_URL=$(echo $RESPONSE | jq -r '.[0].assets.[] | select( .name == "ch-remote-static") | .browser_download_url' ) 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 "fetching latest cloud-hypervisor release metadata"
echo "FAILED TO FETCH DOWNLOAD LINK OF CLOUD-HYPERVISOR-STATIC" if ! RESPONSE=$(curl -fsSL "$API"); then
exit -1 echo "FAILED TO QUERY CLOUD-HYPERVISOR RELEASES API" >&2
exit 1
fi 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
if [ $? -ne 0 ]; then echo "FAILED TO RESOLVE DOWNLOAD URLS (asset missing for $(uname -m)?)" >&2
echo "FAILED TO DOWNLOAD CLOUD-HYPERVISOR or CLOUD-HYPERVISOR IS NOT EXECUTABLE. (wrong arch?)" exit 1
exit -1
fi fi
# install_bin <url> <dest>
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"
+7 -1
View File
@@ -17,7 +17,13 @@ COPY .env /kitchen/.env
RUN sh /kitchen/substitution.sh < /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/daemon /kitchen/overlay/usr/bin/bubble
COPY --from=bubble-builder --chmod=755 /build/auth_server /kitchen/overlay/usr/bin/auth-server 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 \ RUN --security=insecure \
--mount=type=bind,from=host-modules,source=/,target=/lib/modules \ --mount=type=bind,from=host-modules,source=/,target=/lib/modules \
cd /kitchen && rm -f vm.raw && ALPINE_BRANCH="3.24" ./build-image.sh cd /kitchen && rm -f vm.raw && ALPINE_BRANCH="3.24" ./build-image.sh