{% extends "base.html" %} {% import '_ui.html' as ui %} {% block title %}iPXE boot files - PXE Manager{% endblock %} {# Signer as a short chip; the long explanation goes in the row body (it is a safety warning, so never hover-only). #} {% macro sig_chip(info) -%} {% if info.kind == 'microsoft-2011' %}{{ ui.chip('MS UEFI CA 2011 only', 'warn') }} {% elif info.kind == 'microsoft-2023' %}{{ ui.chip('MS UEFI CA 2023', 'ok') }} {% elif info.kind == 'microsoft' %}{{ ui.chip('Microsoft, other CA', 'neutral') }} {% elif info.kind == 'ipxe' %}{{ ui.chip('iPXE Secure Boot CA', 'ok') }} {% elif info.kind == 'bios' %}{{ ui.chip('Legacy BIOS', 'neutral') }} {% elif info.kind == 'unsigned' %}{{ ui.chip('Unsigned', 'err') }} {% elif info.kind == 'error' %}{{ ui.chip('Not checked', 'err') }} {% else %}{{ ui.chip('Other signer', 'warn') }}{% endif %} {%- endmacro %} {% macro sig_text(info, chain_ok=none) -%} {% if info.kind == 'microsoft-2011' %}
Signed only under Microsoft Corporation UEFI CA 2011. That CA's signing period ended in 2026: machines that trust only the 2023 CA, or a DBX/SBAT revocation update, will refuse this file.
{% elif info.kind == 'microsoft-2023' %}
Signed under Microsoft UEFI CA 2023.
{% elif info.kind == 'ipxe' %}
Chains to the iPXE Secure Boot CA, which the iPXE shim trusts.
{% elif info.kind == 'bios' %}
Legacy BIOS has no Secure Boot; the file is unsigned by design.
{% elif info.kind == 'unsigned' %}
Unsigned: Secure Boot firmware will refuse it.
{% elif info.kind == 'error' %}
Signature not checked: {{ info.sig_error }}
{% elif info.kind == 'microsoft' %}
Signed by Microsoft under a CA other than UEFI CA 2011 or 2023.
{% elif info.kind %}
Signed by a signer that is neither Microsoft nor the iPXE CA.
{% endif %} {% if chain_ok == false %}
Shim will refuse: the installed shim trusts {{ info.trusts_needed }}; this file is not signed under it, so the shim will not start it.
{% endif %} {%- endmacro %} {% macro chain(info) -%} {% if info.signers %}
Signer chain ({{ info.signers | length }}) {% for subject, issuer in info.signers %}
{{ subject }} issued by {{ issuer }}
{% endfor %}
{% endif %} {% if info.trusts %}
Starts second stages signed by {{ info.trusts }}
{% endif %} {% if info.sbat %}
SBAT: {{ info.sbat }}
{% endif %} {% if info.drivers == 'native' %}
This iPXE build drives USB itself: it takes the USB controller from the firmware, so a USB keyboard stops working at the iPXE menu. snponly.efi uses the firmware's drivers instead.
{% elif info.drivers == 'firmware' %}
Uses the firmware's network and USB drivers, so USB keyboards keep working.
{% endif %} {%- endmacro %} {% block extra_head %} {% endblock %} {% block content %}
Updates

iPXE boot files

{% if status == 'update' %}{{ ui.chip('Update available: ' ~ latest.tag, 'act') }} {% elif status == 'current' %}{{ ui.chip('Up to date (' ~ latest.tag ~ ')', 'ok') }} {% elif status == 'newer' %}{{ ui.chip('Ahead of ' ~ latest.tag, 'neutral') }} {% elif latest %}{{ ui.chip('Latest: ' ~ latest.tag, 'neutral') }} {% else %}{{ ui.chip('Upstream not checked', 'off') }}{% endif %} {% if latest %}Checked {{ latest.checked | local_dt }}{% endif %}
Speed cap {{ ('%g MB/s' | format(settings.max_mbps)) if settings.max_mbps else 'none' }}
{% set can_stage = latest and latest.sha256 and job.state not in ('queued', 'running') %}
{# ---- Job ----------------------------------------------------------------- #} {% if job.state %}
{% if job.state in ('queued', 'running') %}
{{ 'Starting' if job.state == 'queued' else (job.step | capitalize) }} {{ job.tag }}...
{% elif job.state == 'error' %}
Download of {{ job.tag }} failed: {{ job.error }}
{% elif job.state == 'interrupted' %}
The download of {{ job.tag }} stopped without finishing (server restart?). Start it again; it resumes the partial file.
{% elif job.state == 'done' and job.failed %}
{{ job.tag }} downloaded, but {{ job.failed | join(', ') }} failed verification; see Downloaded and verified below.
{% endif %}
{% endif %}
What this page manages

The Secure Boot chain dnsmasq hands every UEFI client over TFTP: the Microsoft-signed shim (ipxe-shim.efi, dual-signed under Microsoft UEFI CA 2011 and 2023) starts ipxe.efi, which is signed by the iPXE Secure Boot CA. ipxe.efi 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 sbverify before it can be installed. ipxe-bcm.efi (the old build, kept as the rollback), dnsmasq.conf and the Blancco files are never changed here.

{# ---- Installed ----------------------------------------------------------- #}

Installed on the TFTP root

One line per boot file. Open a row for its signer chain, SBAT and hash.
{% for r in rows %} {% set i = r.info %} {% set chain_ok = i.chain_ok if i and i.chain_ok is defined else none %}
{{ r.name }}{% if not r.managed %}read-only here{% endif %} {% if i %}{{ i.version or '-' }}{% else %}Not present{% endif %} {% if not i %}{{ ui.chip('Missing', 'off') }}{% elif chain_ok == false %}{{ ui.chip('Shim will refuse', 'err') }}{% else %}{{ sig_chip(i) }}{% endif %}
{% if i %} {{ sig_text(i, chain_ok) }}
{% if i.release %}
From release
{{ i.release }}
{% endif %}
Size
{{ "{:,}".format(i.size) }} B
Modified
{{ i.modified | timestamp_fmt }}
sha256
{{ i.sha256 }}
{{ chain(i) }} {% else %}
Not present on the TFTP root.
{% endif %}
{% endfor %}
{# ---- Per-client folders ---------------------------------------------------- #} {% if mac_folders %}

Per-client folders

With tftp-unique-root=mac, 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.
{% for m in mac_folders %}
{{ m.mac }}/srv/tftp/{{ m.dir }}/ {{ m.files | length }} file{{ 's' if m.files | length != 1 }}
{% for f in m.files %} {% set i = f.info %} {% set chain_ok = i.chain_ok if i.chain_ok is defined else none %}
{{ f.name }}{% if i.drivers == 'firmware' and f.name == 'ipxe.efi' %}snponly build{% endif %} {{ i.version or '-' }} {% if chain_ok == false %}{{ ui.chip('Shim will refuse', 'err') }}{% else %}{{ sig_chip(i) }}{% endif %}
{{ sig_text(i, chain_ok) }}
Path
/srv/tftp/{{ m.dir }}/{{ f.name }}
Size
{{ "{:,}".format(i.size) }} B
Modified
{{ i.modified | timestamp_fmt }}
sha256
{{ i.sha256 }}
{{ chain(i) }}
{% endfor %} {% endfor %}
{% endif %} {# ---- Upstream ------------------------------------------------------------ #}

Upstream release

The latest GitHub release of iPXE, as last checked.
{% if latest %}
{{ latest.tag }} {% if latest.name and latest.name != latest.tag %}{{ latest.name }}, {% endif %}published {{ latest.published | local_dt }} {% if status == 'update' %}{{ ui.chip('Newer than installed', 'act') }} {% elif status == 'current' %}{{ ui.chip('Installed', 'ok') }} {% elif status == 'newer' %}{{ ui.chip('Installed is newer', 'neutral') }} {% else %}{{ ui.chip('Installed version unknown', 'off') }}{% endif %}
Published
{{ latest.published | local_dt }}
Checked
{{ latest.checked | local_dt }}
{% if latest.size %}
Tarball
{{ "%.1f"|format(latest.size / 1048576) }} MB
{% endif %}
sha256
{{ latest.sha256 or 'not published' }}
Release page
{{ latest.page }}
{% if not latest.sha256 %}
GitHub lists no sha256 for this release, so it cannot be downloaded here; install it by hand (see the instructions below).
{% endif %}
{% else %}
Not checked yet. Check now asks the GitHub API for the latest release (a few seconds).
{% endif %}
{# ---- Staged -------------------------------------------------------------- #} {% if staged %}

Downloaded and verified

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.
{% for s in staged %}
{{ s.tag }} verified {{ s.staged | local_dt }} tarball sha256 {{ (s.tarball_sha256 or '')[:12] }} matches GitHub
{% for name, f in s.files.items() %} {% set fid = 'inst-' ~ loop.index ~ '-' ~ s.tag %}
{{ name }}{% if name not in default_install %}optional{% endif %} {{ f.version or '-' }}{% if f.sha256 and f.sha256 == installed_sha[name] %} (same as installed){% endif %} {% if not f.ok %}{{ ui.chip('Failed', 'err') }}{% elif f.kind %}{{ sig_chip(f) }}{% else %}{{ ui.chip('OK', 'ok') }}{% endif %}
{% if not f.ok %}
{{ f.error }}
{% elif f.kind %}{{ sig_text(f) }}{% endif %} {% if f.note %}
{{ f.note }}
{% endif %}
Source
{{ f.source or '-' }}
{% if f.sha256 %}
sha256
{{ f.sha256 }}
{% endif %}
{% if f.signers is defined %}{{ chain(f) }}{% endif %}
{% endfor %}
The current files are backed up first.
{% endfor %}
{% endif %} {# ---- dnsmasq routing ----------------------------------------------------- #}

Who gets which file

Read from the dnsmasq configuration; never changed here.
{% if routing.unique_root %}
dnsmasq has tftp-unique-root=mac: a client with its own folder (see Per-client folders) is served from it instead of the root, whatever file name this list shows.
{% endif %} {% if routing.boots %} {% for b in routing.boots %}
{{ b.file }} {% if b.managed %}{{ ui.chip('updated here', 'w11') }}{% elif b.read_only %}{{ ui.chip('never touched', 'neutral') }}{% elif b.other_copy %}{{ ui.chip('other copy, not updated here', 'warn') }}{% endif %} {% if b.macs %}{{ b.macs | length }} tagged client{{ 's' if b.macs | length != 1 }} ({{ b.need | join(', ') }}) {%- elif b.need %}clients matching {{ b.need | join(' and ') }} {% else %}every client not excluded{% endif -%}{% if b.exclude %}; not {{ b.exclude | join(', ') }}{% endif %}
{% if b.other_copy %}
This page installs into the TFTP root only; this copy in a subfolder is not updated.
{% endif %}
Requires tag
{{ b.need | join(', ') or '-' }}
Unless tagged
{{ b.exclude | join(', ') or '-' }}
{% if b.macs %}
Clients
{{ b.macs | join(', ') }}
{% endif %}
Line
{{ b.line }}
{% endfor %}
Relevant dnsmasq lines ({{ routing.lines | length }})
{% for l in routing.lines %}{{ l }}
{% endfor %}
{% else %}
No dhcp-boot lines found in the dnsmasq configuration.
{% endif %}
{# ---- Backups ------------------------------------------------------------- #}

Backups

One is made automatically before every install and rollback. Restore is in each row's menu.
{% for b in backups %}
{{ b.id }} {{ b.reason or '-' }} {{ b.files | join(', ') or 'empty' }}
{% if b.reason %}
{{ b.reason }}
{% endif %} {% for f in b.files %}
{{ f }} {{ b.versions[f] or '' }}
{% else %}
Empty backup.
{% endfor %}
{% if b.files %}
{% call ui.more('backup ' ~ b.id) %} {# The id is a folder name (hand-made backups too): keep it out of the JS source. #}
{% endcall %}
{% endif %}
{% else %}
No backups yet. One is made automatically before every install and rollback.
{% endfor %}
{# ---- How-to -------------------------------------------------------------- #}

Find and install the latest signed build by hand

For when the buttons above cannot reach GitHub, or to check what they do.

All commands run on the PXE server as root.

  1. Find the release. The latest release is at {{ releases_page }}. The signed boot files are the asset ipxeboot.tar.gz; GitHub shows its sha256 next to the asset (or ask the API: curl -s https://api.github.com/repos/ipxe/ipxe/releases/latest | grep -B2 -A2 '"digest"'). The tarball's shim is signed by Microsoft's 2011 CA only; the dual-signed shim (2011 and 2023) is the asset ipxe-shimx64.efi of the separate ipxe/shim release.
  2. Download and check the sha256. It must equal the digest on the release page.
    cd /tmp
    curl -fLO {{ tarball_url }}
    curl -fLO https://github.com/ipxe/shim/releases/latest/download/ipxe-shimx64.efi
    sha256sum ipxeboot.tar.gz ipxe-shimx64.efi
    GitHub goes through Zscaler. A certificate error from curl means the Zscaler root CA is missing from /usr/local/share/ca-certificates/ (the playbook installs it).
  3. Unpack. The Secure Boot files are in ipxeboot/x86_64-sb/: shimx64.efi (the shim), ipxe.efi and snponly.efi. ipxe-shim.efi and snponly-shim.efi there are only symlinks to shimx64.efi. The legacy BIOS build is ipxeboot/x86_64/ipxe.pxe.
    tar xzf ipxeboot.tar.gz
    ls -l ipxeboot/x86_64-sb/
  4. Check the signatures.
    sbverify --list ipxe-shimx64.efi
    sbverify --list ipxeboot/x86_64-sb/ipxe.efi
    sbverify --list ipxeboot/x86_64-sb/snponly.efi
    The dual-signed shim shows CN=Microsoft UEFI CA 2023 (sbverify lists the first signature only; the 2011 one is nested inside it). Fall back to the tarball's shimx64.efi (CN=Microsoft Corporation UEFI CA 2011) only if the dual-signed one is unavailable. ipxe.efi must show CN=iPXE Secure Boot Intermediate ... issued by CN=iPXE Secure Boot CA. The shim's warning: data remaining ... gaps between PE/COFF sections? is normal. Do not rely on sbverify --cert: it reports OK even for the wrong certificate.
  5. Back up the current files. A backup made in this folder also shows in the Backups list above, with Restore.
    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"/
  6. Install both files, from the same release. The shim only starts an ipxe.efi signed under the CA it carries, and it always loads ipxe.efi from the TFTP root, so the name must stay ipxe.efi. Copy to a temporary name and rename, so a client that is booting never reads half a file:
    install -m 0644 ipxeboot/x86_64-sb/snponly.efi /srv/tftp/.ipxe.efi.new      && mv -f /srv/tftp/.ipxe.efi.new      /srv/tftp/ipxe.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
    Note the first line: the snponly build goes in as ipxe.efi. Never install the release's full x86_64-sb/ipxe.efi (it kills USB keyboards), and never replace ipxe-bcm.efi (the rollback). No dnsmasq restart is needed.
  7. Who gets it. dnsmasq decides, in /etc/dnsmasq.conf: every UEFI client gets the shim through dhcp-boot=tag:!bios,tag:!ipxe,ipxe-shim.efi. To roll back, point that line at ipxe-bcm.efi and restart dnsmasq; to keep one model on the old build, tag its MAC (dhcp-host=<mac>,set:oldipxe) and add dhcp-boot=tag:oldipxe,tag:!ipxe,tag:!bios,ipxe-bcm.efi above the default line. Who gets which file above shows the live lines.
  8. Roll back. Use Restore in the Backups list, or copy the backed-up files back the same way (temporary name, then mv -f).
  9. Normal log noise. While booting, the shim asks TFTP for optional files such as revocations_*.efi and shim_certificate_0.efi. The "not found" lines for them in the dnsmasq log are expected and harmless.
{% if job.state in ('queued', 'running') %} {% endif %} {% endblock %}