Webapp: iPXE page stages the dual-signed shim, knows snponly and per-MAC folders

Staging fetches ipxe-shimx64.efi from the ipxe/shim release (sha256 from
GitHub), and uses it instead of the tarball's 2011-only shim when it is
signed under the Microsoft UEFI CA 2023 and trusts the same iPXE CA;
otherwise the tarball shim stays, with the reason shown. snponly.efi and
snponly-shim.efi are managed alongside ipxe.efi and ipxe-shim.efi, each
iPXE build says whether it drives USB itself (kills USB keyboards), and
tftp-unique-root per-MAC folders are listed read-only.
This commit is contained in:
cproudlock
2026-10-14 15:37:14 -04:00
parent 4db9dc093f
commit 81c2d85301
3 changed files with 218 additions and 52 deletions

View File

@@ -1,13 +1,25 @@
"""Keep the Secure Boot iPXE chain on the TFTP root current.
What dnsmasq serves (see /etc/dnsmasq.conf):
* ipxe-shim.efi - Microsoft-signed shim (upstream x86_64-sb/shimx64.efi),
for the MACs tagged ipxe2; it always loads ipxe.efi from
the TFTP ROOT, whatever folder it came from;
* ipxe.efi - iPXE signed by the iPXE Secure Boot CA, loaded by the shim;
* 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).
* 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).
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
from it. They are shown here read-only and never written.
Upstream publishes the signed set as one tarball on every release:
https://github.com/ipxe/ipxe/releases/latest/download/ipxeboot.tar.gz, with
@@ -18,10 +30,13 @@ The chain check is by certificate name (sbverify --list); the cryptographic
trust comes from the GitHub digest. Do not swap in `sbverify --cert`: it
prints "Signature verification OK" even for a missing or unrelated cert.
Why it matters: the shim and the Broadcom build are signed by Microsoft's
UEFI CA 2011, whose signing period ended in 2026. Machines that only trust
the 2023 CA, or a revocation (DBX/SBAT) update, will refuse them; the fix is
a newer upstream release, installed here.
Why it matters: the Broadcom build, and the shim in the iPXE tarball, are
signed only by Microsoft's UEFI CA 2011, whose signing period ended in 2026.
Machines that only trust the 2023 CA, or a revocation (DBX/SBAT) update,
will refuse them. iPXE publishes a shim dual-signed with the 2011 and 2023
CAs as a separate release (github.com/ipxe/shim); staging fetches it, checks
its digest and signatures, and uses it instead of the tarball's shim when it
trusts the same iPXE CA.
Staging runs as a background job (python3 -m services.ipxe_update stage);
install and rollback are a few MB of copying and run inline.
@@ -45,6 +60,8 @@ import config
from services import updates_common as uc
GITHUB_API = "https://api.github.com/repos/ipxe/ipxe/releases/latest"
SHIM_API = "https://api.github.com/repos/ipxe/shim/releases/latest"
SHIM_ASSET = "ipxe-shimx64.efi"
RELEASES_PAGE = "https://github.com/ipxe/ipxe/releases"
LATEST_TARBALL_URL = "https://github.com/ipxe/ipxe/releases/latest/download/ipxeboot.tar.gz"
TARBALL = "ipxeboot.tar.gz"
@@ -54,11 +71,14 @@ 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"),
"ipxe.pxe": ("ipxeboot/x86_64/ipxe.pxe", None),
}
SHIMS = ("ipxe-shim.efi", "snponly-shim.efi")
# Installed by default; ipxe.pxe (legacy BIOS) only when asked.
DEFAULT_INSTALL = ("ipxe-shim.efi", "ipxe.efi")
DEFAULT_INSTALL = ("ipxe-shim.efi", "snponly-shim.efi", "ipxe.efi", "snponly.efi")
# Shown on the page for reference, never written.
READ_ONLY = ("ipxe-bcm.efi",)
@@ -193,6 +213,20 @@ def vendor_cert_cn(path):
return None
def drivers_of(path):
"""'native' when an iPXE build carries its own USB host drivers (it takes
the USB controller from the firmware, so a USB keyboard stops working),
'firmware' when it has none (snponly), '' for anything else."""
try:
with open(path, "rb") as f:
data = f.read()
except OSError:
return ""
if b"iPXE" not in data or b"$Version: " in data:
return "" # not iPXE, or a shim
return "native" if re.search(rb"\0xhci\0|\0ehci\0", data) else "firmware"
def describe(path):
if not os.path.isfile(path):
return None
@@ -210,6 +244,7 @@ def describe(path):
"sbat": sbat_generations(path) if efi else "",
"signers": pairs, "kind": (signature_kind(pairs) if not error else "error") if efi else "bios",
"trusts": vendor_cert_cn(path) if efi else None,
"drivers": drivers_of(path) if efi else "",
"sig_error": error,
}
@@ -229,15 +264,46 @@ def installed():
if info and origin.get("sha256") == info["sha256"]:
info["release"] = origin.get("tag")
rows.append({"name": name, "managed": name in MANAGED, "info": info})
# The shim only starts an ipxe.efi signed under the CA it embeds.
# The shim only starts an iPXE signed under the CA it embeds.
by = {r["name"]: r["info"] for r in rows}
shim, ipxe = by.get("ipxe-shim.efi"), by.get("ipxe.efi")
if shim and ipxe and shim.get("trusts"):
ipxe["chain_ok"] = shim["trusts"] in {i for _, i in ipxe["signers"]}
ipxe["trusts_needed"] = shim["trusts"]
_check_chain(by.get("ipxe-shim.efi") or by.get("snponly-shim.efi"),
[by.get("ipxe.efi"), by.get("snponly.efi")])
return rows
def _check_chain(shim, ipxes):
for ipxe in ipxes:
if shim and ipxe and shim.get("trusts"):
ipxe["chain_ok"] = shim["trusts"] in {i for _, i in ipxe["signers"]}
ipxe["trusts_needed"] = shim["trusts"]
MAC_DIR_RE = re.compile(r"^[0-9a-f]{2}(-[0-9a-f]{2}){5}$")
def mac_folders():
"""Per-client TFTP folders (tftp-unique-root=mac): [{mac, files}], each
file described. Read-only; install never writes into them."""
out = []
try:
names = sorted(os.listdir(config.TFTP_ROOT))
except OSError:
return out
for d in names:
folder = os.path.join(config.TFTP_ROOT, d)
if not (MAC_DIR_RE.match(d) and os.path.isdir(folder)):
continue
files = []
for f in sorted(os.listdir(folder)):
info = describe(os.path.join(folder, f))
if info:
files.append({"name": f, "info": info})
by = {f["name"]: f["info"] for f in files}
_check_chain(next((by[n] for n in SHIMS if n in by), None), [by.get("ipxe.efi")])
out.append({"mac": d.replace("-", ":"), "dir": d, "files": files})
return out
# --- dnsmasq ----------------------------------------------------------------
MAC_RE = re.compile(r"^(?:[0-9a-fA-F*]{1,2}[:-]){5}[0-9a-fA-F*]{1,2}$")
@@ -322,7 +388,7 @@ def dnsmasq_routing():
boots.append({"file": rest[0] if rest else "", "need": need, "exclude": exclude,
"line": s, "source": src})
if key in ("dhcp-boot", "dhcp-host", "dhcp-match", "dhcp-userclass", "dhcp-vendorclass",
"tag-if", "enable-tftp", "tftp-root"):
"tag-if", "enable-tftp", "tftp-root", "tftp-unique-root"):
if key != "dhcp-host" or "set:" in val:
raw.append(s)
for b in boots:
@@ -334,7 +400,8 @@ def dnsmasq_routing():
b["read_only"] = rel in READ_ONLY
b["other_copy"] = rel not in MANAGED and os.path.basename(rel) in MANAGED
return {"tags": tag_macs, "boots": boots, "lines": raw,
"shim_macs": sorted({m for b in boots if os.path.basename(b["file"]) == "ipxe-shim.efi"
"unique_root": any(s.startswith("tftp-unique-root") for s in raw),
"shim_macs": sorted({m for b in boots if os.path.basename(b["file"]) in SHIMS
for m in b["macs"]})}
@@ -459,17 +526,59 @@ def stage(info=None):
else:
ok, error = True, ""
files[name] = dict(d, ok=ok, error=error, source=inner)
# The shim from this release must trust the CA its ipxe.efi is signed by.
shim, ipxe = files.get("ipxe-shim.efi") or {}, 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")
_progress(step="shim")
_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")
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)
return record
def _use_dual_signed_shim(folder, files):
"""Swap the tarball's shim for the ipxe/shim release build when that one
is signed under the Microsoft UEFI CA 2023 and trusts the same iPXE CA.
Any problem leaves the tarball shim in place, with the reason noted."""
tar_shim = files.get("ipxe-shim.efi") or {}
if not tar_shim.get("ok") or tar_shim.get("kind") == "microsoft-2023":
return
try:
rel = json.loads(uc.fetch_text(SHIM_API, timeout=20))
asset = next((a for a in rel.get("assets", []) if a.get("name") == SHIM_ASSET), None)
digest = str((asset or {}).get("digest") or "")
if not asset or not digest.startswith("sha256:"):
raise RuntimeError(f"{rel.get('tag_name')} lists no sha256 for {SHIM_ASSET}")
sha = digest.split(":", 1)[1].lower()
dest = os.path.join(folder, SHIM_ASSET)
if not (os.path.isfile(dest) and uc.file_hash(dest).lower() == sha):
if os.path.exists(dest):
os.remove(dest)
uc.download(asset["browser_download_url"], dest, expect=sha, timeout=300)
d = describe(dest)
if d["kind"] != "microsoft-2023":
raise RuntimeError(f"{SHIM_ASSET} is {d['kind']}, not signed under {MS_CA_2023}")
if d.get("trusts") != tar_shim.get("trusts"):
raise RuntimeError(f"{SHIM_ASSET} trusts {d.get('trusts')}, the tarball's iPXE needs "
f"{tar_shim.get('trusts')}")
except Exception as ex: # optional upgrade: keep the tarball shim
note = f"dual-signed shim not used: {str(ex)[:200]}"
for n in SHIMS:
if n in files:
files[n]["note"] = note
return
for n in SHIMS:
if n in files:
shutil.copy2(dest, os.path.join(folder, n))
files[n] = dict(d, ok=True, error="", source=f"github.com/ipxe/shim {rel.get('tag_name')} "
f"{SHIM_ASSET} (dual-signed, 2011 + 2023)")
def staged(tag=None):
root = os.path.join(_dir(), "staged")
if not os.path.isdir(root):
@@ -647,8 +756,8 @@ def install(tag, names=DEFAULT_INSTALL):
names = [n for n in MANAGED if n in names]
if not names:
raise ValueError("nothing selected")
if "ipxe-shim.efi" in names and "ipxe.efi" not in names:
raise ValueError("install ipxe-shim.efi together with ipxe.efi")
if any(n in names for n in SHIMS) and "ipxe.efi" not in names:
raise ValueError("install a shim together with ipxe.efi")
if uc.job_running(JOB_UNIT):
raise RuntimeError("a download is running; wait for it to finish")
with _Lock():
@@ -663,13 +772,16 @@ def _install(tag, rec, names):
path = os.path.join(rec["folder"], n)
if not os.path.isfile(path) or uc.file_hash(path) != f["sha256"]:
raise ValueError(f"{n}: staged copy changed since it was verified; download again")
if "ipxe.efi" in names and "ipxe-shim.efi" not in names:
# ipxe.efi alone must still be one the installed shim will start.
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"))
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 ipxe.efi is not signed "
"under it, install the shim from the same release too")
for n in ("ipxe.efi", "snponly.efi"):
if n not in names:
continue
issuers = {i for _, i in rec["files"][n].get("signers", [])}
if trusts and trusts not in issuers:
raise ValueError(f"the installed shim trusts {trusts}; this {n} 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])
log = _installed_log()

View File

@@ -37,6 +37,8 @@
{% endif %}
{% if info.trusts %}<div class="sub">Starts second stages signed by <span class="mono">{{ info.trusts }}</span></div>{% endif %}
{% if info.sbat %}<div class="sub">SBAT: <span class="mono">{{ info.sbat }}</span></div>{% endif %}
{% if info.drivers == 'native' %}<div class="notice"><span>This iPXE build drives USB itself: it takes the USB controller from the firmware, so a USB keyboard stops working at the iPXE menu. <span class="mono">snponly.efi</span> uses the firmware's drivers instead.</span></div>
{% elif info.drivers == 'firmware' %}<div class="sub">Uses the firmware's network and USB drivers, so USB keyboards keep working.</div>{% endif %}
{%- endmacro %}
{% block extra_head %}
@@ -162,11 +164,14 @@
<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>)
starts <span class="mono">ipxe.efi</span>, which is signed by the iPXE Secure Boot CA. Updates come from the upstream
iPXE release tarball on GitHub; its 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>,
<span class="mono">dnsmasq.conf</span> and the Blancco files are never changed here.
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.
</p>
</details>
@@ -203,6 +208,42 @@
{% endfor %}
</section>
{# ---- Per-client folders ---------------------------------------------------- #}
{% if mac_folders %}
<section class="panel ui-list" aria-labelledby="mac-h">
<div class="sec-h"><div><h2 id="mac-h" class="sec">Per-client folders</h2>
<div class="sub">With <span class="mono">tftp-unique-root=mac</span>, a client whose MAC has a folder gets every file from it, with no fallback to the root. Read-only here: installs do not update them.</div></div></div>
{% for m in mac_folders %}
<div class="rel-h"><strong class="mono">{{ m.mac }}</strong><span class="sub mono">/srv/tftp/{{ m.dir }}/</span>
<span class="sub">{{ m.files | length }} file{{ 's' if m.files | length != 1 }}</span></div>
{% for f in m.files %}
{% set i = f.info %}
{% set chain_ok = i.chain_ok if i.chain_ok is defined else none %}
<div class="ui-row i-row">
<details>
<summary class="igrid">
<span class="name"><span class="mono">{{ f.name }}</span>{% if i.drivers == 'firmware' and f.name == 'ipxe.efi' %}<span class="sub">snponly build</span>{% endif %}</span>
<span class="tip c-ver" data-tip="{{ i.version or '-' }}"><span class="trunc mono">{{ i.version or '-' }}</span></span>
<span>{% if chain_ok == false %}{{ ui.chip('Shim will refuse', 'err') }}{% else %}{{ sig_chip(i) }}{% endif %}</span>
<span class="chev" aria-hidden="true">&#9662;</span>
</summary>
<div class="ibody">
{{ sig_text(i, chain_ok) }}
<dl class="kv">
<dt>Path</dt><dd class="mono">/srv/tftp/{{ m.dir }}/{{ f.name }}</dd>
<dt>Size</dt><dd>{{ "{:,}".format(i.size) }} B</dd>
<dt>Modified</dt><dd class="mono">{{ i.modified | timestamp_fmt }}</dd>
<dt>sha256</dt><dd class="mono">{{ i.sha256 }}</dd>
</dl>
{{ chain(i) }}
</div>
</details>
</div>
{% endfor %}
{% endfor %}
</section>
{% endif %}
{# ---- Upstream ------------------------------------------------------------ #}
<section class="panel ui-list" aria-labelledby="up-h">
<div class="sec-h"><div><h2 id="up-h" class="sec">Upstream release</h2>
@@ -240,7 +281,7 @@
{% if staged %}
<section class="panel ui-list" aria-labelledby="stg-h">
<div class="sec-h"><div><h2 id="stg-h" class="sec">Downloaded and verified</h2>
<div class="sub">Tick the files to install. ipxe-shim.efi is only installed together with ipxe.efi from the same release; ipxe.pxe is for legacy BIOS clients.</div></div></div>
<div class="sub">Tick the files to install. A shim is only installed together with ipxe.efi from the same release; ipxe.pxe is for legacy BIOS clients.</div></div></div>
{% for s in staged %}
<form method="post" action="{{ url_for('ipxe.install') }}" data-tag="{{ s.tag }}" style="margin: 0"
onsubmit="var n=[].slice.call(this.querySelectorAll('input[name=names]:checked')).map(function(c){return c.value;}); if(!n.length){alert('Select at least one file.');return false;} return confirm('Install ' + n.join(', ') + ' from ' + this.dataset.tag + ' into the TFTP root? The current files are backed up first and every client that boots from now on gets the new ones.');">
@@ -266,8 +307,9 @@
</summary>
<div class="ibody picked">
{% if not f.ok %}<div class="errbox">{{ f.error }}</div>{% elif f.kind %}{{ sig_text(f) }}{% endif %}
{% if f.note %}<div class="notice">{{ f.note }}</div>{% endif %}
<dl class="kv">
<dt>From tarball</dt><dd class="mono">{{ f.source or '-' }}</dd>
<dt>Source</dt><dd class="mono">{{ f.source or '-' }}</dd>
{% if f.sha256 %}<dt>sha256</dt><dd class="mono">{{ f.sha256 }}</dd>{% endif %}
</dl>
{% if f.signers is defined %}{{ chain(f) }}{% endif %}
@@ -288,6 +330,7 @@
<section class="panel ui-list" aria-labelledby="rt-h">
<div class="sec-h"><div><h2 id="rt-h" class="sec">Who gets which file</h2>
<div class="sub">Read from the dnsmasq configuration; never changed here.</div></div></div>
{% if routing.unique_root %}<div class="notice info" style="margin: 0 24px 10px"><span>dnsmasq has <span class="mono">tftp-unique-root=mac</span>: a client with its own folder (see Per-client folders) is served from it instead of the root, whatever file name this list shows.</span></div>{% endif %}
{% if routing.boots %}
{% for b in routing.boots %}
<div class="ui-row">
@@ -375,11 +418,14 @@
<li><strong>Find the release.</strong> The latest release is at
<a href="{{ releases_page }}" target="_blank" rel="noopener" class="mono">{{ releases_page }}</a>. The signed boot files are the
asset <span class="mono">ipxeboot.tar.gz</span>; GitHub shows its sha256 next to the asset (or ask the API:
<span class="mono">curl -s https://api.github.com/repos/ipxe/ipxe/releases/latest | grep -B2 -A2 '"digest"'</span>).</li>
<span class="mono">curl -s https://api.github.com/repos/ipxe/ipxe/releases/latest | grep -B2 -A2 '"digest"'</span>).
The tarball's shim is signed by Microsoft's 2011 CA only; the dual-signed shim (2011 and 2023) is the asset
<span class="mono">ipxe-shimx64.efi</span> of the separate <a href="https://github.com/ipxe/shim/releases" target="_blank" rel="noopener" class="mono">ipxe/shim</a> release.</li>
<li><strong>Download and check the sha256.</strong> It must equal the digest on the release page.
<pre class="cmd mono">cd /tmp
curl -fLO {{ tarball_url }}
sha256sum ipxeboot.tar.gz</pre>
curl -fLO https://github.com/ipxe/shim/releases/latest/download/ipxe-shimx64.efi
sha256sum ipxeboot.tar.gz ipxe-shimx64.efi</pre>
<span class="sub">GitHub goes through Zscaler. A certificate error from curl means the Zscaler root CA is missing from
<span class="mono">/usr/local/share/ca-certificates/</span> (the playbook installs it).</span></li>
<li><strong>Unpack.</strong> The Secure Boot files are in <span class="mono">ipxeboot/x86_64-sb/</span>:
@@ -389,27 +435,34 @@ sha256sum ipxeboot.tar.gz</pre>
<pre class="cmd mono">tar xzf ipxeboot.tar.gz
ls -l ipxeboot/x86_64-sb/</pre></li>
<li><strong>Check the signatures.</strong>
<pre class="cmd mono">sbverify --list ipxeboot/x86_64-sb/shimx64.efi
sbverify --list ipxeboot/x86_64-sb/ipxe.efi</pre>
The shim must be issued by <span class="mono">CN=Microsoft Corporation UEFI CA 2011</span> or, on a newer build,
<span class="mono">CN=Microsoft UEFI CA 2023</span> (prefer a 2023-signed shim when upstream ships one).
<pre class="cmd mono">sbverify --list ipxe-shimx64.efi
sbverify --list ipxeboot/x86_64-sb/ipxe.efi
sbverify --list ipxeboot/x86_64-sb/snponly.efi</pre>
The dual-signed shim shows <span class="mono">CN=Microsoft UEFI CA 2023</span> (sbverify lists the first signature only;
the 2011 one is nested inside it). Fall back to the tarball's <span class="mono">shimx64.efi</span>
(<span class="mono">CN=Microsoft Corporation UEFI CA 2011</span>) only if the dual-signed one is unavailable.
<span class="mono">ipxe.efi</span> must show <span class="mono">CN=iPXE Secure Boot Intermediate ...</span> issued by
<span class="mono">CN=iPXE Secure Boot CA</span>. The shim's <span class="mono">warning: data remaining ... gaps between PE/COFF sections?</span>
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/ipxe.efi "$B"/</pre></li>
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>
<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/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">No dnsmasq restart is needed. Never replace <span class="mono">ipxe-bcm.efi</span>; it serves every other UEFI client.</span></li>
<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 <span class="mono">dhcp-boot=tag:!bios,tag:!ipxe2,ipxe-bcm.efi</span>. Who gets which file
above shows the live lines. Test on one tagged machine before tagging more.</li>
<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>
<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
@@ -433,7 +486,7 @@ install -m 0644 ipxeboot/x86_64-sb/shimx64.efi /srv/tftp/.ipxe-shim.efi.new &amp
.then(function (r) { return r.json(); })
.then(function (j) {
if (j.state !== 'queued' && j.state !== 'running') { location.reload(); return; }
var step = j.step === 'download' ? 'Downloading' : j.step === 'verify' ? 'Verifying' : 'Starting';
var step = j.step === 'download' ? 'Downloading' : j.step === 'verify' ? 'Verifying' : j.step === 'shim' ? 'Fetching the dual-signed shim for' : 'Starting';
if (text) text.textContent = step + ' ' + (j.tag || '') + (j.step === 'download' && j.percent != null ? ' (' + j.percent + '%)' : '') + '...';
if (bar) bar.style.width = (j.step === 'verify' ? 100 : (j.percent || 0)) + '%';
setTimeout(tick, 2000);

View File

@@ -32,6 +32,7 @@ def page():
rows=rows,
installed_sha={n: (i or {}).get("sha256") for n, i in by_name.items()},
routing=ipxe_update.dnsmasq_routing(),
mac_folders=ipxe_update.mac_folders(),
latest=latest,
status=status,
job=ipxe_update.job_state(),