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

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