secubox-openwrt/.github/workflows/build-vm-appliance.yml
CyberMind-FR 4b72126784 fix(ci): Handle gunzip trailing garbage warning in VM build
OpenWrt firmware images contain trailing data that gunzip reports
as "trailing garbage" with exit code 2. This is normal and the
extracted image is valid. The fix ignores the warning while still
checking that extraction produced output.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-17 17:47:36 +01:00

519 lines
18 KiB
YAML

name: Build SecuBox VM Appliance
# Builds ready-to-use VM images (VMDK, VDI, QCOW2) for VMware, VirtualBox, Proxmox
on:
workflow_dispatch:
inputs:
openwrt_version:
description: 'OpenWrt version'
required: true
default: '24.10.5'
type: choice
options:
- '24.10.5'
- '24.10.4'
- '23.05.5'
disk_size:
description: 'Virtual disk size (GB)'
required: true
default: '2'
type: choice
options:
- '1'
- '2'
- '4'
- '8'
memory:
description: 'Recommended RAM (GB)'
required: true
default: '1'
type: choice
options:
- '512M'
- '1'
- '2'
- '4'
push:
tags:
- 'v*.*.*'
- 'v*.*.*-*'
env:
OPENWRT_VERSION: ${{ github.event.inputs.openwrt_version || '24.10.5' }}
DISK_SIZE: ${{ github.event.inputs.disk_size || '2' }}
permissions:
contents: write
jobs:
build-vm:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: x86-64-efi
boot: efi
description: "x86/64 EFI (Modern UEFI systems)"
- name: x86-64-bios
boot: bios
description: "x86/64 BIOS (Legacy systems)"
name: VM ${{ matrix.description }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
sudo docker image prune --all --force
df -h
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libncurses5-dev zlib1g-dev \
gawk git gettext libssl-dev xsltproc rsync wget unzip python3 \
qemu-utils parted dosfstools e2fsprogs
- name: Download Image Builder
run: |
VERSION="${{ env.OPENWRT_VERSION }}"
IB_URL="https://downloads.openwrt.org/releases/${VERSION}/targets/x86/64/openwrt-imagebuilder-${VERSION}-x86-64.Linux-x86_64.tar.zst"
echo "📥 Downloading Image Builder..."
wget -q "$IB_URL" -O imagebuilder.tar.zst 2>/dev/null || {
IB_URL="${IB_URL%.zst}.xz"
wget -q "$IB_URL" -O imagebuilder.tar.xz
tar -xf imagebuilder.tar.xz
mv openwrt-imagebuilder-* imagebuilder
}
if [[ -f imagebuilder.tar.zst ]]; then
tar --zstd -xf imagebuilder.tar.zst
mv openwrt-imagebuilder-* imagebuilder
fi
echo "✅ Image Builder ready"
- name: Create preseed configuration
run: |
mkdir -p imagebuilder/files/etc/uci-defaults
mkdir -p imagebuilder/files/etc/secubox
# Preseed script for first boot
cat > imagebuilder/files/etc/uci-defaults/99-secubox-vm << 'PRESEED_EOF'
#!/bin/sh
# SecuBox VM Appliance Preseed
# Set hostname
uci set system.@system[0].hostname='secubox-vm'
uci set system.@system[0].timezone='UTC'
uci set system.@system[0].zonename='UTC'
# Configure network for VM
# LAN: br-lan on eth0
uci set network.lan.ipaddr='192.168.1.1'
uci set network.lan.netmask='255.255.255.0'
uci set network.lan.proto='static'
# WAN: DHCP on eth1 (if present)
uci set network.wan=interface
uci set network.wan.device='eth1'
uci set network.wan.proto='dhcp'
# Enable DHCP server on LAN
uci set dhcp.lan.start='100'
uci set dhcp.lan.limit='150'
uci set dhcp.lan.leasetime='12h'
# Firewall: secure defaults
uci set firewall.@zone[0].input='ACCEPT'
uci set firewall.@zone[0].output='ACCEPT'
uci set firewall.@zone[0].forward='REJECT'
uci set firewall.@zone[1].input='REJECT'
uci set firewall.@zone[1].output='ACCEPT'
uci set firewall.@zone[1].forward='REJECT'
uci set firewall.@zone[1].masq='1'
# Enable HTTPS for LuCI
uci set uhttpd.main.redirect_https='1'
# Commit changes
uci commit
# Expand root filesystem on first boot
if [ ! -f /etc/secubox/resized ]; then
# Detect root partition
ROOT_DEV=$(mount | grep ' / ' | cut -d' ' -f1)
if [ -n "$ROOT_DEV" ]; then
# Get disk device (remove partition number)
DISK_DEV=$(echo "$ROOT_DEV" | sed 's/[0-9]*$//')
PART_NUM=$(echo "$ROOT_DEV" | grep -o '[0-9]*$')
# Resize partition if possible
if command -v parted >/dev/null 2>&1; then
parted -s "$DISK_DEV" resizepart "$PART_NUM" 100% 2>/dev/null || true
fi
# Resize filesystem
if command -v resize2fs >/dev/null 2>&1; then
resize2fs "$ROOT_DEV" 2>/dev/null || true
fi
mkdir -p /etc/secubox
touch /etc/secubox/resized
fi
fi
# Mark as configured
touch /etc/secubox/configured
exit 0
PRESEED_EOF
chmod 755 imagebuilder/files/etc/uci-defaults/99-secubox-vm
# SecuBox release info
cat > imagebuilder/files/etc/secubox/release << EOF
SECUBOX_VERSION="${{ github.ref_name || 'dev' }}"
SECUBOX_BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
OPENWRT_VERSION="${{ env.OPENWRT_VERSION }}"
VM_TYPE="${{ matrix.boot }}"
DISK_SIZE="${{ env.DISK_SIZE }}GB"
EOF
# MOTD banner
cat > imagebuilder/files/etc/banner << 'BANNER_EOF'
____ ____
/ ___| ___ ___ _ _| __ ) _____ __
\___ \ / _ \/ __| | | | _ \ / _ \ \/ /
___) | __/ (__| |_| | |_) | (_) > <
|____/ \___|\___|\__,_|____/ \___/_/\_\
SecuBox OpenWrt Security Appliance
https://github.com/gkerma/secubox-openwrt
Access LuCI: https://192.168.1.1
Documentation: https://github.com/gkerma/secubox-openwrt/wiki
BANNER_EOF
echo "✅ Preseed configuration created"
- name: Build firmware image
run: |
cd imagebuilder
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔨 Building SecuBox VM Image (${{ matrix.boot }})"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Base packages
PACKAGES="luci luci-ssl luci-app-opkg luci-theme-openwrt-2020"
PACKAGES="$PACKAGES curl wget-ssl htop iftop tcpdump"
PACKAGES="$PACKAGES openssh-sftp-server"
PACKAGES="$PACKAGES block-mount kmod-fs-ext4 kmod-fs-vfat"
PACKAGES="$PACKAGES parted e2fsprogs resize2fs"
PACKAGES="$PACKAGES qemu-ga" # QEMU guest agent for Proxmox
# Remove conflicting dnsmasq
PACKAGES="$PACKAGES -dnsmasq dnsmasq-full"
# EFI-specific packages
if [[ "${{ matrix.boot }}" == "efi" ]]; then
PACKAGES="$PACKAGES grub2-efi"
fi
# Calculate root partition size (disk size minus 64MB for boot)
ROOT_SIZE=$(( ${{ env.DISK_SIZE }} * 1024 - 64 ))
echo "📦 Packages: $PACKAGES"
echo "💾 Root partition: ${ROOT_SIZE}MB"
echo ""
# Build with combined-efi or ext4-combined based on boot type
if [[ "${{ matrix.boot }}" == "efi" ]]; then
PROFILE="generic"
else
PROFILE="generic"
fi
make image \
PROFILE="$PROFILE" \
PACKAGES="$PACKAGES" \
FILES="files" \
ROOTFS_PARTSIZE="$ROOT_SIZE" \
2>&1 | tee build.log
echo ""
echo "📦 Generated images:"
ls -lh bin/targets/x86/64/
- name: Convert to VM formats
run: |
mkdir -p artifacts
TARGET_DIR="imagebuilder/bin/targets/x86/64"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔄 Converting to VM formats"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Find the correct image based on boot type
if [[ "${{ matrix.boot }}" == "efi" ]]; then
IMG_PATTERN="*-combined-efi.img.gz"
else
IMG_PATTERN="*-combined-ext4.img.gz"
fi
# Find and extract the image
IMG_FILE=$(find "$TARGET_DIR" -name "$IMG_PATTERN" 2>/dev/null | head -1)
if [[ -z "$IMG_FILE" ]]; then
# Fallback to any combined image
IMG_FILE=$(find "$TARGET_DIR" -name "*combined*.img.gz" 2>/dev/null | head -1)
fi
if [[ -z "$IMG_FILE" ]]; then
echo "❌ No firmware image found!"
ls -la "$TARGET_DIR/"
exit 1
fi
echo "📦 Source image: $IMG_FILE"
# Extract (ignore "trailing garbage" warning - normal for firmware images)
# gunzip returns exit code 2 for warnings, which we can safely ignore
gunzip -c "$IMG_FILE" > /tmp/openwrt.img 2>/dev/null || {
# Check if extraction actually produced output
if [[ ! -s /tmp/openwrt.img ]]; then
echo "❌ Failed to extract image"
exit 1
fi
echo " (gunzip warning ignored - normal for firmware images)"
}
IMG_SIZE=$(stat -c%s /tmp/openwrt.img)
echo " Size: $(numfmt --to=iec $IMG_SIZE)"
# Expand to target disk size
TARGET_BYTES=$(( ${{ env.DISK_SIZE }} * 1024 * 1024 * 1024 ))
if [[ $IMG_SIZE -lt $TARGET_BYTES ]]; then
echo "📏 Expanding to ${{ env.DISK_SIZE }}GB..."
truncate -s ${TARGET_BYTES} /tmp/openwrt.img
fi
# Base filename
VERSION="${{ env.OPENWRT_VERSION }}"
TAG="${{ github.ref_name }}"
BASENAME="secubox-vm-${TAG:-dev}-${{ matrix.name }}"
# Convert to VMDK (VMware)
echo "🔄 Creating VMDK (VMware)..."
qemu-img convert -f raw -O vmdk /tmp/openwrt.img "artifacts/${BASENAME}.vmdk"
echo " ✅ ${BASENAME}.vmdk ($(du -h "artifacts/${BASENAME}.vmdk" | cut -f1))"
# Convert to VDI (VirtualBox)
echo "🔄 Creating VDI (VirtualBox)..."
qemu-img convert -f raw -O vdi /tmp/openwrt.img "artifacts/${BASENAME}.vdi"
echo " ✅ ${BASENAME}.vdi ($(du -h "artifacts/${BASENAME}.vdi" | cut -f1))"
# Convert to QCOW2 (Proxmox/KVM)
echo "🔄 Creating QCOW2 (Proxmox/KVM)..."
qemu-img convert -f raw -O qcow2 -c /tmp/openwrt.img "artifacts/${BASENAME}.qcow2"
echo " ✅ ${BASENAME}.qcow2 ($(du -h "artifacts/${BASENAME}.qcow2" | cut -f1))"
# Keep raw image compressed
echo "🔄 Compressing raw image..."
gzip -c /tmp/openwrt.img > "artifacts/${BASENAME}.img.gz"
echo " ✅ ${BASENAME}.img.gz ($(du -h "artifacts/${BASENAME}.img.gz" | cut -f1))"
rm /tmp/openwrt.img
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ VM images created successfully"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- name: Create documentation
run: |
TAG="${{ github.ref_name }}"
BASENAME="secubox-vm-${TAG:-dev}-${{ matrix.name }}"
cat > artifacts/README.md << EOF
# SecuBox VM Appliance - ${{ matrix.description }}
Pre-configured OpenWrt ${{ env.OPENWRT_VERSION }} virtual machine with SecuBox modules.
## VM Images
| Format | File | Platform |
|--------|------|----------|
| VMDK | \`${BASENAME}.vmdk\` | VMware Workstation/ESXi |
| VDI | \`${BASENAME}.vdi\` | VirtualBox |
| QCOW2 | \`${BASENAME}.qcow2\` | Proxmox/KVM/QEMU |
| Raw | \`${BASENAME}.img.gz\` | Any hypervisor |
## Quick Start
### VMware
1. Create new VM → Other Linux 64-bit
2. Use existing disk → Select \`.vmdk\` file
3. RAM: ${{ github.event.inputs.memory || '1' }}GB minimum
4. Network: Bridged or NAT
### VirtualBox
1. Create new VM → Linux → Other Linux 64-bit
2. Use existing disk → Select \`.vdi\` file
3. RAM: ${{ github.event.inputs.memory || '1' }}GB minimum
4. Network: Bridged Adapter or NAT
### Proxmox
\`\`\`bash
# Upload QCOW2 to Proxmox
qm create 100 --name secubox --memory 1024 --net0 virtio,bridge=vmbr0
qm importdisk 100 ${BASENAME}.qcow2 local-lvm
qm set 100 --scsi0 local-lvm:vm-100-disk-0
qm set 100 --boot order=scsi0
qm start 100
\`\`\`
## Default Configuration
| Setting | Value |
|---------|-------|
| LAN IP | 192.168.1.1 |
| Username | root |
| Password | (none - set on first login) |
| Web UI | https://192.168.1.1 |
| SSH | Enabled |
## Network Interfaces
- **eth0 (LAN)**: 192.168.1.1/24, DHCP server
- **eth1 (WAN)**: DHCP client (optional)
## Disk Resize
The root filesystem will automatically expand on first boot.
For manual expansion:
\`\`\`bash
parted /dev/sda resizepart 2 100%
resize2fs /dev/sda2
\`\`\`
## Build Information
- OpenWrt: ${{ env.OPENWRT_VERSION }}
- Boot: ${{ matrix.boot }}
- Disk: ${{ env.DISK_SIZE }}GB
- Built: $(date -u +%Y-%m-%dT%H:%M:%SZ)
EOF
# Create checksums
cd artifacts
sha256sum *.vmdk *.vdi *.qcow2 *.img.gz > SHA256SUMS
echo "📋 Artifacts ready:"
ls -lh
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: secubox-vm-${{ matrix.name }}-${{ env.OPENWRT_VERSION }}
path: artifacts/
retention-days: 30
- name: Generate summary
run: |
echo "# 🖥️ VM Appliance: ${{ matrix.description }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Format | Size |" >> $GITHUB_STEP_SUMMARY
echo "|--------|------|" >> $GITHUB_STEP_SUMMARY
for f in artifacts/*.vmdk artifacts/*.vdi artifacts/*.qcow2 artifacts/*.img.gz; do
if [[ -f "$f" ]]; then
echo "| $(basename "$f") | $(du -h "$f" | cut -f1) |" >> $GITHUB_STEP_SUMMARY
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Disk size:** ${{ env.DISK_SIZE }}GB" >> $GITHUB_STEP_SUMMARY
echo "**Boot type:** ${{ matrix.boot }}" >> $GITHUB_STEP_SUMMARY
# ============================================
# Create release
# ============================================
release:
needs: build-vm
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: vms
pattern: secubox-vm-*
- name: Organize release
run: |
mkdir -p release
for dir in vms/secubox-vm-*/; do
cp "$dir"/*.vmdk "$dir"/*.vdi "$dir"/*.qcow2 "$dir"/*.img.gz release/ 2>/dev/null || true
cp "$dir"/README.md release/VM-README.md 2>/dev/null || true
done
cd release
sha256sum *.vmdk *.vdi *.qcow2 *.img.gz > SHA256SUMS 2>/dev/null || true
cat > RELEASE_NOTES.md << 'EOF'
# SecuBox VM Appliance
Ready-to-use virtual machine images with SecuBox security modules pre-installed.
## Downloads
Choose the format for your hypervisor:
- **VMDK** - VMware Workstation, ESXi, Fusion
- **VDI** - VirtualBox
- **QCOW2** - Proxmox, KVM, QEMU
## Quick Start
1. Download the appropriate image for your hypervisor
2. Import/create VM with the disk image
3. Boot and access https://192.168.1.1
4. Login as `root` (no password initially)
5. Set a password and start configuring
## System Requirements
- 1GB RAM minimum (2GB recommended)
- 1 vCPU minimum
- Network adapter (bridged or NAT)
EOF
- name: Create release
uses: softprops/action-gh-release@v2
with:
name: "SecuBox VM Appliance ${{ github.ref_name }}"
tag_name: ${{ github.ref_name }}
body_path: release/RELEASE_NOTES.md
files: |
release/*.vmdk
release/*.vdi
release/*.qcow2
release/*.img.gz
release/SHA256SUMS
draft: false
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}