{% 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 %} {%- 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 that dnsmasq hands out over TFTP: the Microsoft-signed shim (ipxe-shim.efi) starts ipxe.efi, 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 sbverify before it can be installed. ipxe-bcm.efi, 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 %}
{# ---- 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. ipxe-shim.efi 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 %}
From tarball
{{ 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.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"').
  2. Download and check the sha256. It must equal the digest on the release page.
    cd /tmp
    curl -fLO {{ tarball_url }}
    sha256sum ipxeboot.tar.gz
    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 ipxeboot/x86_64-sb/shimx64.efi
    sbverify --list ipxeboot/x86_64-sb/ipxe.efi
    The shim must be issued by CN=Microsoft Corporation UEFI CA 2011 or, on a newer build, CN=Microsoft UEFI CA 2023 (prefer a 2023-signed shim when upstream ships one). 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/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
    No dnsmasq restart is needed. Never replace ipxe-bcm.efi; it serves every other UEFI client.
  7. Who gets it. dnsmasq decides, in /etc/dnsmasq.conf: clients listed as dhcp-host=<mac>,set:ipxe2 get the shim through dhcp-boot=tag:ipxe2,tag:!ipxe,tag:!bios,ipxe-shim.efi; every other UEFI client gets ipxe-bcm.efi through dhcp-boot=tag:!bios,tag:!ipxe2,ipxe-bcm.efi. Who gets which file above shows the live lines. Test on one tagged machine before tagging more.
  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 %}