add Flatcar Proxmox Gitea runner bootstrap
Kube-bench CIS scan / Scan ephemeral K3s cluster (push) Failing after 3m12s

This commit is contained in:
2026-08-11 16:26:09 -04:00
parent 51dff6d515
commit 2a9d312326
2 changed files with 267 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# Proxmox Flatcar Gitea runner
`create-flatcar-gitea-runner.sh` creates a Flatcar VM using the Stable Proxmox
image and provisions it with Ignition through Proxmox `cicustom` user-data.
The VM uses 4 CPU cores, 16 GiB memory, and a 100 GiB disk by default. It
enables Docker and starts the Gitea Actions runner in a persistent container.
The script follows the Flatcar Proxmox flow: import the image, attach a
Cloud-Init drive, place Ignition JSON in the Proxmox snippets directory, and
set `qm set --cicustom user=...`. Flatcar uses the same `user-data` path for
Ignition and regular cloud-init, so do not combine this snippet with regular
cloud-init settings.
Before running it:
1. Enable the `snippets` content type on the Proxmox storage used by
`SNIPPET_STORAGE`.
2. Obtain a repository, organization, or instance Gitea runner registration
token.
3. Confirm the VM ID is unused and the bridge and storage names are correct.
Example:
```bash
chmod +x scripts/proxmox/create-flatcar-gitea-runner.sh
VM_ID=9100 \
GITEA_INSTANCE_URL=https://git.example.com \
GITEA_RUNNER_REGISTRATION_TOKEN='replace-with-token' \
SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \
scripts/proxmox/create-flatcar-gitea-runner.sh
```
The registration token is written to a root-only file inside the VM. It is
also present in the generated Proxmox snippet, so protect the snippet storage
and remove it after the VM is provisioned if recovery from the snippet is not
required.
The runner mounts the Docker socket because Gitea Actions uses Docker to
execute workflow jobs. A Docker socket is equivalent to broad host control,
so this VM should be dedicated to trusted CI workloads and should not host
unrelated services.
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.
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
# Create a Flatcar VM on Proxmox and bootstrap a Docker-based Gitea Actions runner.
# Run this on the Proxmox host as root.
set -Eeuo pipefail
usage() {
cat <<'EOF'
Usage:
VM_ID=9100 \
GITEA_INSTANCE_URL=https://gitea.example.com \
GITEA_RUNNER_REGISTRATION_TOKEN='token' \
SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \
./create-flatcar-gitea-runner.sh
Required environment variables:
VM_ID Proxmox VM ID that does not already exist
GITEA_INSTANCE_URL Gitea URL, including scheme
GITEA_RUNNER_REGISTRATION_TOKEN Repository, organization, or instance token
SSH_PUBLIC_KEY SSH public key for the Flatcar core user
Optional environment variables:
VM_NAME VM name, default flatcar-gitea-runner
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
FLATCAR_IMAGE_URL Flatcar image URL, default Stable amd64 current image
FLATCAR_IMAGE_SHA256 Optional expected SHA256 for the downloaded image
IMAGE_CACHE_DIR Host image cache, default /var/lib/vz/template/cache
GITEA_RUNNER_NAME Runner name, default flatcar-gitea-runner-VM_ID
GITEA_RUNNER_LABELS Runner labels, default ubuntu-latest:docker://node:20-bookworm
GITEA_RUNNER_IMAGE Runner image, default docker.io/gitea/act_runner:latest
The script uses DHCP for the VM network. The generated VM receives an Ignition
configuration through Proxmox's user-data snippet mechanism. Do not reuse the
snippet file or VM disk without removing the registration token first.
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}"
: "${GITEA_INSTANCE_URL:?GITEA_INSTANCE_URL is required}"
: "${GITEA_RUNNER_REGISTRATION_TOKEN:?GITEA_RUNNER_REGISTRATION_TOKEN is required}"
: "${SSH_PUBLIC_KEY:?SSH_PUBLIC_KEY is required}"
VM_NAME="${VM_NAME:-flatcar-gitea-runner}"
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}"
FLATCAR_IMAGE_URL="${FLATCAR_IMAGE_URL:-https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_proxmoxve_image.img}"
IMAGE_CACHE_DIR="${IMAGE_CACHE_DIR:-/var/lib/vz/template/cache}"
GITEA_RUNNER_NAME="${GITEA_RUNNER_NAME:-flatcar-gitea-runner-${VM_ID}}"
GITEA_RUNNER_LABELS="${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:20-bookworm}"
GITEA_RUNNER_IMAGE="${GITEA_RUNNER_IMAGE:-docker.io/gitea/act_runner:latest}"
[[ "$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"
[[ "$GITEA_INSTANCE_URL" =~ ^https?:// ]] || die "GITEA_INSTANCE_URL must begin with http:// or https://"
if qm status "$VM_ID" >/dev/null 2>&1; then
die "VM $VM_ID already exists; choose another VM_ID"
fi
storage_config="$(pvesm config "$SNIPPET_STORAGE" 2>/dev/null || true)"
grep -Eq '(^|,)snippets(,|$)' <<<"$storage_config" || die "storage $SNIPPET_STORAGE is not configured for snippets. Enable it with: pvesm set $SNIPPET_STORAGE --content snippets"
mkdir -p "$SNIPPET_DIR" "$IMAGE_CACHE_DIR"
chmod 0750 "$SNIPPET_DIR"
work_dir="$(mktemp -d /tmp/flatcar-runner.XXXXXX)"
cleanup() {
rm -rf "$work_dir"
}
trap cleanup EXIT
image_path="$IMAGE_CACHE_DIR/$(basename "$FLATCAR_IMAGE_URL")"
snippet_name="flatcar-gitea-runner-${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
echo "Checking Flatcar image SHA256"
printf '%s %s\n' "$FLATCAR_IMAGE_SHA256" "$image_path" | sha256sum --check --strict -
fi
runner_env="$work_dir/runner.env"
cat >"$runner_env" <<EOF
GITEA_INSTANCE_URL=$GITEA_INSTANCE_URL
GITEA_RUNNER_REGISTRATION_TOKEN=$GITEA_RUNNER_REGISTRATION_TOKEN
GITEA_RUNNER_NAME=$GITEA_RUNNER_NAME
GITEA_RUNNER_LABELS=$GITEA_RUNNER_LABELS
EOF
runner_unit="$work_dir/gitea-runner.service"
cat >"$runner_unit" <<EOF
[Unit]
Description=Gitea Actions runner container
After=docker.service
Requires=docker.service
[Service]
Type=simple
EnvironmentFile=/var/lib/gitea-runner/runner.env
ExecStartPre=-/usr/bin/docker rm --force gitea-runner
ExecStart=/usr/bin/docker run --name gitea-runner --rm \\
--env GITEA_INSTANCE_URL \\
--env GITEA_RUNNER_REGISTRATION_TOKEN \\
--env GITEA_RUNNER_NAME \\
--env GITEA_RUNNER_LABELS \\
--env-file /var/lib/gitea-runner/runner.env \\
--volume /var/run/docker.sock:/var/run/docker.sock \\
--volume /var/lib/gitea-runner:/data \\
$GITEA_RUNNER_IMAGE
ExecStop=/usr/bin/docker stop --time 30 gitea-runner
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
EOF
jq -n \
--arg ssh_key "$SSH_PUBLIC_KEY" \
--arg hostname "$VM_NAME" \
--rawfile runner_env "$runner_env" \
--rawfile runner_unit "$runner_unit" \
'{
ignition: {version: "3.3.0"},
storage: {
directories: [
{path: "/var/lib/gitea-runner", mode: 448}
],
files: [
{path: "/etc/hostname", mode: 420, contents: {source: ("data:text/plain;charset=utf-8," + ($hostname | @uri))}},
{path: "/var/lib/gitea-runner/runner.env", mode: 384, user: {name: "root"}, group: {name: "root"}, contents: {source: ("data:text/plain;charset=utf-8," + ($runner_env | @uri))}}
]
},
systemd: {
units: [
{name: "docker.service", enabled: true},
{name: "gitea-runner.service", enabled: true, contents: $runner_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}"
echo "Starting VM $VM_ID"
qm start "$VM_ID"
cat <<EOF
Flatcar Gitea runner VM created.
VM ID: $VM_ID
VM name: $VM_NAME
Runner name: $GITEA_RUNNER_NAME
Image: $FLATCAR_IMAGE_URL
Snippet: $snippet_path
Check the VM console or DHCP lease, then verify:
ssh core@<vm-ip> 'systemctl status docker gitea-runner --no-pager'
ssh core@<vm-ip> 'docker ps'
Security note: the runner mounts /var/run/docker.sock so workflow jobs can
launch Docker containers. Treat this VM as a dedicated CI trust boundary.
EOF