diff --git a/scripts/proxmox/README.md b/scripts/proxmox/README.md index e433c3f..89b7460 100644 --- a/scripts/proxmox/README.md +++ b/scripts/proxmox/README.md @@ -45,3 +45,10 @@ The image URL and optional SHA256 value can be overridden through environment variables. The default URL tracks the Flatcar Stable current amd64 image. Pin `FLATCAR_IMAGE_URL` and set `FLATCAR_IMAGE_SHA256` for a reproducible change controlled deployment. + +## Dedicated k3d test host + +Use [k3d-test-host.md](k3d-test-host.md) and +`create-flatcar-k3d-test-host.sh` for a separate Flatcar VM used for Linux +k3d, Kyverno, Falco, and kube-bench testing. This VM has no Gitea runner token +and should remain separate from the CI runner trust boundary. diff --git a/scripts/proxmox/create-flatcar-k3d-test-host.sh b/scripts/proxmox/create-flatcar-k3d-test-host.sh new file mode 100755 index 0000000..0add5a7 --- /dev/null +++ b/scripts/proxmox/create-flatcar-k3d-test-host.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# Create a Flatcar VM for disposable Linux k3d security testing. +# Run this on the Proxmox host as root. +set -Eeuo pipefail + +usage() { + cat <<'EOF' +Usage: + VM_ID=9101 \ + SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ + ./create-flatcar-k3d-test-host.sh + +Required environment variables: + VM_ID Proxmox VM ID that does not already exist + SSH_PUBLIC_KEY SSH public key for the Flatcar core user + +Optional environment variables: + VM_NAME VM name, default flatcar-k3d-test + CORES CPU cores, default 4 + MEMORY_MB Memory in MiB, default 16384 + DISK_SIZE Final disk size, default 100G + BRIDGE Proxmox bridge, default vmbr0 + DISK_STORAGE Storage for the VM disk, default local-lvm + CLOUDINIT_STORAGE Storage for the Cloud-Init drive, default local-lvm + SNIPPET_STORAGE Proxmox storage containing snippets, default local + SNIPPET_DIR Host snippet directory, default /var/lib/vz/snippets + IMAGE_CACHE_DIR Host image cache, default /var/lib/vz/template/cache + FLATCAR_IMAGE_URL Flatcar Stable amd64 Proxmox image URL + FLATCAR_IMAGE_SHA256 Optional expected SHA256 for the image + K3D_VERSION k3d version installed on first boot, default v5.9.0 + KUBECTL_VERSION kubectl version installed on first boot, default v1.35.5 + +The VM uses DHCP and is intended to be a dedicated disposable Linux test host. +It is not a Gitea runner and does not receive a registration token. +EOF +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +[[ "${1:-}" != "--help" && "${1:-}" != "-h" ]] || { usage; exit 0; } +[[ "$(id -u)" -eq 0 ]] || die "run this script as root on the Proxmox host" + +for command_name in qm pvesm curl sha256sum jq; do + require_command "$command_name" +done + +: "${VM_ID:?VM_ID is required}" +: "${SSH_PUBLIC_KEY:?SSH_PUBLIC_KEY is required}" + +VM_NAME="${VM_NAME:-flatcar-k3d-test}" +CORES="${CORES:-4}" +MEMORY_MB="${MEMORY_MB:-16384}" +DISK_SIZE="${DISK_SIZE:-100G}" +BRIDGE="${BRIDGE:-vmbr0}" +DISK_STORAGE="${DISK_STORAGE:-local-lvm}" +CLOUDINIT_STORAGE="${CLOUDINIT_STORAGE:-local-lvm}" +SNIPPET_STORAGE="${SNIPPET_STORAGE:-local}" +SNIPPET_DIR="${SNIPPET_DIR:-/var/lib/vz/snippets}" +IMAGE_CACHE_DIR="${IMAGE_CACHE_DIR:-/var/lib/vz/template/cache}" +FLATCAR_IMAGE_URL="${FLATCAR_IMAGE_URL:-https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_proxmoxve_image.img}" +K3D_VERSION="${K3D_VERSION:-v5.9.0}" +KUBECTL_VERSION="${KUBECTL_VERSION:-v1.35.5}" + +[[ "$VM_ID" =~ ^[0-9]+$ ]] || die "VM_ID must be numeric" +[[ "$CORES" =~ ^[0-9]+$ ]] || die "CORES must be numeric" +[[ "$MEMORY_MB" =~ ^[0-9]+$ ]] || die "MEMORY_MB must be numeric" + +if qm status "$VM_ID" >/dev/null 2>&1; then + die "VM $VM_ID already exists; choose another VM_ID" +fi + +snippet_storages="$(pvesm status --content snippets 2>/dev/null || true)" +awk -v storage="$SNIPPET_STORAGE" '$1 == storage {found = 1} END {exit !found}' <<<"$snippet_storages" || die "storage $SNIPPET_STORAGE is not configured for snippets" + +mkdir -p "$SNIPPET_DIR" "$IMAGE_CACHE_DIR" +chmod 0750 "$SNIPPET_DIR" + +work_dir="$(mktemp -d /tmp/flatcar-k3d.XXXXXX)" +cleanup() { rm -rf "$work_dir"; } +trap cleanup EXIT + +image_path="$IMAGE_CACHE_DIR/$(basename "$FLATCAR_IMAGE_URL")" +snippet_name="flatcar-k3d-test-${VM_ID}-user-data" +snippet_path="$SNIPPET_DIR/$snippet_name" + +if [[ -s "$image_path" ]]; then + echo "Using cached Flatcar image: $image_path" +else + echo "Downloading Flatcar image: $FLATCAR_IMAGE_URL" + curl --fail --location --retry 3 --output "$image_path.partial" "$FLATCAR_IMAGE_URL" + mv "$image_path.partial" "$image_path" +fi + +if [[ -n "${FLATCAR_IMAGE_SHA256:-}" ]]; then + printf '%s %s\n' "$FLATCAR_IMAGE_SHA256" "$image_path" | sha256sum --check --strict - +fi + +bootstrap="$work_dir/bootstrap-k3d-test-host.sh" +cat >"$bootstrap" </home/core/.profile <<'PROFILE' +export PATH="/home/core/bin:/opt/k3d-test/bin:$PATH" +PROFILE +chown -R core:core /home/core/bin /home/core/.profile +touch /opt/k3d-test/.tools-installed +EOF + +bootstrap_unit="$work_dir/k3d-test-tools.service" +cat >"$bootstrap_unit" <<'EOF' +[Unit] +Description=Install k3d test host tooling +After=network-online.target docker.service +Wants=network-online.target +Requires=docker.service +ConditionPathExists=!/opt/k3d-test/.tools-installed + +[Service] +Type=oneshot +ExecStart=/opt/k3d-test/bootstrap-k3d-test-host.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF + +jq -n \ + --arg ssh_key "$SSH_PUBLIC_KEY" \ + --rawfile bootstrap "$bootstrap" \ + --rawfile bootstrap_unit "$bootstrap_unit" \ + '{ + ignition: {version: "3.3.0"}, + storage: { + directories: [ + {path: "/opt/k3d-test", mode: 493}, + {path: "/opt/k3d-test/bin", mode: 493}, + {path: "/opt/k3d-test/workspace", mode: 493} + ], + files: [ + {path: "/opt/k3d-test/bootstrap-k3d-test-host.sh", mode: 365, contents: {source: ("data:text/plain;charset=utf-8," + ($bootstrap | @uri))}} + ] + }, + systemd: { + units: [ + {name: "docker.service", enabled: true}, + {name: "k3d-test-tools.service", enabled: true, contents: $bootstrap_unit} + ] + }, + passwd: {users: [{name: "core", sshAuthorizedKeys: [$ssh_key]}]} + }' >"$snippet_path" + +chmod 0600 "$snippet_path" + +echo "Creating Proxmox VM $VM_ID" +qm create "$VM_ID" \ + --name "$VM_NAME" \ + --ostype l26 \ + --cores "$CORES" \ + --memory "$MEMORY_MB" \ + --net0 "virtio,bridge=$BRIDGE" \ + --ipconfig0 ip=dhcp \ + --agent enabled=1 \ + --onboot 1 \ + --scsihw virtio-scsi-single + +qm disk import "$VM_ID" "$image_path" "$DISK_STORAGE" +qm set "$VM_ID" --scsi0 "$DISK_STORAGE:vm-${VM_ID}-disk-0" +qm resize "$VM_ID" scsi0 "$DISK_SIZE" +qm set "$VM_ID" --boot order=scsi0 +qm set "$VM_ID" --ide2 "$CLOUDINIT_STORAGE:cloudinit" +qm set "$VM_ID" --cicustom "user=${SNIPPET_STORAGE}:snippets/${snippet_name}" +qm start "$VM_ID" + +cat < 'systemctl status docker k3d-test-tools --no-pager' + ssh core@ 'k3d version && kubectl version --client' + +The test host has no Gitea token and is separate from the CI runner. +EOF diff --git a/scripts/proxmox/k3d-test-host.md b/scripts/proxmox/k3d-test-host.md new file mode 100644 index 0000000..d4d8ce9 --- /dev/null +++ b/scripts/proxmox/k3d-test-host.md @@ -0,0 +1,40 @@ +# Flatcar k3d test host + +`create-flatcar-k3d-test-host.sh` creates a dedicated Flatcar VM for Linux +k3d testing. It is separate from the Gitea runner and does not receive a +runner registration token or a repository credential. + +The default profile is 4 CPU cores, 16 GiB memory, and a 100 GiB disk. The +Ignition configuration enables Docker, creates `/opt/k3d-test/workspace`, and +installs pinned k3d and kubectl binaries on first boot. + +Enable snippets on the Proxmox storage first: + +```bash +pvesm set local --content iso,vztmpl,backup,snippets +``` + +Create the VM: + +```bash +VM_ID=9101 \ +SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ +DISK_STORAGE=pve1_nvme \ +SNIPPET_STORAGE=local \ +bash create-flatcar-k3d-test-host.sh +``` + +After the VM receives a DHCP address: + +```bash +ssh core@ 'systemctl status docker k3d-test-tools --no-pager' +ssh core@ 'k3d version && kubectl version --client' +``` + +Clone the repository into the test workspace, then run the project tests from +the Flatcar host. The Docker socket remains local to this VM, so the MacBook +only acts as the SSH operator workstation. + +The VM is intentionally dedicated to disposable security testing. Do not run +untrusted workloads or unrelated services on it. k3d creates K3s nodes as +Docker containers, so the host still has broad control over the test cluster.