Secure Boot iPXE chain is the default for every UEFI client

Live since 2026-10-14: dnsmasq hands every UEFI client ipxe-shim.efi
(iPXE's shim, dual-signed under Microsoft UEFI CA 2011 + 2023), which
loads ipxe.efi from the TFTP root, now the iPXE v2.0.0 snponly build
(firmware network and USB drivers; the full build kills USB keyboards).
The per-MAC test tags and folders are gone; the old Broadcom-signed
1.21.1 build stays as ipxe-bcm.efi, the rollback.

- playbook: pinned sha256 vars, the new dnsmasq default line, and a
  verified, non-optional task that installs ipxe-shim.efi, ipxe.efi
  (snponly), ipxe.pxe and ipxe-bcm.efi (was: copy ipxe.efi and the never
  served undionly.kpxe, errors ignored).
- scripts/fetch-ipxe.sh downloads the pinned tarball and shim into
  boot-files/ for the USB build; README/SETUP list the step.
- iPXE page: ipxe.efi is staged from snponly.efi, a build that drives USB
  itself is refused, and the snponly-shim/snponly root files are gone.

Tested: the playbook task against the real release files (installs the
live hashes; a tampered file stops it before anything is replaced),
ansible --syntax-check, staging, and the full-build refusal.
This commit is contained in:
cproudlock
2026-10-14 16:37:49 -04:00
parent a94c20331a
commit 045a1fb97f
6 changed files with 168 additions and 70 deletions

View File

@@ -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)

View File

@@ -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:**

View File

@@ -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: >

61
scripts/fetch-ipxe.sh Executable file
View File

@@ -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

View File

@@ -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/<aa-bb-cc-dd-ee-ff>/
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])

View File

@@ -164,14 +164,13 @@
<details>
<summary class="disc"><span class="chev" aria-hidden="true">&#9662;</span>What this page manages</summary>
<p class="sub" style="margin: 4px 0 0; max-width: 900px; line-height: 1.6">
The Secure Boot chain that dnsmasq hands out over TFTP: the Microsoft-signed shim (<span class="mono">ipxe-shim.efi</span>,
and the same file as <span class="mono">snponly-shim.efi</span>) starts <span class="mono">ipxe.efi</span>, which is signed by the
iPXE Secure Boot CA. <span class="mono">snponly.efi</span> 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 <span class="mono">sbverify</span> before it can be installed.
<span class="mono">ipxe-bcm.efi</span>, the per-client folders, <span class="mono">dnsmasq.conf</span> and the Blancco files are
never changed here.
The Secure Boot chain dnsmasq hands every UEFI client over TFTP: the Microsoft-signed shim (<span class="mono">ipxe-shim.efi</span>,
dual-signed under Microsoft UEFI CA 2011 and 2023) starts <span class="mono">ipxe.efi</span>, which is signed by the iPXE Secure
Boot CA. <span class="mono">ipxe.efi</span> 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 <span class="mono">sbverify</span> before it can be installed. <span class="mono">ipxe-bcm.efi</span>
(the old build, kept as the rollback), <span class="mono">dnsmasq.conf</span> and the Blancco files are never changed here.
</p>
</details>
@@ -446,23 +445,20 @@ sbverify --list ipxeboot/x86_64-sb/snponly.efi</pre>
is normal. Do not rely on <span class="mono">sbverify --cert</span>: it reports OK even for the wrong certificate.</li>
<li><strong>Back up the current files.</strong> A backup made in this folder also shows in the Backups list above, with Restore.
<pre class="cmd mono">B=/var/lib/pxe-webapp/updates/ipxe/backups/$(date +%Y%m%d-%H%M%S)
mkdir -p "$B" &amp;&amp; cp -p /srv/tftp/ipxe-shim.efi /srv/tftp/snponly-shim.efi /srv/tftp/ipxe.efi /srv/tftp/snponly.efi "$B"/</pre></li>
mkdir -p "$B" &amp;&amp; cp -p /srv/tftp/ipxe-shim.efi /srv/tftp/ipxe.efi "$B"/</pre></li>
<li><strong>Install both files, from the same release.</strong> The shim only starts an <span class="mono">ipxe.efi</span> signed under the
CA it carries, and it always loads <span class="mono">ipxe.efi</span> from the TFTP root, so the name must stay
<span class="mono">ipxe.efi</span>. Copy to a temporary name and rename, so a client that is booting never reads half a file:
<pre class="cmd mono">install -m 0644 ipxeboot/x86_64-sb/ipxe.efi /srv/tftp/.ipxe.efi.new &amp;&amp; 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 &amp;&amp; mv -f /srv/tftp/.snponly.efi.new /srv/tftp/snponly.efi
install -m 0644 ipxe-shimx64.efi /srv/tftp/.ipxe-shim.efi.new &amp;&amp; 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 &amp;&amp; mv -f /srv/tftp/.snponly-shim.efi.new /srv/tftp/snponly-shim.efi</pre>
<span class="sub">No dnsmasq restart is needed. Never replace <span class="mono">ipxe-bcm.efi</span>; it serves every other UEFI client.
A per-client folder keeps its own copies: update its <span class="mono">ipxe.efi</span> (the snponly build) and shim the same way.</span></li>
<li><strong>Who gets it.</strong> dnsmasq decides, in <span class="mono">/etc/dnsmasq.conf</span>: clients listed as
<span class="mono">dhcp-host=&lt;mac&gt;,set:ipxe2</span> get the shim through
<span class="mono">dhcp-boot=tag:ipxe2,tag:!ipxe,tag:!bios,ipxe-shim.efi</span>; every other UEFI client gets
<span class="mono">ipxe-bcm.efi</span> through the untagged <span class="mono">dhcp-boot=tag:!bios,...,ipxe-bcm.efi</span> line. Clients tagged
<span class="mono">ipxesnp</span> get <span class="mono">snponly-shim.efi</span>; since the shim asks for <span class="mono">ipxe.efi</span>
whatever it is called, each of them also needs a per-client folder whose <span class="mono">ipxe.efi</span> is the snponly build.
Who gets which file above shows the live lines. Test on one tagged machine before tagging more.</li>
<pre class="cmd mono">install -m 0644 ipxeboot/x86_64-sb/snponly.efi /srv/tftp/.ipxe.efi.new &amp;&amp; mv -f /srv/tftp/.ipxe.efi.new /srv/tftp/ipxe.efi
install -m 0644 ipxe-shimx64.efi /srv/tftp/.ipxe-shim.efi.new &amp;&amp; mv -f /srv/tftp/.ipxe-shim.efi.new /srv/tftp/ipxe-shim.efi</pre>
<span class="sub">Note the first line: the snponly build goes in as <span class="mono">ipxe.efi</span>. Never install the release's
full <span class="mono">x86_64-sb/ipxe.efi</span> (it kills USB keyboards), and never replace <span class="mono">ipxe-bcm.efi</span>
(the rollback). No dnsmasq restart is needed.</span></li>
<li><strong>Who gets it.</strong> dnsmasq decides, in <span class="mono">/etc/dnsmasq.conf</span>: every UEFI client gets the shim
through <span class="mono">dhcp-boot=tag:!bios,tag:!ipxe,ipxe-shim.efi</span>. To roll back, point that line at
<span class="mono">ipxe-bcm.efi</span> and restart dnsmasq; to keep one model on the old build, tag its MAC
(<span class="mono">dhcp-host=&lt;mac&gt;,set:oldipxe</span>) and add <span class="mono">dhcp-boot=tag:oldipxe,tag:!ipxe,tag:!bios,ipxe-bcm.efi</span>
above the default line. Who gets which file above shows the live lines.</li>
<li><strong>Roll back.</strong> Use Restore in the Backups list, or copy the backed-up files back the same way (temporary name, then
<span class="mono">mv -f</span>).</li>
<li><strong>Normal log noise.</strong> While booting, the shim asks TFTP for optional files such as