diff --git a/README.md b/README.md index 6c66479..9dac23b 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,14 @@ Client PXE boot (UEFI Secure Boot) ```bash ./scripts/download-packages.sh +./scripts/fetch-ipxe.sh ``` +`fetch-ipxe.sh` downloads the Secure Boot iPXE chain into `boot-files/`, pinned by sha256: the iPXE release +tarball (its snponly build becomes `ipxe.efi`) and iPXE's dual-signed shim (Microsoft UEFI CA 2011 + 2023), +which every UEFI client boots as `ipxe-shim.efi`. Keep the old Broadcom-signed `boot-files/ipxe.efi` too: the +playbook installs it as `ipxe-bcm.efi`, the rollback boot file. + Downloads all .deb packages and Python wheels for offline installation (~140 MB of debs, ~20 MB of wheels). ### Step 2: Prepare Boot Tools (optional) diff --git a/SETUP.md b/SETUP.md index 16f5d72..e88289a 100644 --- a/SETUP.md +++ b/SETUP.md @@ -47,8 +47,14 @@ Client PXE boot ```bash ./scripts/download-packages.sh +./scripts/fetch-ipxe.sh ``` +`fetch-ipxe.sh` downloads the Secure Boot iPXE chain into `boot-files/`, pinned by sha256: the iPXE release +tarball (its snponly build becomes `ipxe.efi`) and iPXE's dual-signed shim (Microsoft UEFI CA 2011 + 2023), +which every UEFI client boots as `ipxe-shim.efi`. Keep the old Broadcom-signed `boot-files/ipxe.efi` too: the +playbook installs it as `ipxe-bcm.efi`, the rollback boot file. + Downloads all .deb packages (ansible, dnsmasq, apache2, samba, wimtools, etc.) into `offline-packages/` and Python wheels (flask, lxml) into `pip-wheels/`. Approximately 252 packages (~140 MB) + 8 Python wheels. **Packages included:** diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml index aff6ae8..e5d24af 100644 --- a/playbook/pxe_server_setup.yml +++ b/playbook/pxe_server_setup.yml @@ -38,6 +38,17 @@ samba_share: "/srv/samba/winpeapps" usb_mount: "/mnt/usb/playbook" # playbook location on USB usb_root: "/mnt/usb" # CIDATA partition root + # Secure Boot iPXE chain, fetched into boot-files/ by scripts/fetch-ipxe.sh + # (keep these pins in step with that script). Every UEFI client gets the + # dual-signed shim (Microsoft UEFI CA 2011 + 2023), which loads ipxe.efi + # from the TFTP root: the release's SNPONLY build (firmware network and + # USB drivers; the full ipxe.efi drives USB itself and kills USB + # keyboards at the menu). The old Broadcom-signed 1.21.1 build stays on + # the box as ipxe-bcm.efi, the rollback. + ipxe_tarball_sha256: "01a526d4cc791fc30362259c609d6c506cc64a7bdff51b9a5eb788354e17eee1" # ipxe v2.0.0 ipxeboot.tar.gz + ipxe_shim_sha256: "5eecca2780bd49c900565e124516a1bd666ec5e012825f34991b6ba1ef2fa6cf" # ipxe/shim ipxe-16.1 ipxe-shimx64.efi + ipxe_snponly_sha256: "b1e67c3e4a1e8708ddfd0079ad4505e3a02245acb55ee9a95437ab3c507be82a" # x86_64-sb/snponly.efi + ipxe_bios_sha256: "0fb21c695c82fd70d4f89732ddcd285063776e2bbd71de9a2c39e9cfe4b180b8" # x86_64/ipxe.pxe # Zscaler Root CA: GE's Zscaler re-signs GitHub TLS, so the iPXE update # check fails without it. Repo copy first, else the copy on the image share. zscaler_ca_src: "{{ playbook_dir }}/certs/zscaler-root-ca.crt" @@ -176,17 +187,18 @@ tftp-root={{ tftp_dir }} # Arch-aware NBP for Blancco/WinPE PXE clients: # - legacy BIOS (vendorclass PXEClient:Arch:00000) -> ipxe.pxe, a - # BIOS full-feature iPXE build (boot.ipxe.org) with an HTTP/TCP - # stack. The old undionly.kpxe client-arch=0 scheme was stale - - # undionly.kpxe was never staged in tftp-root so it broke BIOS - # boot; this vendorclass+ipxe.pxe scheme is what runs on the box. + # BIOS full-feature iPXE build with an HTTP/TCP stack. # - iPXE-running clients (userclass "iPXE") -> chain to the HTTP menu - # - everything else (UEFI x86_64) -> ipxe.efi + # - every UEFI client -> ipxe-shim.efi, iPXE's Microsoft-signed shim + # (dual-signed, UEFI CA 2011 + 2023). Over TFTP it always loads + # ipxe.efi from the TFTP root, which is the iPXE snponly build + # (see the ipxe_* vars). Rollback: point the last line back at + # ipxe-bcm.efi (the old Broadcom-signed 1.21.1 build). dhcp-vendorclass=set:bios,PXEClient:Arch:00000 dhcp-userclass=set:ipxe,iPXE dhcp-boot=tag:bios,tag:!ipxe,ipxe.pxe dhcp-boot=tag:ipxe,http://172.16.9.1:4433/Altiris/iPXE/GetPxeScript.aspx - dhcp-boot=tag:!bios,ipxe.efi + dhcp-boot=tag:!bios,tag:!ipxe,ipxe-shim.efi log-dhcp # Per-lease state cleanup: flush conntrack + port-445 sockets for # the client IP on add/del. Prevents "System error 53" when a PXE @@ -808,15 +820,36 @@ executable: /bin/bash ignore_errors: yes - - name: "Copy iPXE binaries from USB (skipped if not present)" - copy: - src: "{{ usb_root }}/{{ item }}" - dest: "{{ tftp_dir }}/{{ item }}" - mode: '0755' - loop: - - ipxe.efi - - undionly.kpxe - ignore_errors: yes + - name: "Install the Secure Boot iPXE chain into the TFTP root (pinned, verified)" + # Not ignore_errors: without these files no client can boot. Files come + # from boot-files/ via scripts/fetch-ipxe.sh; each is checked against + # its pin before anything is replaced, and each is put in place by + # rename so a client that is booting never reads half a file. + shell: | + set -euo pipefail + U="{{ usb_root }}"; T="{{ tftp_dir }}" + check() { echo "$2 $1" | sha256sum -c --status || { echo "sha256 mismatch or missing: $1" >&2; exit 1; }; } + check "$U/ipxeboot.tar.gz" "{{ ipxe_tarball_sha256 }}" + check "$U/ipxe-shimx64.efi" "{{ ipxe_shim_sha256 }}" + W=$(mktemp -d); trap 'rm -rf "$W"' EXIT + tar -xzf "$U/ipxeboot.tar.gz" -C "$W" ipxeboot/x86_64-sb/snponly.efi ipxeboot/x86_64/ipxe.pxe + check "$W/ipxeboot/x86_64-sb/snponly.efi" "{{ ipxe_snponly_sha256 }}" + check "$W/ipxeboot/x86_64/ipxe.pxe" "{{ ipxe_bios_sha256 }}" + put() { install -m 0644 -o root -g root "$1" "$T/.$2.new" && mv -f "$T/.$2.new" "$T/$2"; } + put "$U/ipxe-shimx64.efi" ipxe-shim.efi + put "$W/ipxeboot/x86_64-sb/snponly.efi" ipxe.efi + put "$W/ipxeboot/x86_64/ipxe.pxe" ipxe.pxe + # Rollback boot file: the old Broadcom-signed build (boot-files/ipxe.efi). + if [ -f "$U/ipxe.efi" ]; then put "$U/ipxe.efi" ipxe-bcm.efi; fi + echo "installed ipxe-shim.efi, ipxe.efi (snponly), ipxe.pxe$( [ -f "$U/ipxe.efi" ] && echo ', ipxe-bcm.efi')" + args: + executable: /bin/bash + register: ipxe_chain + changed_when: true + + - name: "Show the installed iPXE chain" + debug: + msg: "{{ ipxe_chain.stdout }}" - name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)" shell: > diff --git a/scripts/fetch-ipxe.sh b/scripts/fetch-ipxe.sh new file mode 100755 index 0000000..a7e7420 --- /dev/null +++ b/scripts/fetch-ipxe.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# fetch-ipxe.sh - Download the pinned Secure Boot iPXE chain into boot-files/ +# for the USB build (build-usb.sh copies boot-files/* to the USB root, and +# playbook/pxe_server_setup.yml installs them into the TFTP root). +# +# ipxeboot.tar.gz iPXE release (github.com/ipxe/ipxe). The playbook takes +# x86_64-sb/snponly.efi (installed as ipxe.efi: firmware +# network + USB drivers, so USB keyboards work at the menu) +# and x86_64/ipxe.pxe (legacy BIOS). +# ipxe-shimx64.efi iPXE's Microsoft-signed shim (github.com/ipxe/shim), +# dual-signed under Microsoft UEFI CA 2011 and 2023. +# Installed as ipxe-shim.efi, the boot file every UEFI +# client gets. Not the tarball's shimx64.efi, which is +# signed under the 2011 CA only. +# +# Every file is checked against the sha256 pinned below; a mismatch deletes +# it and fails. To move to a new release, change the pins here AND the +# matching ipxe_* vars in playbook/pxe_server_setup.yml. +# +# Usage: ./scripts/fetch-ipxe.sh [boot-files dir] + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)" +OUT_DIR="${1:-$REPO_ROOT/boot-files}" + +IPXE_TAG="v2.0.0" +IPXE_TARBALL_SHA256="01a526d4cc791fc30362259c609d6c506cc64a7bdff51b9a5eb788354e17eee1" +SHIM_TAG="ipxe-16.1" +SHIM_SHA256="5eecca2780bd49c900565e124516a1bd666ec5e012825f34991b6ba1ef2fa6cf" + +fetch() { # url dest sha256 + local url="$1" dest="$2" sha="$3" + if [ -f "$dest" ] && echo "$sha $dest" | sha256sum -c --status; then + echo " ok (cached) $(basename "$dest")" + return + fi + rm -f "$dest" + # GitHub goes through Zscaler on GE networks: a TLS error here means the + # Zscaler root CA is missing from the system trust store. + curl -fsSL --retry 3 -o "$dest" "$url" + if ! echo "$sha $dest" | sha256sum -c --status; then + echo "ERROR: sha256 mismatch for $(basename "$dest"); removed it" >&2 + rm -f "$dest" + exit 1 + fi + echo " ok $(basename "$dest")" +} + +mkdir -p "$OUT_DIR" +echo "Fetching the Secure Boot iPXE chain into $OUT_DIR" +fetch "https://github.com/ipxe/ipxe/releases/download/$IPXE_TAG/ipxeboot.tar.gz" \ + "$OUT_DIR/ipxeboot.tar.gz" "$IPXE_TARBALL_SHA256" +fetch "https://github.com/ipxe/shim/releases/download/$SHIM_TAG/ipxe-shimx64.efi" \ + "$OUT_DIR/ipxe-shimx64.efi" "$SHIM_SHA256" +if [ ! -f "$OUT_DIR/ipxe.efi" ]; then + echo "NOTE: $OUT_DIR/ipxe.efi (the old Broadcom-signed 1.21.1 build) is missing." + echo " The playbook installs it as ipxe-bcm.efi, the rollback boot file;" + echo " copy it from an existing PXE server's /srv/tftp/ipxe-bcm.efi." +fi diff --git a/webapp/services/ipxe_update.py b/webapp/services/ipxe_update.py index 19ff459..3bbd6f0 100644 --- a/webapp/services/ipxe_update.py +++ b/webapp/services/ipxe_update.py @@ -1,21 +1,19 @@ """Keep the Secure Boot iPXE chain on the TFTP root current. -What dnsmasq serves (see /etc/dnsmasq.conf): - * ipxe-shim.efi - Microsoft-signed shim, for the MACs tagged ipxe2. Over - TFTP it always loads ipxe.efi from the TFTP root (it - does not derive the name from its own, whatever the - upstream docs say); - * snponly-shim.efi - the same shim under the name dnsmasq hands the - ipxesnp-tagged MACs; - * ipxe.efi - iPXE signed by the iPXE Secure Boot CA, loaded by the - shim. The full build drives USB itself, which kills a - USB keyboard at the iPXE menu; - * snponly.efi - the same release built to use the firmware's network - and USB drivers (keyboard keeps working). Served to a - client as ipxe.efi from its per-MAC folder; - * ipxe-bcm.efi - the old 2021 Broadcom-signed build, everyone else. - Never touched here; - * ipxe.pxe - legacy BIOS iPXE (unsigned; BIOS has no Secure Boot). +What dnsmasq serves (see /etc/dnsmasq.conf), since 2026-10-14: + * ipxe-shim.efi - iPXE's Microsoft-signed shim, every UEFI client. Over + TFTP it always loads ipxe.efi from the TFTP root (it does + not derive the name from its own, whatever the upstream + docs say); + * ipxe.efi - iPXE signed by the iPXE Secure Boot CA, loaded by the + shim. It is the release's SNPONLY build (x86_64-sb/ + snponly.efi): firmware network and USB drivers. The + release's full ipxe.efi drives USB itself, which kills a + USB keyboard at the iPXE menu, so it is never installed; + * ipxe-bcm.efi - the old 2021 Broadcom-signed 1.21.1 build, kept as the + rollback (point dnsmasq's UEFI line back at it). Never + touched here; + * ipxe.pxe - legacy BIOS iPXE (unsigned; BIOS has no Secure Boot). Per-MAC folders: with tftp-unique-root=mac, /srv/tftp// wins over the root for that client, with NO fallback for a file missing @@ -71,14 +69,13 @@ JOB_UNIT = "pxe-webapp-ipxe-stage" # Secure Boot signature (and from whom). MANAGED = { "ipxe-shim.efi": ("ipxeboot/x86_64-sb/shimx64.efi", "microsoft"), - "snponly-shim.efi": ("ipxeboot/x86_64-sb/shimx64.efi", "microsoft"), - "ipxe.efi": ("ipxeboot/x86_64-sb/ipxe.efi", "ipxe"), - "snponly.efi": ("ipxeboot/x86_64-sb/snponly.efi", "ipxe"), + # The snponly build, installed under the name the shim loads. + "ipxe.efi": ("ipxeboot/x86_64-sb/snponly.efi", "ipxe"), "ipxe.pxe": ("ipxeboot/x86_64/ipxe.pxe", None), } -SHIMS = ("ipxe-shim.efi", "snponly-shim.efi") +SHIMS = ("ipxe-shim.efi",) # Installed by default; ipxe.pxe (legacy BIOS) only when asked. -DEFAULT_INSTALL = ("ipxe-shim.efi", "snponly-shim.efi", "ipxe.efi", "snponly.efi") +DEFAULT_INSTALL = ("ipxe-shim.efi", "ipxe.efi") # Shown on the page for reference, never written. READ_ONLY = ("ipxe-bcm.efi",) @@ -266,8 +263,7 @@ def installed(): rows.append({"name": name, "managed": name in MANAGED, "info": info}) # The shim only starts an iPXE signed under the CA it embeds. by = {r["name"]: r["info"] for r in rows} - _check_chain(by.get("ipxe-shim.efi") or by.get("snponly-shim.efi"), - [by.get("ipxe.efi"), by.get("snponly.efi")]) + _check_chain(by.get("ipxe-shim.efi"), [by.get("ipxe.efi")]) return rows @@ -530,11 +526,13 @@ def stage(info=None): _use_dual_signed_shim(folder, files) # The shim must trust the CA this release's iPXE builds are signed by. shim = files.get("ipxe-shim.efi") or {} - for name in ("ipxe.efi", "snponly.efi"): - ipxe = files.get(name) or {} - if shim.get("ok") and ipxe.get("ok") and shim.get("trusts"): - if shim["trusts"] not in {i for _, i in ipxe["signers"]}: - ipxe.update(ok=False, error=f"not signed under {shim['trusts']}, which this shim trusts") + ipxe = files.get("ipxe.efi") or {} + if shim.get("ok") and ipxe.get("ok") and shim.get("trusts"): + if shim["trusts"] not in {i for _, i in ipxe["signers"]}: + ipxe.update(ok=False, error=f"not signed under {shim['trusts']}, which this shim trusts") + if ipxe.get("ok") and ipxe.get("drivers") == "native": + # The full build would kill USB keyboards on every client. + ipxe.update(ok=False, error="this build drives USB itself (not snponly); refusing it as ipxe.efi") record = {"tag": info["tag"], "published": info.get("published"), "folder": folder, "tarball_sha256": info["sha256"], "files": files, "staged": uc.now()} uc.write_json(os.path.join(folder, "staged.json"), record) @@ -775,12 +773,10 @@ def _install(tag, rec, names): if not any(n in names for n in SHIMS): # iPXE alone must still be one the installed shim will start. trusts = vendor_cert_cn(os.path.join(config.TFTP_ROOT, "ipxe-shim.efi")) - for n in ("ipxe.efi", "snponly.efi"): - if n not in names: - continue - issuers = {i for _, i in rec["files"][n].get("signers", [])} + if "ipxe.efi" in names: + issuers = {i for _, i in rec["files"]["ipxe.efi"].get("signers", [])} if trusts and trusts not in issuers: - raise ValueError(f"the installed shim trusts {trusts}; this {n} is not signed " + raise ValueError(f"the installed shim trusts {trusts}; this ipxe.efi is not signed " "under it, install the shim from the same release too") backup = _backup(names, f"before installing {tag}") _place_all([(os.path.join(rec["folder"], n), n, rec["files"][n]["sha256"]) for n in names]) diff --git a/webapp/templates/updates_ipxe.html b/webapp/templates/updates_ipxe.html index b887e8b..0273b9b 100644 --- a/webapp/templates/updates_ipxe.html +++ b/webapp/templates/updates_ipxe.html @@ -164,14 +164,13 @@
What this page manages

- The Secure Boot chain that dnsmasq hands out over TFTP: the Microsoft-signed shim (ipxe-shim.efi, - and the same file as snponly-shim.efi) starts ipxe.efi, which is signed by the - iPXE Secure Boot CA. snponly.efi is the same release built on the firmware's drivers (USB keyboards - keep working). Updates come from the upstream iPXE release tarball on GitHub, plus the dual-signed shim (Microsoft UEFI CA - 2011 and 2023) from the separate ipxe/shim release; each download's sha256 is checked against GitHub's published digest and - each file's signer chain is read with sbverify before it can be installed. - ipxe-bcm.efi, the per-client folders, dnsmasq.conf and the Blancco files are - never changed here. + The Secure Boot chain dnsmasq hands every UEFI client over TFTP: the Microsoft-signed shim (ipxe-shim.efi, + dual-signed under Microsoft UEFI CA 2011 and 2023) starts ipxe.efi, which is signed by the iPXE Secure + Boot CA. ipxe.efi is the release's snponly build (firmware network and USB drivers, so USB keyboards keep + working); the release's full build is refused. Updates come from the upstream iPXE release tarball on GitHub, plus the dual-signed + shim from the separate ipxe/shim release; each download's sha256 is checked against GitHub's published digest and each file's + signer chain is read with sbverify before it can be installed. ipxe-bcm.efi + (the old build, kept as the rollback), dnsmasq.conf and the Blancco files are never changed here.

@@ -446,23 +445,20 @@ sbverify --list ipxeboot/x86_64-sb/snponly.efi is normal. Do not rely on sbverify --cert: it reports OK even for the wrong certificate.
  • Back up the current files. A backup made in this folder also shows in the Backups list above, with Restore.
    B=/var/lib/pxe-webapp/updates/ipxe/backups/$(date +%Y%m%d-%H%M%S)
    -mkdir -p "$B" && cp -p /srv/tftp/ipxe-shim.efi /srv/tftp/snponly-shim.efi /srv/tftp/ipxe.efi /srv/tftp/snponly.efi "$B"/
  • +mkdir -p "$B" && cp -p /srv/tftp/ipxe-shim.efi /srv/tftp/ipxe.efi "$B"/
  • Install both files, from the same release. The shim only starts an ipxe.efi signed under the CA it carries, and it always loads ipxe.efi from the TFTP root, so the name must stay ipxe.efi. Copy to a temporary name and rename, so a client that is booting never reads half a file: -
    install -m 0644 ipxeboot/x86_64-sb/ipxe.efi     /srv/tftp/.ipxe.efi.new         && mv -f /srv/tftp/.ipxe.efi.new         /srv/tftp/ipxe.efi
    -install -m 0644 ipxeboot/x86_64-sb/snponly.efi  /srv/tftp/.snponly.efi.new      && mv -f /srv/tftp/.snponly.efi.new      /srv/tftp/snponly.efi
    -install -m 0644 ipxe-shimx64.efi                /srv/tftp/.ipxe-shim.efi.new    && mv -f /srv/tftp/.ipxe-shim.efi.new    /srv/tftp/ipxe-shim.efi
    -install -m 0644 ipxe-shimx64.efi                /srv/tftp/.snponly-shim.efi.new && mv -f /srv/tftp/.snponly-shim.efi.new /srv/tftp/snponly-shim.efi
    - No dnsmasq restart is needed. Never replace ipxe-bcm.efi; it serves every other UEFI client. - A per-client folder keeps its own copies: update its ipxe.efi (the snponly build) and shim the same way.
  • -
  • Who gets it. dnsmasq decides, in /etc/dnsmasq.conf: clients listed as - dhcp-host=<mac>,set:ipxe2 get the shim through - dhcp-boot=tag:ipxe2,tag:!ipxe,tag:!bios,ipxe-shim.efi; every other UEFI client gets - ipxe-bcm.efi through the untagged dhcp-boot=tag:!bios,...,ipxe-bcm.efi line. Clients tagged - ipxesnp get snponly-shim.efi; since the shim asks for ipxe.efi - whatever it is called, each of them also needs a per-client folder whose ipxe.efi is the snponly build. - Who gets which file above shows the live lines. Test on one tagged machine before tagging more.
  • +
    install -m 0644 ipxeboot/x86_64-sb/snponly.efi /srv/tftp/.ipxe.efi.new      && mv -f /srv/tftp/.ipxe.efi.new      /srv/tftp/ipxe.efi
    +install -m 0644 ipxe-shimx64.efi               /srv/tftp/.ipxe-shim.efi.new && mv -f /srv/tftp/.ipxe-shim.efi.new /srv/tftp/ipxe-shim.efi
    + Note the first line: the snponly build goes in as ipxe.efi. Never install the release's + full x86_64-sb/ipxe.efi (it kills USB keyboards), and never replace ipxe-bcm.efi + (the rollback). No dnsmasq restart is needed. +
  • Who gets it. dnsmasq decides, in /etc/dnsmasq.conf: every UEFI client gets the shim + through dhcp-boot=tag:!bios,tag:!ipxe,ipxe-shim.efi. To roll back, point that line at + ipxe-bcm.efi and restart dnsmasq; to keep one model on the old build, tag its MAC + (dhcp-host=<mac>,set:oldipxe) and add dhcp-boot=tag:oldipxe,tag:!ipxe,tag:!bios,ipxe-bcm.efi + above the default line. Who gets which file above shows the live lines.
  • Roll back. Use Restore in the Backups list, or copy the backed-up files back the same way (temporary name, then mv -f).
  • Normal log noise. While booting, the shim asks TFTP for optional files such as