Impermanence (idea and patterns)
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.
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:
- Root subvolume
@
- On boot, optionally reset
@to empty snapshot
- Mount
@persistat/persist
- 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 pathsOr 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 persistedCapture 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 || trueClassify 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
- Map every path under
/var/liblarger than 100 MB to persist/regenerate/ignore.
- Design persist modes for a postgres host vs a stateless web edge.
- Explain why wiping
/nixis not impermanence (one paragraph).
- Write a failure story: age key on ephemeral root—detection and recovery.
- 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.