nixos/etc-overlay: ship empty /etc/machine-id on immutable /etc

The symlink to /var/lib/nixos/machine-id never persists the ID,
systemd-machine-id-commit.service requires a writable /etc, and
machine_id_commit() does not follow symlinks for its mountpoint check.
So the backing file stays "uninitialized", every boot is
ConditionFirstBoot=yes, and the machine-id is random per boot.

Ship an empty regular file instead, systemd then overlays /run/machine-id
for the session, ConditionFirstBoot is correctly "no", commit is
cleanly condition-skipped, and the file is a usable bind target for
users that want persistence.

Fixes #523878
This commit is contained in:
r-vdp 2026-06-09 16:54:19 +03:00
commit 3eaafe4d48
No known key found for this signature in database
2 changed files with 25 additions and 10 deletions

View file

@ -182,16 +182,18 @@
})
(lib.mkIf (config.system.etc.overlay.enable && !config.system.etc.overlay.mutable) {
# Systemd requires /etc/machine-id exists or can be initialized on first
# boot. This file should not be part of an image or system config because
# it is unique to the machine, so it is initialized at first boot and
# persisted in the system state directory, /var/lib/nixos.
environment.etc."machine-id".source = lib.mkDefault "/var/lib/nixos/machine-id";
boot.initrd.systemd.tmpfiles.settings.machine-id."/sysroot/var/lib/nixos/machine-id".f =
lib.mkDefault
{
argument = "uninitialized";
};
# An empty regular file means systemd will bind mount /run/machine-id
# on top, and ConditionFirstBoot will be false (the file will never
# change, so this makes sense). See machine-id(5) "First Boot
# Semantics". It also serves as a target to bind mount an actually
# persistent machine-id onto. A symlink doesn't work here since
# systemd-machine-id-commit checks /etc/machine-id itself for being a
# mountpoint without following symlinks, so it would never commit
# through a symlink.
environment.etc.machine-id = lib.mkDefault {
text = "";
mode = "0444";
};
})
];

View file

@ -76,6 +76,19 @@
with subtest("/etc is mounted as an overlay"):
machine.succeed("findmnt --kernel --type overlay /etc")
with subtest("machine-id is set up without first-boot looping"):
# The baked-in placeholder is an empty regular file; systemd overlays
# /run/machine-id on top so the session has a valid ID while the
# commit service is condition-skipped (no writable /etc to commit to).
machine.succeed("stat --format '%F' /etc/machine-id | tee /dev/stderr | grep -q 'regular'")
machine.succeed("grep -qE '^[0-9a-f]{32}$' /etc/machine-id")
machine.fail("journalctl -b | grep -F 'System cannot boot: Missing /etc/machine-id'")
machine.fail("journalctl -b | grep -F 'Detected first boot'")
machine.fail("systemctl is-failed --quiet systemd-machine-id-commit.service")
assert machine.succeed(
"systemctl show -P ConditionResult systemd-machine-id-commit.service"
).strip() == "no"
with subtest("modes work correctly"):
machine.succeed("stat --format '%F' /etc/modetest | tee /dev/stderr | grep -q 'regular file'")
machine.succeed("stat --format '%F' /etc/modetest2 | tee /dev/stderr | grep -q 'regular file'")