Impermanence (idea and patterns)

Updated

July 30, 2026

Impermanence (idea and patterns)

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.” Disk layouts pair with Disko; secrets with sops-nix/agenix. Target: NixOS 26.05.

Why it matters

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 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";
{
  imports = [ inputs.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"
      "/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 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
# Prefer copying a pattern you understand over opaque one-liners.
# Pair with Disko btrfs subvolumes from the Disko chapter.
Layout piece Role
@ Ephemeral root
@persist Declared state
@nix Store (never auto-wipe)
EFI Bootloader

ZFS native datasets with 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
ACME/certs confusion Live cert state paths Persist or use DNS-01 carefully

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
Unknown files on root Impermanence wipe
Lost age key Offline backup; real 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 Home Manager layouts with persist lists.

Services checklist (starting set)

Path Usually
/etc/ssh/ssh_host_* Persist
/var/lib/nixos Persist
/var/log Persist or ship logs off-box
/var/lib/postgresql Persist + backups
/var/cache Ephemeral
/tmp Ephemeral
sops key Persist, mode 600
/var/lib/docker Prefer not to; redesign or persist knowingly

Worked example — minimal VM proof

# hosts/imperm-lab/configuration.nix fragments
{ inputs, ... }:
{
  imports = [
    ./disko-btrfs.nix
    inputs.impermanence.nixosModules.impermanence
    ./persist.nix
  ];

  fileSystems."/persist".neededForBoot = true;

  users.users.root.openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAA… lab"
  ];
  services.openssh.enable = true;
  system.stateVersion = "26.05";
}
# persist.nix
{
  environment.persistence."/persist" = {
    hideMounts = true;
    directories = [
      "/var/log"
      "/var/lib/nixos"
    ];
    files = [
      "/etc/machine-id"
      "/etc/ssh/ssh_host_ed25519_key"
      "/etc/ssh/ssh_host_ed25519_key.pub"
    ];
  };
}

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
ssh lab true          # should still work if keys persisted

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


Worked example — IMPERMANENCE.md template

# IMPERMANENCE.md
## Level
A / B / C

## Inventory (path → persist|regenerate|ignore)
- 

## Wipe mechanism
btrfs reset @ / tmpfs / none

## Proof
- date:
- SCRATCH gone: yes/no
- PROOF present: yes/no
- SSH after reboot: yes/no

## Secrets
- age key path:
- offline backup location:

## Why not full wipe (if B/C)
- 

Lab — multi-step

Suggested notes: ~/lab/nixos/ops/impermanence

Step 1 — Decide commit level

Level Action
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 2>/dev/null | 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.

Step 7 — If Level C

Write one page: threat model, backup story, 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.

Step 9 — Second surprise reboot

After a week of use (or a forced second cycle), reboot again; capture new paths that “should have been persisted.” Update the list.


Exercises

  1. Map every path under /var/lib larger than 100 MB to persist/regenerate/ignore.
  2. Design persist modes for a postgres host vs a stateless web edge.
  3. Explain why wiping /nix is not impermanence (one paragraph).
  4. Write a failure story: age key on ephemeral root—detection and recovery.
  5. Compare tmpfs-root vs btrfs-reset-root (pros/cons table).

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
Age key mode 644 Tighten; treat as root-equivalent
Claiming impermanence without wipe Call it policy, not impermanence

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
  • Persist list reviewed for “too much”

Commit

git add .
git commit -m "ops: impermanence level and reboot proof"

Write three personal gotchas before deploy-rs.