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:
@@ -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()
|
||||
|
||||
@@ -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">▾</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">▾</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" && cp -p /srv/tftp/ipxe-shim.efi /srv/tftp/ipxe.efi "$B"/</pre></li>
|
||||
mkdir -p "$B" && 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 && 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 && 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 && 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</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=<mac>,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 &
|
||||
.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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user