From 96249d0c06354744fde1e30a55df4df3fac5ac84 Mon Sep 17 00:00:00 2001 From: Pynezz Date: Mon, 2 Feb 2026 21:50:03 +0100 Subject: [PATCH] iPXE Server --- Containerfile | 246 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 48 ++++++++++ ipxe.caddyfile | 114 +++++++++++++++++++++++ ipxe.snippet | 51 ++++++++++ quadlet | 50 ++++++++++ setup.sh | 234 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 743 insertions(+) create mode 100644 Containerfile create mode 100644 README.md create mode 100644 ipxe.caddyfile create mode 100644 ipxe.snippet create mode 100644 quadlet create mode 100755 setup.sh diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..a311eca --- /dev/null +++ b/Containerfile @@ -0,0 +1,246 @@ +# ------------------------------------------------------------------ +# ██╗██████╗ ██╗ ██╗███████╗ Lightweight HTTP server +# ██║██╔══██╗╚██╗██╔╝██╔════╝ for network booting. +# ██║██████╔╝ ╚███╔╝ █████╗ +# ██║██╔═══╝ ██╔██╗ ██╔══╝ Serves boot menus, kernels, and images +# ██║██║ ██╔╝ ██╗███████╗ +# ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝ Version 1.0.0 +# ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +# ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +# ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +# ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +# ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +# ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +# Author: Kevin [+ Claude] +# Created: 2025-02-02 +# ------------------------------------------------------------------ + +FROM docker.io/library/alpine:3.21 AS base + +# Install runtime dependencies +RUN apk add --no-cache \ + nginx \ + curl \ + tzdata \ + tini \ + && rm -rf /var/cache/apk/* + +# Create ipxe user +RUN addgroup -g 1000 ipxe && \ + adduser -D -s /sbin/nologin -u 1000 -G ipxe ipxe + +# Create directory structure +RUN mkdir -p \ + /srv/boot \ + /srv/menus \ + /srv/images \ + /srv/scripts \ + /etc/ipxe \ + /var/log/nginx \ + /var/lib/nginx \ + /run/nginx && \ + chown -R ipxe:ipxe /srv /var/log/nginx /var/lib/nginx /run/nginx + +# nginx configuration for iPXE +RUN cat > /etc/nginx/nginx.conf << 'EOF' +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /run/nginx/nginx.pid; + +events { + worker_connections 256; + use epoll; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Custom MIME types for iPXE + types { + application/octet-stream ipxe efi; + text/plain ipxe menu; + } + + log_format ipxe '$remote_addr - [$time_local] "$request" $status $body_bytes_sent ' + '"$http_user_agent" rt=$request_time'; + + access_log /var/log/nginx/access.log ipxe; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + + # Disable buffering for faster boot file delivery + proxy_buffering off; + + server { + listen 8080; + server_name _; + + root /srv; + + # Boot files (kernels, initramfs, iPXE binaries) + location /boot/ { + alias /srv/boot/; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + } + + # iPXE menus and scripts + location /menus/ { + alias /srv/menus/; + autoindex on; + default_type text/plain; + } + + # ISO/squashfs images + location /images/ { + alias /srv/images/; + autoindex on; + autoindex_exact_size on; + + # Support range requests for large files + add_header Accept-Ranges bytes; + } + + # Dynamic menu endpoint (can be extended with scripts) + location /menu { + alias /srv/menus/boot.ipxe; + default_type text/plain; + } + + # Health check endpoint + location /health { + access_log off; + return 200 'OK\n'; + add_header Content-Type text/plain; + } + + # Status page + location /status { + stub_status on; + access_log off; + } + } +} +EOF + +# Health check script +RUN cat > /usr/local/bin/healthcheck.sh << 'EOF' +#!/bin/sh +set -e +curl -sf http://localhost:8080/health > /dev/null || exit 1 +echo "iPXE server healthy" +EOF +RUN chmod +x /usr/local/bin/healthcheck.sh + +# Entrypoint script +RUN cat > /usr/local/bin/entrypoint.sh << 'EOF' +#!/bin/sh +set -e + +echo "=== iPXE Boot Server ===" +echo "Boot files: /srv/boot" +echo "Menus: /srv/menus" +echo "Images: /srv/images" +echo "" + +# List available boot assets +echo "Available boot files:" +ls -la /srv/boot/ 2>/dev/null || echo " (none)" +echo "" +echo "Available menus:" +ls -la /srv/menus/ 2>/dev/null || echo " (none)" +echo "" + +# Validate nginx config +nginx -t + +# Start nginx in foreground +exec nginx -g 'daemon off;' +EOF +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Default boot menu template +RUN cat > /srv/menus/boot.ipxe << 'EOF' +#!ipxe +# iPXE Boot Menu - NUC Home Server +# Served via Caddy reverse proxy + +set menu-timeout 30000 +set menu-default local + +:start +menu iPXE Boot Menu - nuc.lan +item --gap -- -------- Boot Options -------- +item local Boot from local disk +item --gap -- -------- Network Boot -------- +item fedora-live Fedora 42 Live (Minimal) +item fedora-kiosk Fedora 42 Kiosk/PoS +item fedora-rescue Fedora 42 Rescue +item --gap -- -------- Utilities -------- +item memtest Memtest86+ +item shell iPXE Shell +item reboot Reboot +item exit Exit to BIOS +choose --timeout ${menu-timeout} --default ${menu-default} selected || goto cancel +goto ${selected} + +:cancel +echo Boot cancelled +goto start + +:local +sanboot --no-describe --drive 0x80 || goto start + +:fedora-live +echo Booting Fedora 42 Live (Minimal)... +set base-url http://ipxe.nuc.lan/images/fedora-42 +kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image rd.live.overlay.overlayfs=1 quiet +initrd ${base-url}/initrd.img +boot || goto start + +:fedora-kiosk +echo Booting Fedora 42 Kiosk... +set base-url http://ipxe.nuc.lan/images/fedora-42-kiosk +kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image quiet +initrd ${base-url}/initrd.img +boot || goto start + +:fedora-rescue +echo Booting Fedora 42 Rescue... +set base-url http://ipxe.nuc.lan/images/fedora-42 +kernel ${base-url}/vmlinuz initrd=initrd.img rescue quiet +initrd ${base-url}/initrd.img +boot || goto start + +:memtest +echo Loading Memtest86+... +kernel http://ipxe.nuc.lan/boot/memtest86+.bin +boot || goto start + +:shell +echo Dropping to iPXE shell... +shell + +:reboot +reboot + +:exit +exit +EOF + +EXPOSE 8080 + +USER ipxe +WORKDIR /srv + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["/usr/local/bin/entrypoint.sh"] + +LABEL maintainer="Kevin" \ + version="1.0.0" \ + description="iPXE boot server for network booting" diff --git a/README.md b/README.md new file mode 100644 index 0000000..bff10e5 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# IPXE Boot server + +Boot from ipxe.nuc.lan:8080 + +## Files + +| File | Purpose | +|-------------------------|-------------------------------------------------| +| `ipxe-server.container` | Quadlet unit joining `internal_caddy` network | +| `Containerfile` | Alpine + nginx serving boot assets on port 8080 | +| `snippets/ipxe` | Caddy snippet with reusable proxy directives | +| `caddy/ipxe.caddyfile` | Site block for `ipxe.nuc.lan` (HTTP + HTTPS) | +| `setup.sh` | Automated deployment script | + +**Integration with existing Caddyfile:** + +1. Copy `snippets/ipxe` to your snippets directory +2. Add to top of your Caddyfile: + + ``` + import snippets/ipxe + ``` + +3. Add the `ipxe.nuc.lan` block (or paste the content from `ipxe.caddyfile`) + + +**Or** since you have `*.nuc.lan` already, add this matcher to your wildcard block: + +```caddy +@ipxe host ipxe.nuc.lan +handle @ipxe { + reverse_proxy ipxe-server:8080 +} +``` + +**Quick deploy:** + +```bash +./setup.sh install # Creates dirs, builds image, installs Quadlet +./setup.sh start # Starts via systemd + +# Add boot files +cp vmlinuz initrd.img ~/ipxe/boot/ +cp squashfs.img ~/ipxe/images/fedora-42/ +``` + +**Note:** HTTP is intentionally kept open for `ipxe.nuc.lan:80` because most PXE ROMs chainload via HTTP before the full iPXE stack with HTTPS support is loaded. The local network restriction handles security. + diff --git a/ipxe.caddyfile b/ipxe.caddyfile new file mode 100644 index 0000000..ec7e6ff --- /dev/null +++ b/ipxe.caddyfile @@ -0,0 +1,114 @@ +# === iPXE BOOT SERVER === +# Add this to your main Caddyfile or include via import +# Requires: import snippets/ipxe at the top of Caddyfile + +# HTTPS endpoint for iPXE (local network) +ipxe.nuc.lan { + tls /etc/caddy/tls/wildcard.nuc.lan.crt /etc/caddy/tls/wildcard.nuc.lan.key { + protocols tls1.2 tls1.3 + } + + # Only allow local network access + @local_network { + remote_ip 192.168.1.0/24 10.89.0.0/24 10.10.5.0/24 + } + + # Boot menus - no caching + @menus { + path /menus/* /menu + } + handle @menus { + import ipxe-headers + import ipxe-upstream + } + + # Boot files (kernels, initramfs) - cache aggressively + @boot { + path /boot/* + } + handle @boot { + import ipxe-boot-headers + import ipxe-upstream + } + + # Images (ISO, squashfs) - moderate caching, range support + @images { + path /images/* + } + handle @images { + import ipxe-image-headers + import ipxe-upstream + } + + # Health/status endpoints + @health { + path /health /status + } + handle @health { + import ipxe-upstream + } + + # Default handler for local network + handle @local_network { + import ipxe-upstream + } + + # Block external access + handle { + respond "Access denied" 403 + } + + log { + output file /var/log/caddy/ipxe.log { + roll_size 10MB + roll_keep 3 + } + format json + } +} + +# HTTP endpoint for iPXE (required for legacy/chainloading) +# iPXE firmware often starts with HTTP before HTTPS +ipxe.nuc.lan:80 { + # Allow HTTP for initial iPXE chainload (most PXE ROMs don't support HTTPS) + @local_network { + remote_ip 192.168.1.0/24 10.89.0.0/24 10.10.5.0/24 + } + + # Menus via HTTP (for initial boot) + @menus { + path /menus/* /menu + } + handle @menus { + import ipxe-headers + import ipxe-upstream + } + + # Boot files via HTTP + @boot { + path /boot/* + } + handle @boot { + import ipxe-boot-headers + import ipxe-upstream + } + + # Images via HTTP (for clients that don't support HTTPS) + @images { + path /images/* + } + handle @images { + import ipxe-image-headers + import ipxe-upstream + } + + # Health endpoint + handle @local_network { + import ipxe-upstream + } + + handle { + respond "Access denied" 403 + } +} + diff --git a/ipxe.snippet b/ipxe.snippet new file mode 100644 index 0000000..be7c2f2 --- /dev/null +++ b/ipxe.snippet @@ -0,0 +1,51 @@ +# snippets/ipxe +# iPXE Boot Server reverse proxy configuration +# Import in Caddyfile with: import snippets/ipxe + +(ipxe-headers) { + header { + # Disable caching for menus (dynamic content) + Cache-Control "no-cache, no-store, must-revalidate" + Pragma "no-cache" + -Server + } +} + +(ipxe-boot-headers) { + header { + # Cache boot files aggressively (kernels, initramfs rarely change) + Cache-Control "public, max-age=86400" + -Server + } +} + +(ipxe-image-headers) { + header { + # Moderate caching for images + Cache-Control "public, max-age=3600" + # Support range requests for large files + Accept-Ranges "bytes" + -Server + } +} + +(ipxe-upstream) { + # Reverse proxy to iPXE container + reverse_proxy ipxe-server:8080 { + # Timeouts for large file transfers + transport http { + response_header_timeout 30s + read_timeout 300s + write_timeout 300s + } + + # Health check + health_uri /health + health_interval 30s + health_timeout 5s + + # Fail fast if unhealthy + fail_duration 30s + } +} + diff --git a/quadlet b/quadlet new file mode 100644 index 0000000..32e7e74 --- /dev/null +++ b/quadlet @@ -0,0 +1,50 @@ +# ipxe-server.container +# Quadlet unit for iPXE boot server +# Place in: ~/.config/containers/systemd/ (rootless) or /etc/containers/systemd/ (root) + +[Unit] +Description=iPXE Boot Server +After=network-online.target +Wants=network-online.target +Requires=caddy.service +After=caddy.service + +[Container] +Image=localhost/ipxe-server:latest +ContainerName=ipxe-server + +# Join caddy network for reverse proxy +Network=internal_caddy + +# Mount points for boot assets and configuration +Volume=%h/ipxe/boot:/srv/boot:ro,Z +Volume=%h/ipxe/menus:/srv/menus:ro,Z +Volume=%h/ipxe/config:/etc/ipxe:ro,Z + +# Optional: mount ISO storage for serving live images +Volume=%h/ipxe/images:/srv/images:ro,Z + +# Environment +Environment=IPXE_LOG_LEVEL=info +Environment=TZ=Europe/Oslo + +# Resource limits +PodmanArgs=--memory=256m --cpus=0.5 + +# Labels for Caddy/Traefik discovery (optional) +Label=ipxe.server=true + +# Health check +HealthCmd=/usr/local/bin/healthcheck.sh +HealthInterval=30s +HealthTimeout=5s +HealthRetries=3 + +[Service] +Restart=on-failure +RestartSec=10 +TimeoutStartSec=90 + +[Install] +WantedBy=default.target + diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..0d4f419 --- /dev/null +++ b/setup.sh @@ -0,0 +1,234 @@ +#!/bin/bash +# Setup script for iPXE boot server +# Creates directory structure and deploys Quadlet + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' + +log_info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; } +log_ok() { printf "${GREEN}[OK]${NC} %s\n" "$1"; } +log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1"; } +log_err() { printf "${RED}[ERROR]${NC} %s\n" "$1"; } + +# Configuration +IPXE_BASE="${IPXE_BASE:-$HOME/ipxe}" +QUADLET_DIR="${QUADLET_DIR:-$HOME/.config/containers/systemd}" +CADDY_SNIPPETS="${CADDY_SNIPPETS:-$HOME/Caddy/snippets}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + cat << EOF +iPXE Boot Server Setup + +Usage: $0 [command] + +Commands: + install Install Quadlet and create directories + build Build the container image + start Start the iPXE server + stop Stop the iPXE server + status Show server status + logs Show server logs + uninstall Remove Quadlet unit + +Options: + --rootless Install for rootless Podman (default) + --root Install system-wide (requires sudo) +EOF +} + +create_directories() { + log_info "Creating directory structure..." + + mkdir -p "$IPXE_BASE"/{boot,menus,images,config} + mkdir -p "$QUADLET_DIR" + mkdir -p "$CADDY_SNIPPETS" + + log_ok "Directories created at $IPXE_BASE" +} + +install_quadlet() { + log_info "Installing Quadlet unit..." + + # Copy Quadlet file + cp "$SCRIPT_DIR/ipxe-server.container" "$QUADLET_DIR/" + + # Update paths in Quadlet to use actual home directory + sed -i "s|%h/ipxe|$IPXE_BASE|g" "$QUADLET_DIR/ipxe-server.container" + + log_ok "Quadlet installed to $QUADLET_DIR/ipxe-server.container" +} + +install_caddy_snippet() { + log_info "Installing Caddy snippet..." + + cp "$SCRIPT_DIR/snippets/ipxe" "$CADDY_SNIPPETS/" + + log_ok "Caddy snippet installed to $CADDY_SNIPPETS/ipxe" + log_warn "Don't forget to:" + log_warn " 1. Add 'import snippets/ipxe' to your Caddyfile" + log_warn " 2. Add the ipxe.nuc.lan site block from caddy/ipxe.caddyfile" + log_warn " 3. Reload Caddy: podman exec caddy caddy reload --config /etc/caddy/Caddyfile" +} + +copy_default_menu() { + log_info "Copying default boot menu..." + + if [[ ! -f "$IPXE_BASE/menus/boot.ipxe" ]]; then + # Extract default menu from Containerfile or use embedded one + cat > "$IPXE_BASE/menus/boot.ipxe" << 'EOF' +#!ipxe +# iPXE Boot Menu - NUC Home Server + +set menu-timeout 30000 +set menu-default local + +:start +menu iPXE Boot Menu - nuc.lan +item --gap -- -------- Boot Options -------- +item local Boot from local disk +item --gap -- -------- Network Boot -------- +item fedora-live Fedora 42 Live (Minimal) +item fedora-kiosk Fedora 42 Kiosk/PoS +item --gap -- -------- Utilities -------- +item shell iPXE Shell +item reboot Reboot +choose --timeout ${menu-timeout} --default ${menu-default} selected || goto cancel +goto ${selected} + +:cancel +echo Boot cancelled +goto start + +:local +sanboot --no-describe --drive 0x80 || goto start + +:fedora-live +echo Booting Fedora 42 Live... +set base-url http://ipxe.nuc.lan/images/fedora-42 +kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image quiet +initrd ${base-url}/initrd.img +boot || goto start + +:fedora-kiosk +echo Booting Fedora 42 Kiosk... +set base-url http://ipxe.nuc.lan/images/fedora-42-kiosk +kernel ${base-url}/vmlinuz initrd=initrd.img root=live:${base-url}/squashfs.img rd.live.image quiet +initrd ${base-url}/initrd.img +boot || goto start + +:shell +shell + +:reboot +reboot +EOF + log_ok "Default boot menu created" + else + log_warn "Boot menu already exists, skipping" + fi +} + +build_image() { + log_info "Building iPXE server container image..." + + podman build -t localhost/ipxe-server:latest -f "$SCRIPT_DIR/Containerfile" "$SCRIPT_DIR" + + log_ok "Image built: localhost/ipxe-server:latest" +} + +reload_systemd() { + log_info "Reloading systemd user units..." + systemctl --user daemon-reload + log_ok "Systemd reloaded" +} + +start_server() { + log_info "Starting iPXE server..." + systemctl --user start ipxe-server + log_ok "iPXE server started" +} + +stop_server() { + log_info "Stopping iPXE server..." + systemctl --user stop ipxe-server + log_ok "iPXE server stopped" +} + +show_status() { + systemctl --user status ipxe-server +} + +show_logs() { + journalctl --user -u ipxe-server -f +} + +uninstall() { + log_info "Uninstalling iPXE server..." + + systemctl --user stop ipxe-server 2>/dev/null || true + rm -f "$QUADLET_DIR/ipxe-server.container" + systemctl --user daemon-reload + + log_ok "Quadlet removed" + log_warn "Data in $IPXE_BASE preserved. Remove manually if needed." +} + +do_install() { + create_directories + install_quadlet + install_caddy_snippet + copy_default_menu + build_image + reload_systemd + + echo "" + log_ok "Installation complete!" + echo "" + echo "Next steps:" + echo " 1. Add boot files to: $IPXE_BASE/boot/" + echo " 2. Add images to: $IPXE_BASE/images/" + echo " 3. Edit boot menu: $IPXE_BASE/menus/boot.ipxe" + echo " 4. Update Caddy config (see caddy/ipxe.caddyfile)" + echo " 5. Start server: $0 start" + echo "" +} + +# Main +case "${1:-}" in + install) + do_install + ;; + build) + build_image + ;; + start) + start_server + ;; + stop) + stop_server + ;; + status) + show_status + ;; + logs) + show_logs + ;; + uninstall) + uninstall + ;; + -h|--help|help|"") + usage + ;; + *) + log_err "Unknown command: $1" + usage + exit 1 + ;; +esac