Day 58 — Impermanence

Updated

July 30, 2026

Day 58 — Impermanence

Stage VI · ~4h
Goal: Implement or consciously reject root filesystem impermanence using impermanence patterns—proving that unpersisted state disappears on reboot while /persist (or equivalent) keeps what you declare.

Tip

Impermanence is optional as a lifestyle. It is not optional to understand. Skipping still requires a written “why” for the Stage VI gate. Disk layouts pair with Day 57 Disko; secrets with Days 34–35. Target: NixOS 26.05.

Why this day exists

Snowflakes are not only packages—they are mutated roots: /etc edits, /var surprises, leftover packages. Impermanence makes the root a camping tent and /persist the shipping container. Combined with Disko btrfs/ZFS layouts, reinstalls become boring—and Day 88 disaster recovery gets a cleaner story.

Even if you never wipe root on a daily driver, the inventory habit (what must survive reboot?) is permanent skill.


Theory 1 — Model

boot
  │
  ├─ tmpfs or fresh subvolume for /
  ├─ bind/mount persisted paths from /persist
  └─ everything else is recreated by activation
Path class Examples Strategy
Store /nix Own subvolume/partition; never wipe carelessly
Persist /persist/var/lib/postgres, ssh host keys Explicit list
Ephemeral /var/cache, junk in /root Accept loss
Secrets sops age keys Persist carefully; permissions matter
Boot EFI, kernels, generations Separate story; do not “wipe /boot” casually

What “disappears” means

On reboot, anything not in the persist list is gone if you actually wipe/reset root. Soft modes that only document policy without wipe are still useful—but do not claim impermanence until the reboot proof works.


Theory 2 — impermanence module (nix-community)

# flake input
inputs.impermanence.url = "github:nix-community/impermanence";
# follows nixpkgs when the project supports it
{
  imports = [ impermanence.nixosModules.impermanence ];

  environment.persistence."/persist" = {
    hideMounts = true;
    directories = [
      "/var/log"
      "/var/lib/nixos"
      "/var/lib/systemd/coredump"
      { directory = "/var/lib/tailscale"; mode = "0700"; }
    ];
    files = [
      "/etc/machine-id"
      # ssh host keys often:
      "/etc/ssh/ssh_host_ed25519_key"
      "/etc/ssh/ssh_host_ed25519_key.pub"
    ];
    users.alice = {
      directories = [ "Downloads" ".ssh" ];
      files = [ ".config/sops/age/keys.txt" ]; # only if you accept this layout
    };
  };
}

Exact option paths can track module versions—confirm against current impermanence README when options shift.

neededForBoot

fileSystems."/persist".neededForBoot = true;

If /persist is not available early enough, activation ordering fails in mysterious ways. Treat this as non-negotiable for real setups.


Theory 3 — btrfs rollback-on-boot pattern

Common blog/production pattern:

  1. Root subvolume @
  2. On boot, optionally reset @ to empty snapshot
  3. Mount @persist at /persist
  4. Impermanence binds declared paths
# Conceptual boot.initrd or systemd unit logic—use a maintained pattern
# (e.g. known btrfs wipe scripts in community flakes). Prefer copying a
# pattern you understand over opaque one-liners.
Layout piece Role
@ Ephemeral root
@persist Declared state
@nix Store (never auto-wipe)
EFI Bootloader

ZFS native datasets with mountpoint and rollback are an alternative school—pick one for the lab. Mixing half-understood btrfs blog posts with half-understood ZFS is how you reinstall for the wrong reason.


Theory 4 — What breaks first (expect it)

Symptom Cause Fix
Cannot SSH after reboot Host keys not persisted Persist /etc/ssh/ssh_host_*
Machine ID churn /etc/machine-id ephemeral Persist or accept
sops fails Age key path wiped Persist key path; mode 600
Docker/podman volumes gone State under /var/lib Persist selected dirs
User linger/services odd systemd user state Persist carefully or redesign
Tailscale identity reset State dir wiped Persist with tight mode
NetworkManager connections gone Connection profiles ephemeral Persist or declarative NM

Iterate: wipe/reboot is a test suite, not a one-shot.


Theory 5 — Soft impermanence

Not ready to wipe root? Still gain discipline:

# Document-only policy
# - no manual /etc edits
# - all state dirs listed in inventory
# - weekly diff against expected paths

Or tmpfs for /tmp only—training wheels.

Level What you prove
A Full Wipe + persist list + reboot proof
B Partial Persist mounts without wipe; inventory honest
C Skip Written architecture why mutable root is OK

All three are valid if documented. Silent skip is not.


Theory 6 — Interaction with generations and secrets

Impermanence does not replace Nix generations. Store rollbacks and root wipes are different axes:

Generation rollback  →  previous system closure
Root wipe            →  empty / except binds from /persist
Secrets              →  must live on persist or offline media
Scenario Tool
Bad module activation Generation rollback (Day 13 / Day 89)
Unknown files on root Impermanence wipe
Lost age key Offline backup; Day 88 pain if missing

Theory 7 — Home Manager alignment

If HM manages ~/.config/... on an impermanent home, either:

  • Persist the HM-managed paths you care about, or
  • Accept regenerate-from-flake behavior, or
  • Keep home on a persistent dataset

Mismatch symptoms: “my HM files vanished” after reboot. Align Day 25–27 layouts with today’s persist lists.


Worked example — minimal VM proof

# hosts/imperm-lab/configuration.nix fragments
{
  imports = [
    ./disko-btrfs.nix
    ./persist.nix
  ];

  fileSystems."/persist".neededForBoot = true;

  users.users.root.openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAA… lab"
  ];
  services.openssh.enable = true;
  system.stateVersion = "26.05";
}

Proof protocol:

echo "ephemeral-should-die" | sudo tee /root/SCRATCH
echo "persist-me" | sudo tee /persist/PROOF
sudo reboot
# after boot:
cat /root/SCRATCH     # should fail
cat /persist/PROOF    # should work

Capture both outcomes in notes. A proof that only tests the success path is incomplete.


Lab — multi-step

Suggested workspace notes: ~/lab/90daysofx/02-nixos/day58

Step 1 — Decide commit level

Level Action today
A Full btrfs/ZFS + impermanence module + wipe proof
B Partial Persist module without wipe; list future wipe
C Skip Written architecture review why not (still do path inventory)

Record choice in IMPERMANENCE.md.

Step 2 — State inventory

On current lab host:

sudo du -xh /var/lib | sort -h | tail -n 30
ls /etc/ssh
systemctl --user list-units --failed 2>/dev/null || true

Classify each important path: persist / regenerate / ignore.

Step 3 — Implement level A or B

Wire flake input; persist SSH keys + one service state dir you actually use. Set modes deliberately.

Step 4 — Proof reboot

Create SCRATCH vs PROOF files; reboot; record outcomes with timestamps.

Step 5 — Secrets path

Confirm sops-nix/agenix key path survives (or document regenerate procedure and offline backup location).

Step 6 — Rollback story interaction

Write 5 lines: store generations vs root wipes. Link Day 89 thinking early.

Step 7 — If Level C

Write one page: threat model, backup story (Day 69), why mutable root is acceptable for this host class, and what would trigger revisiting (e.g. multi-user, kiosk, fleet sameness).

Step 8 — Persist list review

Print effective list; remove anything that is “comfort mutable root.” Goal is intentional survival, not nostalgia.


Common gotchas

Symptom / mistake What to do
/persist not neededForBoot Boot fails ordering; set flag
Persisted too little Iterate after each surprise reboot
Persisted too much You rebuilt a mutable root in disguise
Home manager vs system paths Align HM files with persist lists
LUKS + wipe scripts Order of unlock vs mounts
Forgetting /nix separation Do not wipe the store subvolume
SSH host key regenerate Clients scream TOFU; persist keys or automate known_hosts
Age key mode 644 Tighten; treat as root-equivalent

Checkpoint

  • Level A/B/C chosen and documented
  • State inventory exists
  • Reboot proof (or skip rationale) recorded
  • SSH still works after reboot if remote
  • Secrets path understood
  • Generations vs wipe note written

Commit

git add .
git commit -m "day58: impermanence proof or documented skip"

Write three personal gotchas before continuing.

Tomorrow

Day 59 — deploy-rs: push a lab config to a remote VM the boring way.