Second review pass: strip account fields, clear stale cidata inputs, await OEM finalization

Codex xhigh round 2:

- Strip account authentication fields (!users, !root-password,
  root_enc_password, auth_config account entries) from the archinstall config
  in OEM mode, not just user_credentials — a combined/legacy config could
  otherwise have archinstall create a provisioning account before OOBE.
- Clear stale OEM inputs (oem marker, credentials, optional files) from /root
  before copying a cidata drive, so a retried install in the same live session
  can't force a fresh drive back into OEM mode or reuse an old LUKS passphrase.
- The test harness waits for the OEM pending flag to clear before saving the
  base image, so it never snapshots a half-finalized first boot; it also
  handles the deliberately skipped mode picker on blank disks built against a
  pre-OEM runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5 committed 2026-08-07 19:03:49 -07:00
1 parent e49ea2bc96
commit 8aee09cb29
5 files changed
+90 -2

No files matched your search

+20 -1
View File
@@ -703,7 +703,7 @@ drive_oem_setup() {
capture_console "success-oem-02-password"
press ret
wait_for_screen "Confirm" 60
wait_for_screen "Conf *irm" 60
type_text "$GUEST_PASSWORD"
press ret
@@ -932,6 +932,25 @@ install_phase() {
fi
bootstrap_ssh
# SSH can come up (console bootstrap logs in on a spare TTY) while
# omarchy-oem-setup is still finalizing the user and re-keying LUKS. Saving
# the base mid-finalization produces a flaky image, so wait for the pending
# flag to clear before powering off.
if $OEM; then
log "Waiting for OEM first-boot setup to finish"
local waited=0
while ssh_guest "test -f /var/lib/omarchy/oem/pending" 2>/dev/null; do
if ((waited >= 900)); then
capture_console "failure-oem-finalization-timeout"
echo "Timed out waiting for OEM first-boot setup to complete" >&2
return 1
fi
sleep 10
((waited += 10))
done
capture_console "success-oem-06-setup-complete"
fi
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install-timing.json 2>/dev/null" \
>"$RUN_DIR/omarchy-install-timing.json" 2>/dev/null || true
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/pacman.log 2>/dev/null" \
@@ -38,10 +38,20 @@ done
mkdir -p "$mnt"
mount -o ro "$device" "$mnt" || exit 1
# Clear any inputs a previous load in this live session left in /root before
# copying the current drive: a stale `oem` marker or credentials file would
# otherwise force a fresh drive's config back into OEM mode or hand it an old
# LUKS passphrase. The interactive wizard writes these same names, but a
# cidata load always precedes it, so nothing of the user's is discarded here.
optional_inputs=(user_credentials.json oem user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey)
for file in user_configuration.json "${optional_inputs[@]}"; do
rm -f "$dest/$file"
done
loaded=1
if [[ -f $mnt/user_configuration.json ]] && [[ -f $mnt/user_credentials.json || -f $mnt/oem ]]; then
files=("$mnt"/user_configuration.json)
for file in user_credentials.json oem user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey; do
for file in "${optional_inputs[@]}"; do
[[ -f $mnt/$file ]] && files+=("$mnt/$file")
done
if cp "${files[@]}" "$dest"/; then
@@ -76,6 +76,14 @@ class InstallContext:
arch_configuration = dict(user_configuration)
arch_configuration.pop("omarchy_install", None)
if oem:
# The userless invariant covers the archinstall config too: a
# combined/legacy config could carry account authentication that
# archinstall would still act on (users, a root password). Strip
# every such field so no account is created before first boot.
_strip_account_fields(arch_configuration)
state_dir = Path(os.environ.get("OMARCHY_INSTALL_STATE_DIR", "/run/omarchy-install"))
state_dir.mkdir(parents=True, exist_ok=True)
@@ -135,6 +143,20 @@ class InstallContext:
return self.mode == "protected"
def _strip_account_fields(arch_configuration: dict) -> None:
"""Remove every field archinstall reads to create users or set a root
password, so an OEM install cannot ship a hidden provisioning account.
Encryption material lives elsewhere (disk_config.disk_encryption) and is
untouched."""
for key in ("!users", "!root-password", "root_enc_password", "users"):
arch_configuration.pop(key, None)
auth = arch_configuration.get("auth_config")
if isinstance(auth, dict):
for key in ("users", "root_enc_password"):
auth.pop(key, None)
def _inject_oem_encryption_password(arch_configuration: dict, user_credentials: dict) -> None:
"""Ensure an encrypted OEM install has a LUKS passphrase archinstall can
use. Reuse one the input already carries (an autoinstall rig may supply
+23
View File
@@ -153,6 +153,29 @@ attach_drive cidata
[[ ! -e $sandbox/root/oem ]] || fail "oem marker alone copies nothing"
pass "oem marker alone falls back to the wizard"
# Stale OEM inputs from a previous load in the same session are cleared before
# the current drive is copied: a normal drive must not inherit an old oem
# marker or credentials.
new_sandbox
attach_drive cidata
write_required_pair
: >"$sandbox/root/oem" # leftover from a prior OEM load
echo 'old-keys' >"$sandbox/root/authorized_keys"
run_load || fail "normal drive after a stale OEM load loads"
[[ ! -e $sandbox/root/oem ]] || fail "stale oem marker is cleared"
[[ ! -e $sandbox/root/authorized_keys ]] || fail "stale optional inputs are cleared"
pass "stale OEM inputs are cleared before loading a normal drive"
# A drive that isn't an autoinstall drive at all still clears stale inputs so
# the wizard that follows doesn't inherit them.
new_sandbox
attach_drive cidata
echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json" # half a pair
: >"$sandbox/root/oem"
! run_load || fail "half-pair drive still falls back"
[[ ! -e $sandbox/root/oem ]] || fail "stale oem marker cleared even on fallback"
pass "stale inputs are cleared even when falling back to the wizard"
# Half the required pair is not an autoinstall drive: unmount and fall back.
new_sandbox
attach_drive cidata
+14
View File
@@ -137,6 +137,20 @@ class ContextOemTest(unittest.TestCase):
self.assertEqual(synthesized["users"], [])
self.assertNotIn("root_enc_password", synthesized)
def test_oem_strips_account_fields_from_arch_config(self):
config = self.base_config(oem=True)
config["!users"] = [{"username": "rig"}]
config["!root-password"] = "hunter2"
config["root_enc_password"] = "x"
config["auth_config"] = {"users": [{"username": "rig"}], "root_enc_password": "x"}
self.write_config(config)
ctx = self.from_env()
arch_config = json.loads(ctx.arch_config_path.read_text())
for key in ("!users", "!root-password", "root_enc_password", "users"):
self.assertNotIn(key, arch_config)
self.assertEqual(arch_config["auth_config"], {})
def test_oem_unencrypted_touches_nothing(self):
self.write_config(self.base_config(oem=True))
ctx = self.from_env()