Skip to content

Address this server by its certificate's name when netboot is HTTPS - #1228

Merged
darksidemilk merged 1 commit into
working-1.6from
netboot-cert-name
Aug 19, 2026
Merged

Address this server by its certificate's name when netboot is HTTPS#1228
darksidemilk merged 1 commit into
working-1.6from
netboot-cert-name

Conversation

@darksidemilk

Copy link
Copy Markdown
Member

The report

An HTTPS netboot with a Let's Encrypt certificate and Secure Boot handed iPXE:

chain https://fog/fog/service/ipxe/boot.php##params

— the server's short hostname, against a certificate issued to
fog.arrowheaddental.com. iPXE has no --insecure and fails the handshake on a
name mismatch, so the boot stopped before it fetched anything.

Root cause

configureDefaultiPXEfile() used $ipaddress for the entire prior life of that
line. When HTTPS netboot arrived it became ${hostname:-$ipaddress}, guarded
only by validip — so the only rejected value was an IPv4 literal, and
validhostname() accepts a single label. A short name passed every check.

The guard was wrong in a way testing could not see. _defaultServerNames()
puts both the FQDN and the short ${hostname%%.*} into the SAN list, so on a
FOG-issued leaf a short name is a genuine SAN and $hostname works fine. But
_createWebLeaf() returns early when acmeLeaf/publicWebCert is set — FOG's
SAN list is never applied to a publicly-issued leaf, which carries only the names
its issuer was asked for. Since publicWebCert is one of exactly two triggers for
HTTPS netboot, this is not an edge case: it is most of the population that selects
HTTPS netboot at all.

The installer already had the right helper, and this was the one self-reference
that missed it. _servedCertName() reads the CN off the leaf the vhost actually
serves, and the vhost's ServerName plus every installer HTTPS self-call had
already been moved onto it.

The second half was worse than the reported case

A boot has two hops with two host sources, and nothing compared them:

Hop URL Host came from
1 default.ipxeboot.php shell $hostname
2..n ${boot-url}/service/ipxe/*, web=, Secure Boot MOK.der / mmx64.efi FOG_WEB_HOST DB row
kernel / init relative — inherits hop 1

FOG_WEB_HOST is seeded from $ipaddress on a fresh schema deploy and was then
never written again — the string appears nowhere in lib/ or bin/. So a
fresh --install-mode public-cert install pointed all of hops 2..n at
https://<address>/, which no public CA will ever certify. The reported install
only broke at hop 1 because its admin had typed the FQDN into FOG Settings by
hand.

The fix

  • _resolveNetbootHost() resolves the name once into $netboothost
    (deliberately not local), so configureDefaultiPXEfile and
    recordNetbootWebHost cannot disagree. Idempotent and silent on a second call.
  • _certServesName() checks it against that certificate using iPXE's rule,
    not OpenSSL's: per ADR 0016 a commonName counts only when there is no
    subjectAltName at all, so once any SAN exists the CN is ignored. IP SANs are
    ignored throughout — they cannot satisfy a URL built from a name.
  • Fatal before anything is written, listing the names the certificate does
    carry. An install that completes having laid down an unbootable default.ipxe
    is worse than one that stops and says why. --extra-server-name is
    deliberately not offered as the remedy — it only feeds FOG's own SAN list.
  • recordNetbootWebHost() records FOG_WEB_HOST from the same resolved name,
    reusing recordGitUpdateSettings()'s existing INSERT … ON DUPLICATE KEY
    pattern.
  • A wildcard-only match is accepted with a printed note rather than trusted
    silently: whether iPXE's x509_check_name() honours a wildcard SAN is
    unverified, as fog-ipxe is an overlay with no upstream crypto/x509.c to read.

Plain-HTTP netboot is untouched and still uses $ipaddress.

Behaviour change worth flagging

Under netbootproto=https only, FOG_WEB_HOST becomes a record rather than
a control — rewritten on every install run, so an edit via FOG Settings will not
survive. It joins FOG_GIT_PATH, FOG_EXTRA_SERVER_NAMES and SERVICE_LOG_PATH.

That gate is load-bearing. On a plain-HTTP install FOG_WEB_HOST is a name plenty
of admins set deliberately, no certificate has to match it, and rewriting it there
would be a regression dressed as a fix.

Tests

tests/netboot-host.test.sh is new because there was no coverage of the
netboot host at all — install-settings-resolution.test.sh covers which
protocol is chosen but never calls the function that writes the URL, which is
why this shipped. 23 assertions, on generated fixtures only (no install, no
network, no root, no DB). It asserts the reported URL end to end and pins the case
most likely to be "simplified" later: that the CN must be ignored once any SAN
exists.

Verified against the baseline by stashing the change and re-running: the suite
goes from 57 passed / 15 failed to 58 / 14, the two failure lists
differing only by this test. The remaining 14 are pre-existing openssl-fixture
and PHP failures on the dev machine, unrelated to this change.

Docs

  • docs/adr/0018-netboot-addresses-this-server-by-its-certificate-name.md — new,
    including the FOG_WEB_HOST record/control change and the rejected
    alternatives (FQDN-shape check only; reading FOG_WEB_HOST for hop 1; deriving
    hops 2..n from $_SERVER['HTTP_HOST']; warning instead of failing).
  • docs/PKI_ZONES.md — its prose said "Set FOG_WEB_HOST to that FQDN or the
    generated boot URLs will not match the certificate"
    and nothing enforced it.
    Now describes what the installer does instead.
  • docs/HTTPPROTO_COVERAGE_AUDIT.md §A2 — the step table listed the URLs without
    saying that <host> is two different values.

Not in scope

  • bootmenu.class.php:477-478 hardcodes /fog/ in $_booturl, so Secure Boot and
    advanced.php URLs ignore FOG_WEB_ROOT — already recorded in
    HTTPPROTO_COVERAGE_AUDIT.md as a Make custom webroot work or remove from the installer #529 leftover. Independent bug, harmless
    at the default webroot.
  • No REST route changed, so OpenAPI::document() needs no edit and there is no
    downstream FogApi class-list sync.

🤖 Generated with Claude Code

An HTTPS netboot handed iPXE https://fog/fog/service/ipxe/boot.php against a
Let's Encrypt certificate issued to fog.arrowheaddental.com. iPXE has no
--insecure and fails the handshake on a name mismatch, so the boot stopped
before it fetched anything.

configureDefaultiPXEfile() used $ipaddress for the whole prior life of that
line. When HTTPS netboot arrived it became ${hostname:-$ipaddress} guarded only
by validip -- so the only rejected value was an IPv4 literal, and
validhostname() accepts a single label. A short name passed every check on the
path.

The guard was wrong in a way testing could not see. _defaultServerNames() puts
both the FQDN and the short ${hostname%%.*} in the SAN list, so on a FOG-issued
leaf a short name is a genuine SAN and $hostname works. But _createWebLeaf()
returns early when acmeLeaf/publicWebCert is set, so FOG's SAN list is never
applied to a publicly-issued leaf -- which carries only the names its issuer was
asked for. publicWebCert is one of exactly two triggers for HTTPS netboot, so
this is not an edge case; it is most of the population that selects HTTPS
netboot at all.

The installer already had the right helper and this was the one self-reference
that missed it: _servedCertName() reads the CN off the leaf the vhost serves,
and the vhost's ServerName and every installer HTTPS self-call had already been
moved onto it.

_resolveNetbootHost() resolves the name once into $netboothost -- not local, on
purpose -- and _certServesName() checks it against that certificate using iPXE's
rule rather than OpenSSL's: per ADR 0016 a commonName counts only when there is
no subjectAltName at all, so once any SAN exists the CN is ignored. IP SANs are
ignored throughout, since they cannot satisfy a URL built from a name. Failing
that check is fatal before anything is written, because an install that
completes having laid down an unbootable default.ipxe is worse than one that
stops and names what the certificate does carry. --extra-server-name is
deliberately not offered as the remedy: it only feeds FOG's own SAN list.

A wildcard-only match is accepted with a printed note rather than trusted
silently -- whether iPXE's x509_check_name() honours a wildcard SAN is
unverified, as fog-ipxe is an overlay with no upstream crypto/x509.c to read.

The second half was worse than the reported case. A boot has two hops with two
host sources: default.ipxe names the server for the fetch of boot.php, and
BootMenu builds everything after it -- the menu, the kernel's web=, the Secure
Boot MOK.der and mmx64.efi -- from FOG_WEB_HOST. That row is seeded from
$ipaddress on a fresh schema deploy and was then never written again; the string
does not appear anywhere in lib/ or bin/. So a fresh --install-mode public-cert
install pointed all of those at https://<address>/, which no public CA will
certify. recordNetbootWebHost() now records it from the same resolved name.

That makes FOG_WEB_HOST a record rather than a control, and only under HTTPS
netboot. The gate is load-bearing: on a plain-HTTP install it is a name plenty
of admins set deliberately, no certificate has to match it, and rewriting it
there would be a regression dressed as a fix. Plain-HTTP netboot keeps using
$ipaddress for the same reason.

tests/netboot-host.test.sh is new because there was no coverage of the netboot
host at all -- install-settings-resolution.test.sh covers which protocol is
chosen but never calls the function that writes the URL, which is why this
shipped. It asserts the reported URL end to end, and pins the case most likely
to be simplified later: that the CN must be ignored once any SAN exists.

Verified against the baseline: the suite goes from 57 passed / 15 failed to
58 / 14, the two failure lists differing only by this test. The other 14 are
pre-existing openssl-fixture and PHP failures on the dev machine.

Co-Authored-By: Claude <noreply@anthropic.com>
@darksidemilk
darksidemilk merged commit 47ddae8 into working-1.6 Aug 19, 2026
3 checks passed
@darksidemilk
darksidemilk deleted the netboot-cert-name branch August 19, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant