Add Proxmox ISO builder, CSRF protection, boot-files integration

- Add build-proxmox-iso.sh: remaster Ubuntu ISO with autoinstall config,
  offline packages, playbook, webapp, and boot files for zero-touch
  Proxmox VM deployment
- Add boot-files/ directory for WinPE boot files (wimboot, boot.wim,
  BCD, ipxe.efi, etc.) sourced from WestJeff playbook
- Update build-usb.sh and test-vm.sh to bundle boot-files automatically
- Add usb_root variable to playbook, fix all file copy paths to use it
- Unify Apache VirtualHost config (merge default site + webapp proxy)
- Add CSRF token protection to all webapp POST forms and API endpoints
- Update README with Proxmox deployment instructions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-09 20:01:19 -05:00
parent cb442f971b
commit f3a384fa1a
14 changed files with 492 additions and 32 deletions

3
.gitignore vendored
View File

@@ -25,6 +25,9 @@ offline-packages/
# Boot tool binaries (built by prepare-boot-tools.sh)
boot-tools/
# WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.)
boot-files/
# Python wheels for offline install (built by download-packages.sh)
pip-wheels/

View File

@@ -157,6 +157,7 @@ pxe-server/
├── download-packages.sh # Downloads offline .debs + pip wheels
├── build-usb.sh # Builds the installer USB (2-partition)
├── prepare-boot-tools.sh # Extracts and patches boot tool files
├── build-proxmox-iso.sh # Builds self-contained Proxmox installer ISO
├── test-vm.sh # KVM test environment for validation
├── SETUP.md # Detailed setup guide
└── setup-guide-original.txt # Original manual setup notes (reference)
@@ -183,6 +184,41 @@ sudo ./test-vm.sh --destroy
The test VM creates an isolated libvirt network (10.9.100.0/24) and runs the full autoinstall + Ansible provisioning.
## Proxmox Deployment
A single ISO can be built for deploying the PXE server in a Proxmox VM:
### Build the ISO
```bash
# Prerequisites (on build workstation)
sudo apt install xorriso p7zip-full
# Build the installer ISO
./build-proxmox-iso.sh /path/to/ubuntu-24.04-live-server-amd64.iso
```
This creates `pxe-server-proxmox.iso` containing the Ubuntu installer, autoinstall config, all offline packages, the Ansible playbook, webapp, and boot tools.
### Deploy on Proxmox
1. Upload `pxe-server-proxmox.iso` to Proxmox storage (Datacenter -> Storage -> ISO Images)
2. Create a new VM:
- **OS:** Linux 6.x kernel
- **BIOS:** OVMF (UEFI) or SeaBIOS
- **Memory:** 4096 MB
- **CPU:** 2+ cores
- **Disk:** 40+ GB (VirtIO SCSI)
- **Network:** Bridge connected to your isolated PXE network
3. Attach the ISO as CD-ROM and start the VM
4. Ubuntu auto-installs with zero interaction (~10-15 minutes)
5. After reboot, first-boot configures all PXE services automatically
6. Access the web interface at `http://10.9.100.1:9009`
### Import WinPE Images
After the server is running, import deployment images via the web interface at `http://10.9.100.1:9009/import` or by mounting a USB drive with WinPE content.
## Samba Shares
| Share | Path | Purpose |
@@ -203,13 +239,10 @@ Blancco Drive Eraser is configured to automatically save XML erasure reports to
Reports are viewable and downloadable from the web interface at `http://10.9.100.1:9009/reports`.
## Known Issues / TODO
## Notes
- **wimtools** must be downloaded with `download-packages.sh` before building USB (used for startnet.cmd editing)
- **Apache VirtualHost conflict**: Two VirtualHosts on port 80 (default site and pxe-webapp proxy) — should disable default or merge
- **WinPE boot files** (wimboot, BCD, boot.sdi, bootx64.efi, boot.stl, boot.wim) must be manually placed on USB or sourced from the legacy `WestJeff` playbook folder
- **CSRF protection** not yet implemented on webapp POST forms
- Test VM requires re-download of Ubuntu ISO if `--destroy` is run (fixed in latest test-vm.sh)
- Run `download-packages.sh` before building USB — it downloads all offline `.deb` packages including wimtools (needed for startnet.cmd editing)
- The webapp uses session-based CSRF tokens on all POST forms and API endpoints
## Commit History

348
build-proxmox-iso.sh Executable file
View File

@@ -0,0 +1,348 @@
#!/bin/bash
#
# build-proxmox-iso.sh — Build a self-contained PXE server installer ISO for Proxmox
#
# Repackages the Ubuntu 24.04 Server ISO with:
# - Autoinstall configuration (zero-touch install)
# - All offline .deb packages and Python wheels
# - Ansible playbook, Flask webapp, and boot tools
#
# The resulting ISO can be uploaded to Proxmox, attached to a VM, and booted.
# Ubuntu auto-installs, then first-boot configures all PXE services automatically.
#
# Usage:
# ./build-proxmox-iso.sh /path/to/ubuntu-24.04-live-server-amd64.iso [output.iso]
#
# Prerequisites (on build workstation):
# sudo apt install xorriso p7zip-full
#
# Before building, run:
# ./download-packages.sh (downloads offline .debs + pip wheels)
# ./prepare-boot-tools.sh ... (extracts Clonezilla, Blancco, Memtest)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
AUTOINSTALL_DIR="$SCRIPT_DIR/autoinstall"
PLAYBOOK_DIR="$SCRIPT_DIR/playbook"
OFFLINE_PKG_DIR="$SCRIPT_DIR/offline-packages"
WEBAPP_DIR="$SCRIPT_DIR/webapp"
PIP_WHEELS_DIR="$SCRIPT_DIR/pip-wheels"
BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
# --- Validate arguments ---
if [ $# -lt 1 ]; then
echo "Usage: $0 /path/to/ubuntu-24.04-live-server-amd64.iso [output.iso]"
echo ""
echo " Creates a self-contained ISO for deploying the PXE server in Proxmox."
echo " The ISO auto-installs Ubuntu and configures all PXE services."
echo ""
echo "Prerequisites:"
echo " sudo apt install xorriso p7zip-full"
exit 1
fi
UBUNTU_ISO="$(realpath "$1")"
OUTPUT_ISO="${2:-$SCRIPT_DIR/pxe-server-proxmox.iso}"
# --- Validate prerequisites ---
echo "============================================"
echo "PXE Server Proxmox ISO Builder"
echo "============================================"
echo ""
MISSING_CMDS=()
for cmd in xorriso 7z; do
if ! command -v "$cmd" &>/dev/null; then
MISSING_CMDS+=("$cmd")
fi
done
if [ ${#MISSING_CMDS[@]} -gt 0 ]; then
echo "ERROR: Missing required tools: ${MISSING_CMDS[*]}"
echo "Install with: sudo apt install xorriso p7zip-full"
exit 1
fi
if [ ! -f "$UBUNTU_ISO" ]; then
echo "ERROR: ISO not found at $UBUNTU_ISO"
exit 1
fi
# Quick sanity check: ensure it looks like an Ubuntu Server ISO
ISO_CONTENTS=$(7z l "$UBUNTU_ISO" 2>&1) || true
if ! echo "$ISO_CONTENTS" | grep -q "casper/vmlinuz"; then
echo "ERROR: Does not appear to be an Ubuntu Server ISO (missing casper/vmlinuz)"
exit 1
fi
if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then
echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data"
exit 1
fi
if [ ! -f "$PLAYBOOK_DIR/pxe_server_setup.yml" ]; then
echo "ERROR: pxe_server_setup.yml not found at $PLAYBOOK_DIR/"
exit 1
fi
echo "Ubuntu ISO : $UBUNTU_ISO"
echo "Output ISO : $OUTPUT_ISO"
echo "Source Dir : $SCRIPT_DIR"
echo ""
# --- Setup work directory with cleanup trap ---
WORK_DIR=$(mktemp -d)
cleanup() { rm -rf "$WORK_DIR"; }
trap cleanup EXIT
EXTRACT_DIR="$WORK_DIR/iso"
BOOT_IMG_DIR="$WORK_DIR/BOOT"
# --- Step 1: Extract Ubuntu ISO ---
echo "[1/6] Extracting Ubuntu ISO..."
7z x -o"$EXTRACT_DIR" "$UBUNTU_ISO" -y >/dev/null 2>&1
# 7z extracts [BOOT] directory containing EFI images needed for rebuild
# Move it out so it doesn't end up in the final ISO filesystem
if [ -d "$EXTRACT_DIR/[BOOT]" ]; then
mv "$EXTRACT_DIR/[BOOT]" "$BOOT_IMG_DIR"
echo " Extracted boot images for BIOS + UEFI"
else
echo "ERROR: [BOOT] directory not found in extracted ISO"
echo " The Ubuntu ISO may be corrupted or an unsupported version."
exit 1
fi
# Ensure files are writable (ISO extraction may set read-only)
chmod -R u+w "$EXTRACT_DIR"
# --- Step 2: Generate autoinstall user-data ---
echo "[2/6] Generating autoinstall configuration..."
mkdir -p "$EXTRACT_DIR/server"
touch "$EXTRACT_DIR/server/meta-data"
# Reuse the common sections (identity, network, storage, SSH) from existing user-data
# and replace late-commands with ISO-specific versions
sed '/^ late-commands:/,$d' "$AUTOINSTALL_DIR/user-data" > "$EXTRACT_DIR/server/user-data"
# Append ISO-specific late-commands
cat >> "$EXTRACT_DIR/server/user-data" << 'LATE_COMMANDS'
late-commands:
# Copy project files from ISO (/cdrom/pxe-data/) to the installed system
- mkdir -p /target/opt/pxe-setup
- cp -r /cdrom/pxe-data/packages /target/opt/pxe-setup/ 2>/dev/null || true
- cp -r /cdrom/pxe-data/playbook /target/opt/pxe-setup/ 2>/dev/null || true
- cp -r /cdrom/pxe-data/webapp /target/opt/pxe-setup/ 2>/dev/null || true
- cp -r /cdrom/pxe-data/pip-wheels /target/opt/pxe-setup/ 2>/dev/null || true
- cp -r /cdrom/pxe-data/boot-tools /target/opt/pxe-setup/ 2>/dev/null || true
# Copy boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) from pxe-data root
- sh -c 'for f in /cdrom/pxe-data/*; do [ -f "$f" ] && cp "$f" /target/opt/pxe-setup/; done' || true
# Install deb packages in target chroot
- |
curtin in-target --target=/target -- bash -c '
if compgen -G "/opt/pxe-setup/packages/*.deb" > /dev/null; then
dpkg -i /opt/pxe-setup/packages/*.deb 2>/dev/null || true
dpkg -i /opt/pxe-setup/packages/*.deb 2>/dev/null || true
if command -v nmcli >/dev/null; then
systemctl enable NetworkManager
fi
fi
'
# Create first-boot script (reads from local /opt/pxe-setup/)
- |
curtin in-target --target=/target -- bash -c '
cat <<"EOF" > /opt/first-boot.sh
#!/bin/bash
SRC=/opt/pxe-setup
# Install all offline .deb packages
if compgen -G "$SRC/packages/*.deb" > /dev/null; then
dpkg -i $SRC/packages/*.deb 2>/dev/null || true
dpkg -i $SRC/packages/*.deb 2>/dev/null || true
fi
# Run the Ansible playbook (override USB paths to local source)
if [ -f $SRC/playbook/pxe_server_setup.yml ]; then
cd $SRC/playbook
ansible-playbook -i localhost, -c local pxe_server_setup.yml \
-e usb_root=$SRC -e usb_mount=$SRC/playbook
fi
# Disable rc.local to prevent rerunning
sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local
lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv || true
# Clean up large setup files to save disk space
rm -rf $SRC/packages $SRC/pip-wheels $SRC/boot-tools
rm -f $SRC/boot.wim $SRC/boot.sdi $SRC/bootx64.efi $SRC/wimboot $SRC/ipxe.efi $SRC/BCD $SRC/boot.stl
EOF
'
- curtin in-target --target=/target -- chmod +x /opt/first-boot.sh
# Create rc.local to run first-boot on next startup
- |
curtin in-target --target=/target -- bash -c '
cat <<"EOF" > /etc/rc.local
#!/bin/bash
/opt/first-boot.sh > /var/log/first-boot.log 2>&1 &
exit 0
EOF
'
- curtin in-target --target=/target -- chmod +x /etc/rc.local
user-data:
disable_root: false
refresh-installer:
update: no
LATE_COMMANDS
echo " Generated server/user-data and server/meta-data"
# --- Step 3: Copy project files to pxe-data/ ---
echo "[3/6] Copying project files to ISO..."
PXE_DATA="$EXTRACT_DIR/pxe-data"
mkdir -p "$PXE_DATA"
# Offline .deb packages
if [ -d "$OFFLINE_PKG_DIR" ]; then
mkdir -p "$PXE_DATA/packages"
DEB_COUNT=0
for deb in "$OFFLINE_PKG_DIR"/*.deb; do
if [ -f "$deb" ]; then
cp "$deb" "$PXE_DATA/packages/"
DEB_COUNT=$((DEB_COUNT + 1))
fi
done
echo " Copied $DEB_COUNT .deb packages"
else
echo " WARNING: No offline-packages/ directory. Run download-packages.sh first."
fi
# Ansible playbook
mkdir -p "$PXE_DATA/playbook"
cp "$PLAYBOOK_DIR/"* "$PXE_DATA/playbook/" 2>/dev/null || true
echo " Copied playbook/"
# Flask webapp
if [ -d "$WEBAPP_DIR" ]; then
mkdir -p "$PXE_DATA/webapp"
cp "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$PXE_DATA/webapp/"
cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$PXE_DATA/webapp/"
echo " Copied webapp/"
fi
# Python wheels
if [ -d "$PIP_WHEELS_DIR" ]; then
cp -r "$PIP_WHEELS_DIR" "$PXE_DATA/pip-wheels"
echo " Copied pip-wheels/"
else
echo " WARNING: No pip-wheels/ found (run download-packages.sh first)"
fi
# WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.)
BOOT_FILES_DIR="$SCRIPT_DIR/boot-files"
if [ -d "$BOOT_FILES_DIR" ]; then
BOOT_FILE_COUNT=0
for bf in "$BOOT_FILES_DIR"/*; do
if [ -f "$bf" ]; then
cp "$bf" "$PXE_DATA/"
BOOT_FILE_COUNT=$((BOOT_FILE_COUNT + 1))
fi
done
BOOT_FILES_SIZE=$(du -sh "$BOOT_FILES_DIR" | cut -f1)
echo " Copied $BOOT_FILE_COUNT boot files ($BOOT_FILES_SIZE) — wimboot, boot.wim, ipxe.efi, etc."
else
echo " WARNING: No boot-files/ found (copy WinPE boot files from Media Creator)"
fi
# Boot tools (Clonezilla, Blancco, Memtest)
if [ -d "$BOOT_TOOLS_DIR" ]; then
cp -r "$BOOT_TOOLS_DIR" "$PXE_DATA/boot-tools"
TOOLS_SIZE=$(du -sh "$PXE_DATA/boot-tools" | cut -f1)
echo " Copied boot-tools/ ($TOOLS_SIZE)"
else
echo " No boot-tools/ found (run prepare-boot-tools.sh first)"
fi
# --- Step 4: Modify GRUB for autoinstall ---
echo "[4/6] Configuring autoinstall boot..."
GRUB_CFG="$EXTRACT_DIR/boot/grub/grub.cfg"
if [ ! -f "$GRUB_CFG" ]; then
echo "ERROR: boot/grub/grub.cfg not found in extracted ISO"
exit 1
fi
# Add autoinstall kernel parameter with nocloud datasource pointing to /cdrom/server/
# The semicolon must be escaped as \; in GRUB (it's a command separator)
# Apply to both regular and HWE kernels
sed -i 's|/casper/vmlinuz\b|/casper/vmlinuz autoinstall ds=nocloud\\;s=/cdrom/server/|g' "$GRUB_CFG"
sed -i 's|/casper/hwe-vmlinuz\b|/casper/hwe-vmlinuz autoinstall ds=nocloud\\;s=/cdrom/server/|g' "$GRUB_CFG"
# Reduce timeout for automatic boot (1 second instead of default 30)
sed -i 's/set timeout=.*/set timeout=1/' "$GRUB_CFG"
echo " Modified GRUB: autoinstall enabled, timeout=1s"
# --- Step 5: Rebuild ISO ---
echo "[5/6] Rebuilding ISO (this may take a few minutes)..."
# Verify required boot images exist
EFI_IMG="$BOOT_IMG_DIR/2-Boot-NoEmul.img"
if [ ! -f "$EFI_IMG" ]; then
echo "ERROR: EFI boot image not found at $EFI_IMG"
exit 1
fi
if [ ! -f "$EXTRACT_DIR/boot/grub/i386-pc/eltorito.img" ]; then
echo "ERROR: BIOS boot image not found at boot/grub/i386-pc/eltorito.img"
exit 1
fi
xorriso -as mkisofs -r \
-V 'PXE-SERVER' \
-o "$OUTPUT_ISO" \
--grub2-mbr --interval:local_fs:0s-15s:zero_mbrpt,zero_gpt:"$UBUNTU_ISO" \
--protective-msdos-label \
-partition_cyl_align off \
-partition_offset 16 \
--mbr-force-bootable \
-append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b "$EFI_IMG" \
-appended_part_as_gpt \
-iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \
-c '/boot.catalog' \
-b '/boot/grub/i386-pc/eltorito.img' \
-no-emul-boot -boot-load-size 4 -boot-info-table --grub2-boot-info \
-eltorito-alt-boot \
-e '--interval:appended_partition_2:::' \
-no-emul-boot \
"$EXTRACT_DIR"
# --- Step 6: Done ---
echo "[6/6] Cleaning up..."
ISO_SIZE=$(du -sh "$OUTPUT_ISO" | cut -f1)
echo ""
echo "============================================"
echo "Proxmox ISO build complete!"
echo "============================================"
echo ""
echo "Output: $OUTPUT_ISO ($ISO_SIZE)"
echo ""
echo "Proxmox deployment:"
echo " 1. Upload ISO to Proxmox storage (Datacenter -> Storage -> ISO Images)"
echo " 2. Create a new VM:"
echo " - BIOS: OVMF (UEFI) — or SeaBIOS (both work)"
echo " - Memory: 4096 MB"
echo " - CPU: 2+ cores"
echo " - Disk: 40+ GB (VirtIO SCSI)"
echo " - Network: Bridge connected to isolated PXE network"
echo " 3. Attach ISO as CD-ROM and start the VM"
echo " 4. Ubuntu auto-installs (~10-15 minutes, zero interaction)"
echo " 5. After reboot, first-boot configures all PXE services"
echo " 6. Access webapp at http://10.9.100.1:9009"
echo ""
echo "NOTE: The VM's network bridge must be connected to your isolated PXE"
echo " network. The server will use static IP 10.9.100.1/24."
echo ""

View File

@@ -194,6 +194,22 @@ else
echo " No pip-wheels/ found (run download-packages.sh first)"
fi
# Copy WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.)
BOOT_FILES_DIR="$SCRIPT_DIR/boot-files"
if [ -d "$BOOT_FILES_DIR" ]; then
BOOT_FILE_COUNT=0
for bf in "$BOOT_FILES_DIR"/*; do
if [ -f "$bf" ]; then
cp "$bf" "$MOUNT_POINT/"
BOOT_FILE_COUNT=$((BOOT_FILE_COUNT + 1))
fi
done
BOOT_FILES_SIZE=$(du -sh "$BOOT_FILES_DIR" | cut -f1)
echo " Copied $BOOT_FILE_COUNT boot files ($BOOT_FILES_SIZE) — wimboot, boot.wim, ipxe.efi, etc."
else
echo " WARNING: No boot-files/ found (copy WinPE boot files from Media Creator)"
fi
# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
BOOT_TOOLS_DIR="$SCRIPT_DIR/boot-tools"
if [ -d "$BOOT_TOOLS_DIR" ]; then

View File

@@ -31,7 +31,8 @@
tftp_dir: "/srv/tftp"
web_root: "/var/www/html"
samba_share: "/srv/samba/winpeapps"
usb_mount: "/mnt/usb/playbook" # where your USB is mounted
usb_mount: "/mnt/usb/playbook" # playbook location on USB
usb_root: "/mnt/usb" # CIDATA partition root
image_types:
- gea-standard
- gea-engineer
@@ -274,7 +275,7 @@
- name: "Copy WinPE & boot files from USB (skipped if not present)"
copy:
src: "{{ usb_mount }}/{{ item.src }}"
src: "{{ usb_root }}/{{ item.src }}"
dest: "{{ web_root }}/win11/{{ item.dest }}"
mode: '0644'
loop:
@@ -288,7 +289,7 @@
- name: "Copy iPXE binaries from USB (skipped if not present)"
copy:
src: "{{ usb_mount }}/{{ item }}"
src: "{{ usb_root }}/{{ item }}"
dest: "{{ tftp_dir }}/{{ item }}"
mode: '0755'
loop:
@@ -297,8 +298,7 @@
- name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)"
shell: >
cp -r "{{ usb_mount }}/../boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null ||
cp -r "{{ usb_mount }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true
cp -r "{{ usb_root }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true
loop:
- clonezilla
- blancco
@@ -306,12 +306,12 @@
- name: "Check for WinPE deployment content on USB"
stat:
path: "{{ usb_mount }}/images"
path: "{{ usb_root }}/images"
register: usb_images_dir
- name: "Import WinPE deployment content from USB (if present)"
shell: >
cp -rn "{{ usb_mount }}/images/{{ item }}/"* "{{ samba_share }}/{{ item }}/" 2>/dev/null || true
cp -rn "{{ usb_root }}/images/{{ item }}/"* "{{ samba_share }}/{{ item }}/" 2>/dev/null || true
loop: "{{ image_types }}"
when: usb_images_dir.stat.exists
@@ -359,8 +359,7 @@
- name: "Copy webapp from USB"
shell: >
cp -r "{{ usb_mount }}/../webapp/"* /opt/pxe-webapp/ 2>/dev/null ||
cp -r "{{ usb_mount }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true
cp -r "{{ usb_root }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true
args:
creates: /opt/pxe-webapp/app.py
@@ -370,7 +369,7 @@
shell: |
# Find the pip-wheels directory on the CIDATA mount
export WHEEL_DIR=""
for d in "{{ usb_mount }}/../pip-wheels" "{{ usb_mount }}/pip-wheels"; do
for d in "{{ usb_root }}/pip-wheels" "{{ usb_mount }}/pip-wheels"; do
if [ -d "$d" ] && compgen -G "$d/*.whl" > /dev/null; then
export WHEEL_DIR="$(cd "$d" && pwd)"
break
@@ -427,11 +426,17 @@
enabled: yes
daemon_reload: yes
- name: "Configure Apache reverse proxy for webapp"
- name: "Configure unified Apache site (static files + webapp proxy)"
copy:
dest: /etc/apache2/sites-available/pxe-webapp.conf
dest: /etc/apache2/sites-available/pxe-server.conf
content: |
<VirtualHost *:80>
DocumentRoot {{ web_root }}
<Directory "{{ web_root }}">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ProxyPreserveHost On
ProxyPass /manage http://127.0.0.1:9009/
ProxyPassReverse /manage http://127.0.0.1:9009/
@@ -442,10 +447,20 @@
args:
creates: /etc/apache2/mods-enabled/proxy.load
- name: "Enable webapp Apache site"
command: a2ensite pxe-webapp.conf
- name: "Disable default Apache site"
command: a2dissite 000-default.conf
args:
creates: /etc/apache2/sites-enabled/pxe-webapp.conf
removes: /etc/apache2/sites-enabled/000-default.conf
- name: "Enable unified PXE server site"
command: a2ensite pxe-server.conf
args:
creates: /etc/apache2/sites-enabled/pxe-server.conf
- name: "Reload Apache after site changes"
systemd:
name: apache2
state: reloaded
- name: "Configure static IP for PXE interface"
copy:

View File

@@ -22,7 +22,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VM_NAME="pxe-test"
VM_DISK="/var/lib/libvirt/images/${VM_NAME}.qcow2"
CIDATA_ISO="/tmp/${VM_NAME}-cidata.iso"
CIDATA_ISO="${SCRIPT_DIR}/.${VM_NAME}-cidata.iso"
VM_RAM=4096
VM_CPUS=2
VM_DISK_SIZE=40 # GB
@@ -32,7 +32,7 @@ if [ "${1:-}" = "--destroy" ]; then
echo "Destroying test environment..."
virsh destroy "$VM_NAME" 2>/dev/null || true
virsh undefine "$VM_NAME" 2>/dev/null || true
rm -f "$VM_DISK"
virsh vol-delete "${VM_NAME}.qcow2" --pool default 2>/dev/null || true
rm -f "$CIDATA_ISO"
rm -f "/tmp/${VM_NAME}-vmlinuz" "/tmp/${VM_NAME}-initrd"
echo "Done."
@@ -95,6 +95,14 @@ elif [ -d "$SCRIPT_DIR/offline-packages/pip-wheels" ]; then
echo " Copied pip-wheels/ (from offline-packages/)"
fi
# WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.)
if [ -d "$SCRIPT_DIR/boot-files" ]; then
for bf in "$SCRIPT_DIR/boot-files"/*; do
[ -f "$bf" ] && cp "$bf" "$CIDATA_DIR/"
done
echo " Copied boot-files/ (wimboot, boot.wim, ipxe.efi, etc.)"
fi
# Boot tools
if [ -d "$SCRIPT_DIR/boot-tools" ]; then
cp -r "$SCRIPT_DIR/boot-tools" "$CIDATA_DIR/boot-tools"
@@ -110,23 +118,20 @@ rm -rf "$CIDATA_DIR"
# --- Step 2: Create VM disk ---
echo ""
echo "[2/4] Creating VM disk (${VM_DISK_SIZE}GB)..."
if [ -f "$VM_DISK" ]; then
if virsh vol-info "$VM_NAME.qcow2" --pool default &>/dev/null; then
echo " Disk already exists. Destroy first with: $0 --destroy"
exit 1
fi
qemu-img create -f qcow2 "$VM_DISK" "${VM_DISK_SIZE}G"
virsh vol-create-as default "${VM_NAME}.qcow2" "${VM_DISK_SIZE}G" --format qcow2
# --- Step 3: Extract kernel/initrd from ISO ---
echo ""
echo "[3/4] Extracting kernel and initrd from ISO..."
ISO_MNT=$(mktemp -d)
mount -o loop,ro "$UBUNTU_ISO" "$ISO_MNT"
KERNEL="/tmp/${VM_NAME}-vmlinuz"
INITRD="/tmp/${VM_NAME}-initrd"
cp "$ISO_MNT/casper/vmlinuz" "$KERNEL"
cp "$ISO_MNT/casper/initrd" "$INITRD"
umount "$ISO_MNT"
rmdir "$ISO_MNT"
7z e -o/tmp -y "$UBUNTU_ISO" casper/vmlinuz casper/initrd 2>/dev/null
mv /tmp/vmlinuz "$KERNEL"
mv /tmp/initrd "$INITRD"
echo " Extracted vmlinuz and initrd from casper/"
# --- Step 4: Launch VM ---

View File

@@ -3,6 +3,7 @@
import logging
import os
import secrets
import shutil
import subprocess
import tempfile
@@ -11,12 +12,14 @@ from pathlib import Path
from flask import (
Flask,
abort,
flash,
jsonify,
redirect,
render_template,
request,
send_file,
session,
url_for,
)
from lxml import etree
@@ -71,6 +74,32 @@ FRIENDLY_NAMES = {
"ge-shopfloor-mce": "GE Legacy Shop Floor MCE",
}
# ---------------------------------------------------------------------------
# CSRF protection
# ---------------------------------------------------------------------------
def generate_csrf_token():
"""Return the CSRF token for the current session, creating one if needed."""
if "_csrf_token" not in session:
session["_csrf_token"] = secrets.token_hex(32)
return session["_csrf_token"]
@app.context_processor
def inject_csrf_token():
"""Make csrf_token() available in all templates."""
return {"csrf_token": generate_csrf_token}
@app.before_request
def validate_csrf():
"""Reject POST requests with a missing or invalid CSRF token."""
if request.method != "POST":
return
token = request.form.get("_csrf_token") or request.headers.get("X-CSRF-Token")
if not token or token != generate_csrf_token():
abort(403)
NS = "urn:schemas-microsoft-com:unattend"
WCM = "http://schemas.microsoft.com/WMIConfig/2002/State"
NSMAP = {None: NS, "wcm": WCM}

View File

@@ -243,9 +243,13 @@ document.addEventListener('DOMContentLoaded', function () {
saveRawBtn.disabled = true;
saveRawBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Saving...';
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ raw_xml: xmlContent })
})
.then(function (resp) { return resp.json(); })

View File

@@ -72,6 +72,7 @@
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Upload Backup</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
@@ -100,6 +101,7 @@
<div class="modal-dialog">
<div class="modal-content">
<form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}PXE Server Manager{% endblock %}</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">

View File

@@ -13,6 +13,7 @@
<div class="card-body">
{% if usb_mounts %}
<form method="POST" id="importForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="source" class="form-label fw-semibold">Source (USB Mount Point)</label>
<select class="form-select" name="source" id="source" required>

View File

@@ -79,6 +79,7 @@
<div class="modal-dialog">
<div class="modal-content">
<form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>

View File

@@ -52,6 +52,7 @@
</div>
<div class="card-body p-0">
<form action="{{ url_for('startnet_save') }}" method="post" id="startnetForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<textarea name="content" class="form-control cmd-editor" id="cmdEditor"
spellcheck="false">{{ content }}</textarea>
</form>

View File

@@ -70,6 +70,7 @@
</ul>
<form method="POST" id="unattendForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="tab-content">
<!-- ==================== FORM VIEW ==================== -->