Address this server by its certificate's name when netboot is HTTPS - #1228
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The report
An HTTPS netboot with a Let's Encrypt certificate and Secure Boot handed iPXE:
— the server's short hostname, against a certificate issued to
fog.arrowheaddental.com. iPXE has no--insecureand fails the handshake on aname mismatch, so the boot stopped before it fetched anything.
Root cause
configureDefaultiPXEfile()used$ipaddressfor the entire prior life of thatline. When HTTPS netboot arrived it became
${hostname:-$ipaddress}, guardedonly by
validip— so the only rejected value was an IPv4 literal, andvalidhostname()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 aFOG-issued leaf a short name is a genuine SAN and
$hostnameworks fine. But_createWebLeaf()returns early whenacmeLeaf/publicWebCertis set — FOG'sSAN list is never applied to a publicly-issued leaf, which carries only the names
its issuer was asked for. Since
publicWebCertis one of exactly two triggers forHTTPS 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 actuallyserves, and the vhost's
ServerNameplus every installer HTTPS self-call hadalready 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:
default.ipxe→boot.php$hostname${boot-url}/service/ipxe/*,web=, Secure BootMOK.der/mmx64.efiFOG_WEB_HOSTDB rowFOG_WEB_HOSTis seeded from$ipaddresson a fresh schema deploy and was thennever written again — the string appears nowhere in
lib/orbin/. So afresh
--install-mode public-certinstall pointed all of hops 2..n athttps://<address>/, which no public CA will ever certify. The reported installonly 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), soconfigureDefaultiPXEfileandrecordNetbootWebHostcannot 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
commonNamecounts only when there is nosubjectAltNameat all, so once any SAN exists the CN is ignored. IP SANs areignored throughout — they cannot satisfy a URL built from a name.
carry. An install that completes having laid down an unbootable
default.ipxeis worse than one that stops and says why.
--extra-server-nameisdeliberately not offered as the remedy — it only feeds FOG's own SAN list.
recordNetbootWebHost()recordsFOG_WEB_HOSTfrom the same resolved name,reusing
recordGitUpdateSettings()'s existingINSERT … ON DUPLICATE KEYpattern.
silently: whether iPXE's
x509_check_name()honours a wildcard SAN isunverified, as
fog-ipxeis an overlay with no upstreamcrypto/x509.cto read.Plain-HTTP netboot is untouched and still uses
$ipaddress.Behaviour change worth flagging
Under
netbootproto=httpsonly,FOG_WEB_HOSTbecomes a record rather thana control — rewritten on every install run, so an edit via FOG Settings will not
survive. It joins
FOG_GIT_PATH,FOG_EXTRA_SERVER_NAMESandSERVICE_LOG_PATH.That gate is load-bearing. On a plain-HTTP install
FOG_WEB_HOSTis a name plentyof 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.shis new because there was no coverage of thenetboot host at all —
install-settings-resolution.test.shcovers whichprotocol 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_HOSTrecord/control change and the rejectedalternatives (FQDN-shape check only; reading
FOG_WEB_HOSTfor hop 1; derivinghops 2..n from
$_SERVER['HTTP_HOST']; warning instead of failing).docs/PKI_ZONES.md— its prose said "SetFOG_WEB_HOSTto that FQDN or thegenerated 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 withoutsaying that
<host>is two different values.Not in scope
bootmenu.class.php:477-478hardcodes/fog/in$_booturl, so Secure Boot andadvanced.phpURLs ignoreFOG_WEB_ROOT— already recorded inHTTPPROTO_COVERAGE_AUDIT.mdas a Make custom webroot work or remove from the installer #529 leftover. Independent bug, harmlessat the default webroot.
OpenAPI::document()needs no edit and there is nodownstream FogApi class-list sync.
🤖 Generated with Claude Code