Skip to content

fix: a first boot nobody can answer asks nothing, and names the machine - #39

Merged
marcos-mendez merged 1 commit into
masterfrom
fix/headless-first-boot
Oct 3, 2026
Merged

marcos-mendez merged 1 commit into
masterfrom
fix/headless-first-boot

Conversation

@marcos-mendez

Copy link
Copy Markdown
Collaborator

Three findings of the Web image (keel-web 19.0-3, inithooks 2.3.6+keel21) booted headless in an LXC container with no console attached, 2026-10-03.

1. 31fqdn held the first boot. Unless FQDN=SKIP was preseeded, its screen waited on tty1, a pty nobody reads. lib/console.sh is now the one rule for run's notices and every hook that draws a screen. Nobody can answer a console with no size (stty 0 0), one that does not take a 128 KiB NUL probe within 2 s, or no terminal at all. A small write is not enough: an unread pty takes about 17 KB before it blocks (measured on 6.12), so the screen after a small write would still hang. run asks once and exports INITHOOKS_UNATTENDED. From then on the hooks' output goes to the inithooks log, because a console nobody reads blocks its writer once its buffer is full (95secupdates prints the whole upgrade). Each hook logs one line and does what it does with no answer:

  • 30rootpass keeps the password
  • 31fqdn keeps the name and records instance.hostname/fqdn if the name is a valid FQDN
  • 75keel-role and 80keel-cloud run confconsole's screen with stdin from /dev/null. keelfirstboot.py then draws nothing and still stores a preseeded key, so confconsole needs no change.
  • 85secalerts sets no email
  • 95secupdates installs and 99reboot reboots, as SEC_UPDATES=FORCE (the headless preseed) does

The README's preseed example lists FQDN and says an unattended boot skips the questions.

2. CN=core on a machine named web. Given no names, the fork's turnkey-make-ssl-cert sets CN and SAN from hostname -A, a reverse lookup of every address, which falls back to hostname. It supports SANs and --ip. 15regen-sslcert now passes the fqdn and the hostname with --ip (lib/sslcert.sh, names from fqdn.py --machine, no DNS). 31fqdn makes the certificate again when the name it settles is not the CN of a self-signed one. A certificate signed by an authority (confconsole's Let's Encrypt writes the same files) is kept. Nothing is generated at build time.

3. No /etc/hosts line after a skip. 31fqdn now ends every first boot with 127.0.1.1 <fqdn> <hostname>, or 127.0.1.1 <hostname> without a domain, which matches what keel's apply --system writes (keel/system/hosts.py). A kept name keeps the domain its hosts line already gives it.

Tested. New tests/test-console.bats: every interactive hook runs on an unsized pty and on a sized pty whose master is never read, within a deadline, and must finish. Run against master's 31fqdn, those tests fail. Also added: hosts and certificate tests in test-fqdn.bats (CN after a rename with real openssl), test-run.bats, test-secupdates.bats, test-keel-firstboot.bats, test-regen-sslcert.bats, and pytest for --machine. Local run: 367 bats, including the real-dialog pty tests, and 530 pytest. Shell coverage is 99.69 and Python 99.

Three findings of the Web 19.0-3 booted headless in an LXC container with
no console attached (2026-10-03).

31fqdn stopped the first boot for good: unless FQDN=SKIP was preseeded,
its screen waited on tty1, a pty nobody read. lib/console.sh is now the
one rule for run's notices and for every hook that draws a screen
(30rootpass, 31fqdn, 75keel-role and 80keel-cloud through
lib/keel-firstboot.sh, 85secalerts, 95secupdates, 99reboot): nobody can
answer a console with no size, one that does not take a 128 KiB probe of
NUL bytes within 2 s (an unread pty blocks after about 17 KB, so a small
write passes and the screen after it hangs), or no terminal at all. run
asks once, exports the answer as INITHOOKS_UNATTENDED, and sends the
hooks' output to the inithooks log from then on, since a console nobody
reads blocks its writer once full. Each hook does what it does with no
answer and logs one line: the password is kept, the name is kept and
recorded as instance.hostname and instance.fqdn when it has a domain,
confconsole's screens run without a terminal, no alert email is set, and
security updates are installed and a new kernel rebooted into as
SEC_UPDATES=FORCE does, the headless preseed's value.

/etc/hosts had no line for the hostname when 31fqdn was skipped: common
drops 127.0.1.1 at export and 31fqdn wrote the entry only for an answer.
31fqdn now always ends with `127.0.1.1 <fqdn> <hostname>`, or
`127.0.1.1 <hostname>`, the form keel spec apply --system writes; a kept
name keeps the domain its hosts line gives it (fqdn.py --machine).

The certificate said CN=core on a machine named web. turnkey-make-ssl-cert
given no names takes them from `hostname -A`, a reverse lookup of the
machine's addresses. 15regen-sslcert now passes the fqdn and hostname,
with --ip (lib/sslcert.sh), and 31fqdn makes the certificate again when
the settled name is not the CN of a self-signed one; a certificate an
authority signed is kept. No key is generated at build time.
@marcos-mendez
marcos-mendez merged commit f859e67 into master Oct 3, 2026
3 checks passed
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