Archived
Fix lxc-* hosts never completing first-boot user/secrets activation
virtualisation/proxmox-lxc.nix registers the Nix store DB via a systemd service, never an activation script -- so neededForUsers sops secrets (password hashes) and the user-creation step that consumes them never ran on a real first boot, leaving /etc/shadow stuck with build-time placeholder entries. boot.postBootCommands looked like the right hook (stage-2-init.sh does invoke it) but switch-to-configuration behaves unreliably that early, before systemd itself is up. Fixed with a genuine oneshot systemd service, gated by ConditionPathExists so it only ever runs once. Confirmed live via a from-scratch destroy+rebuild+redeploy of the lxc-nix-cache test container: real password hashes applied automatically, systemctl is-system-running -> running, zero failed units. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
This commit is contained in:
@@ -85,4 +85,64 @@ in
|
|||||||
mode = "0644";
|
mode = "0644";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
|
||||||
|
# store DB via a systemd service (register-nix-paths) -- it never runs
|
||||||
|
# an activation script at all. Confirmed live this means neither
|
||||||
|
# sops-nix's "for users" secrets (password hashes -- installed by the
|
||||||
|
# activation script itself, not a systemd service, since they need to
|
||||||
|
# exist *before* user creation) nor the user-creation step that
|
||||||
|
# consumes them ever run on a real lxc-* boot. Regular secrets
|
||||||
|
# (nix-serve's key, beszel's token, etc.) work anyway because sops-nix
|
||||||
|
# provides its own systemd service for those.
|
||||||
|
#
|
||||||
|
# A systemd service, not boot.postBootCommands: tried that first (it's
|
||||||
|
# a genuine, generally-invoked hook -- nixos/modules/system/boot/stage-2-init.sh,
|
||||||
|
# which becomes this container's actual /sbin/init, unconditionally
|
||||||
|
# runs it) but switch-to-configuration behaves differently that early in
|
||||||
|
# boot (raw stage-2-init.sh, before systemd itself has even started) --
|
||||||
|
# confirmed live it silently failed to rewrite /etc/shadow from there
|
||||||
|
# even in "test" mode, despite the exact same command working reliably
|
||||||
|
# every time when run post-boot (i.e. as a normal systemd service, which
|
||||||
|
# is what this is). Not fully root-caused why the early context
|
||||||
|
# specifically breaks it; a real systemd service sidesteps needing to.
|
||||||
|
#
|
||||||
|
# /etc/shadow already has PLACEHOLDER entries for every declared user
|
||||||
|
# baked in at build time (part of constructing the system closure).
|
||||||
|
# update-users-groups.pl deliberately never overwrites an *existing*
|
||||||
|
# shadow entry -- a correct safety property in general (don't clobber a
|
||||||
|
# real user's real password on a config rebuild) -- but on a genuine
|
||||||
|
# first boot that only means the real hashedPasswordFile-derived hash
|
||||||
|
# never gets the chance to be applied either, since the placeholder is
|
||||||
|
# already "seen". Safe to clear here specifically: there is no real
|
||||||
|
# password yet to protect on a first boot.
|
||||||
|
#
|
||||||
|
# "test" mode, not "boot": confirmed live "boot" mode aborts partway
|
||||||
|
# through (before rewriting /etc/shadow) on a warning that "/boot" is on
|
||||||
|
# a different filesystem -- a real check for a host with a bootloader to
|
||||||
|
# update, meaningless for a container that has none
|
||||||
|
# (boot.loader.{grub,systemd-boot}.enable are both false above), but it
|
||||||
|
# still aborts the script. "test" runs every activation step without
|
||||||
|
# touching boot-loader state at all.
|
||||||
|
#
|
||||||
|
# ConditionPathExists (systemd-native, not a bash-level check) means
|
||||||
|
# this only ever runs once, on the genuine first boot -- systemd itself
|
||||||
|
# skips even starting it on every later boot once the marker exists.
|
||||||
|
# switch-to-configuration is otherwise the operator's call per this
|
||||||
|
# repo's own safety rules, not something to run on every boot.
|
||||||
|
systemd.services.nixos-lxc-first-boot-activate = {
|
||||||
|
description = "Complete first-boot NixOS activation (users, secrets) for this LXC container";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
unitConfig.ConditionPathExists = "!/var/lib/nixos-lxc-first-boot-activated";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
rm -f /etc/shadow
|
||||||
|
/run/current-system/bin/switch-to-configuration test
|
||||||
|
mkdir -p /var/lib
|
||||||
|
touch /var/lib/nixos-lxc-first-boot-activated
|
||||||
|
'';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user