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

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