From 9254b63a18c52e82d752a077a795d9f652b15669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ryan4yin=20=7C=20=E4=BA=8C=E8=8A=B1?= Date: Fri, 2 Oct 2026 21:58:21 +0800 Subject: [PATCH] chore(idols-ai): move tmp state off RAM onto btrfs and harden tmp mounts (#348) * feat(idols-ai): persist /var/tmp and small state dirs, wipe /var/tmp at boot Move the strata model staging area (~3G) off the root tmpfs onto the persistent btrfs volume to free RAM, preserve btrbk/cups/logrotate state, and wipe /var/tmp at boot so it only accumulates between reboots. * chore(idols-ai): add nosuid,nodev to root tmpfs and tmp btrfs mounts Hardens /, /tmp and /snapshots; safe because /dev, /run, /nix and /run/setuid-programs are separate mounts, so nothing there needs suid bits or device nodes. * chore(idols-ai): add noatime to the @tmp btrfs mount Temp files do not need access times; matches the @nix subvolume style and saves btrfs metadata writes. * chore(idols-ai): harden the /var/tmp bind mount with nosuid,nodev World-writable staging data should not honor suid bits or device nodes, same as /tmp and /snapshots. * fix(idols-ai): preserve logrotate state as a file, fix persisted dir ownership - /var/lib/logrotate.status is a regular file, not a directory: move it from preservation directories to files. A missing state entry makes logrotate treat every log as freshly rotated, so it must survive reboots. - keep /var/lib/btrbk owned by btrbk:btrbk 0750 so preservation does not override the btrbk module's rule with root:root. - set /var/tmp mode 1777 explicitly instead of relying on systemd's tmp.conf ordering. - rewrite comments to describe the paths generically. * chore(idols-ai): cap journal, wipe coredumps at boot, persist upower/noctalia - services.journald.settings.Journal: SystemMaxUse=10G, RuntimeMaxUse=256M, matching the cap already set on idols-kana/idols-ruby. - wipe /var/lib/systemd/coredump at boot: it lives in the persisted /var/lib/systemd and crash dumps can contain secrets copied from memory. - persist /var/lib/upower (device battery history) and ~/.local/state/noctalia (shell settings, notification/clipboard history). --- hosts/idols-ai/default.nix | 7 ++++++ hosts/idols-ai/disko-fs.nix | 7 ++++++ hosts/idols-ai/preservation.nix | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/hosts/idols-ai/default.nix b/hosts/idols-ai/default.nix index 2abdd882..3f806bb8 100644 --- a/hosts/idols-ai/default.nix +++ b/hosts/idols-ai/default.nix @@ -62,6 +62,13 @@ in modules.btrbk.enable = true; + # systemd-journal - cap disk usage + # https://www.freedesktop.org/software/systemd/man/latest/journald.conf.html + services.journald.settings.Journal = { + SystemMaxUse = "10G"; + RuntimeMaxUse = "256M"; + }; + # QEMU/KVM(HostCpuOnly) for local VMs. environment.systemPackages = [ pkgs.qemu_kvm ]; diff --git a/hosts/idols-ai/disko-fs.nix b/hosts/idols-ai/disko-fs.nix index a0698564..50514bc2 100644 --- a/hosts/idols-ai/disko-fs.nix +++ b/hosts/idols-ai/disko-fs.nix @@ -17,6 +17,8 @@ nodev."/" = { fsType = "tmpfs"; mountOptions = [ + "nosuid" # Ignore setuid bit; no suid binaries live on the root tmpfs + "nodev" # Ignore device files; nothing here needs to be a device node "size=4G" "relatime" # Update inode access times relative to modify/change time "mode=755" @@ -96,12 +98,17 @@ "@snapshots" = { mountpoint = "/snapshots"; mountOptions = [ + "nosuid" + "nodev" "compress-force=zstd:1" ]; }; "@tmp" = { mountpoint = "/tmp"; mountOptions = [ + "nosuid" + "nodev" + "noatime" # Temp files don't need access times; saves btrfs metadata writes "compress-force=zstd:1" ]; }; diff --git a/hosts/idols-ai/preservation.nix b/hosts/idols-ai/preservation.nix index 9361611c..d68fa279 100644 --- a/hosts/idols-ai/preservation.nix +++ b/hosts/idols-ai/preservation.nix @@ -51,10 +51,36 @@ in "/var/log" # system caches (e.g. restic, plocate; slow to rebuild) "/var/cache" + # General scratch space. It is re-creatable and can grow large, so keep it + # off the root tmpfs and on the persistent volume. Contents only + # accumulate between reboots: the tmpfiles rule below wipes it at boot. + # World-writable scratch, so harden the bind mount like /tmp; the mode + # must stay 1777 to match that `D!` rule. + { + directory = "/var/tmp"; + mode = "1777"; + mountOptions = [ + "nosuid" + "nodev" + ]; + } + # btrbk state/home (SSH keys, cache). The owner must match the btrbk + # module's `d /var/lib/btrbk 0750 btrbk btrbk` rule, which preservation + # would otherwise override with the default root:root. + { + directory = "/var/lib/btrbk"; + user = "btrbk"; + group = "btrbk"; + mode = "0750"; + } + # CUPS state (the print spool lives in /var/spool/cups, not here) + "/var/lib/cups" # system-core "/var/lib/nixos" "/var/lib/systemd" + # upower device battery history (e.g. Magic Trackpad) + "/var/lib/upower" { directory = "/var/lib/private"; mode = "0700"; @@ -82,6 +108,10 @@ in file = "/etc/machine-id"; inInitrd = true; } + # logrotate's state file (a regular file, not a directory). It records the + # last rotation per log; a missing entry makes logrotate treat the log as + # freshly rotated, so the state must survive reboots. + { file = "/var/lib/logrotate.status"; } ]; # the following directories will be passed to /persistent/home/$USER @@ -127,6 +157,7 @@ in ".local/state/home-manager" ".local/state/nix/profiles" + ".local/state/noctalia" # shell settings, notification/clipboard history ".local/share/nix" # ====================================== @@ -372,6 +403,18 @@ in "/home/${username}/.terraform.d".d = permission; }; + # /var/tmp is re-creatable scratch, but it must not consume RAM, so it is + # bind-mounted onto the persistent volume above. Wipe it at boot (mirrors + # nixpkgs `boot.tmp.cleanOnBoot` for /tmp) so contents only accumulate + # between reboots. + # + # Crash dumps live in the persisted /var/lib/systemd and can contain secrets + # copied out of process memory, so don't keep them across reboots either. + systemd.tmpfiles.rules = [ + "D! /var/tmp 1777 root root" + "D! /var/lib/systemd/coredump 0755 root root" + ]; + # systemd-machine-id-commit.service would fail but it is not relevant # in this specific setup for a persistent machine-id so we disable it #