Files
nix-config-ryan4yin/hosts/idols-aquamarine/preservation.nix
T
Ryan Yin 560a6cc2d8 fix(preservation): mount /etc/ssh in initrd so key changes apply on boot
NixOS copies mode-carrying /etc entries such as authorized_keys.d during
stage-2 activation, which runs before systemd starts. preservation mounted the
persisted /etc/ssh from preservation.target afterwards and hid that copy, so a
key change only took effect on `switch`: revoking a key and rebooting silently
restored the old one.

Prepare the mount in initrd, before activation runs, and cover it with a VM
test that seeds a stale authorized_keys file, reboots, and asserts the declared
key replaced it. The same test fails on that assertion with inInitrd disabled.

Verified on all four hosts: /persistent/etc/ssh/authorized_keys.d/root is now
rewritten at boot instead of keeping the timestamp of the last switch.

Assisted-by: Claude Code (claude-opus-5-5)
2026-10-31 01:09:16 +08:00

139 lines
4.4 KiB
Nix

{
preservation,
lib,
pkgs,
myvars,
...
}:
let
inherit (myvars) username;
in
{
imports = [
preservation.nixosModules.default
];
preservation.enable = true;
environment.systemPackages = [
# Whole-filesystem disk usage (`-x` stays on one filesystem):
# sudo gdu -n -x / # non-interactive, plain text
pkgs.gdu
];
# NOTE: preservation only mounts the directory/file list below to /persistent
# If the directory/file already exists in the root filesystem you should
# move those files/directories to /persistent first!
preservation.preserveAt."/persistent" = {
# Mount in initrd: NixOS copies mode-carrying /etc entries (such as
# authorized_keys.d) during stage-2 activation, which runs before systemd
# starts. A mount created by preservation.target comes later and would hide
# those copies behind the previous `switch`, so key changes only applied on
# `switch`, never on `boot` + reboot. Preparing the mount in initrd puts
# /persistent/etc/ssh at /sysroot/etc/ssh before activation runs.
directories = [
{
directory = "/etc/ssh";
inInitrd = true;
}
"/etc/nix/inputs"
"/etc/secureboot" # lanzaboote - secure boot (see ./secureboot.nix)
"/var/log"
"/var/lib"
"/var/cache"
# Re-creatable scratch, but it must not consume the root tmpfs (RAM), so
# it is bind-mounted onto the persistent volume and wiped at boot below.
# The mode must stay 1777 to match that `D!` rule.
{
directory = "/var/tmp";
mode = "1777";
mountOptions = [
"nosuid"
"nodev"
];
}
];
files = [
# auto-generated machine ID
{
file = "/etc/machine-id";
inInitrd = true;
}
];
# the following directories will be passed to /persistent/home/$USER
users.${username} = {
directories = [
# Rootless Podman's image and layer store. On the root tmpfs the 4 GiB root
# fills up during the first build, so it lives on the persistent volume
# like it does on `ai`.
".local/share/containers"
"codes"
# Third-party source checkouts, the same layout `ai` uses: Strata, the
# inference engine whose image this host builds, lives at ~/src/Strata.
"src"
"nix-config"
"tmp"
# Model weights live here. The root filesystem is tmpfs, so anything
# not listed is gone on the next boot; weights are re-downloadable, but
# re-downloading tens of GB is not a boot step.
"models"
# Caches. A cold flake cache makes `nix run <flake-ref>` re-download its
# input at boot, which needs DNS, and on a tmpfs root cache writes are RAM.
".cache"
".config/systemd/user"
".config/containers/systemd"
];
};
};
# The parents of the persisted paths below are not themselves persisted, so
# tmpfiles would create them root:root 0755 and the user could not write under
# them. Same fix as hosts/idols-ai/preservation.nix.
systemd.tmpfiles.settings.preservation = {
"/home/${username}/.cache".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.config".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.config/systemd".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.config/containers".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.local".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.local/share".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
};
systemd.tmpfiles.rules = [
# /var/tmp only accumulates between reboots (mirrors `boot.tmp.cleanOnBoot`
# for /tmp). Crash dumps can hold secrets copied out of process memory.
"D! /var/tmp 1777 root root"
"D! /var/lib/systemd/coredump 0755 root root"
];
# Mask the commit service: /etc/machine-id is persisted by preservation
# (see the `files` entry above), so there is no transient ID to commit.
systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];
}