diff --git a/hosts/artemis/default.nix b/hosts/artemis/default.nix index e717f65..92e2077 100644 --- a/hosts/artemis/default.nix +++ b/hosts/artemis/default.nix @@ -77,6 +77,9 @@ in sessionVariables.WINE_BIN = getExe pkgs.wine; }; + environment.persistence."/persist".enable = mkForce true; + environment.persistence."/state".enable = mkForce true; + modules = { services = { borgmatic = { diff --git a/hosts/artemis/disko-config.nix b/hosts/artemis/disko-config.nix index f6af2c7..6fe9766 100644 --- a/hosts/artemis/disko-config.nix +++ b/hosts/artemis/disko-config.nix @@ -35,80 +35,59 @@ ashift = "12"; }; rootFsOptions = { - canmount = "off"; - mountpoint = "none"; - dnodesize = "auto"; + compression = "zstd"; + acltype = "posix"; + atime = "off"; xattr = "sa"; + dnodesize = "auto"; + mountpoint = "none"; + canmount = "off"; + devices = "off"; + exec = "off"; + setuid = "off"; }; - postCreateHook = "zfs snapshot rpool@blank"; datasets = { - local = { + "local" = { type = "zfs_fs"; + }; + "local/root" = { + type = "zfs_fs"; + mountpoint = "/"; options = { - mountpoint = "none"; + canmount = "noauto"; + mountpoint = "/"; + exec = "on"; + setuid = "on"; }; + postCreateHook = "zfs snapshot rpool/local/root@blank"; }; "local/nix" = { type = "zfs_fs"; mountpoint = "/nix"; options = { - atime = "off"; - mountpoint = "legacy"; + canmount = "noauto"; + mountpoint = "/nix"; + exec = "on"; + setuid = "on"; }; }; - "local/tmp" = { + "local/state" = { type = "zfs_fs"; - mountpoint = "/tmp"; + mountpoint = "/state"; options = { - setuid = "off"; - devices = "off"; - mountpoint = "legacy"; + canmount = "noauto"; + mountpoint = "/state"; }; }; - system = { + "safe" = { type = "zfs_fs"; - mountpoint = "/"; - options = { - mountpoint = "legacy"; - }; }; - "system/var" = { + "safe/persist" = { type = "zfs_fs"; - mountpoint = "/var"; + mountpoint = "/persist"; options = { - mountpoint = "legacy"; - }; - }; - "system/var/tmp" = { - type = "zfs_fs"; - mountpoint = "/var/tmp"; - options = { - devices = "off"; - mountpoint = "legacy"; - }; - }; - "system/var/log" = { - type = "zfs_fs"; - mountpoint = "/var/log"; - options = { - compression = "on"; - acltype = "posix"; - mountpoint = "legacy"; - }; - }; - user = { - type = "zfs_fs"; - options = { - mountpoint = "none"; - }; - }; - "user/home" = { - type = "zfs_fs"; - mountpoint = "/home"; - options = { - setuid = "off"; - devices = "off"; - mountpoint = "legacy"; + canmount = "noauto"; + mountpoint = "/persist"; }; }; }; diff --git a/hosts/common.nix b/hosts/common.nix index 3d0e333..4160463 100644 --- a/hosts/common.nix +++ b/hosts/common.nix @@ -4,12 +4,12 @@ pkgs, ... }: - { imports = [ inputs.agenix.nixosModules.age inputs.home-manager.nixosModules.home-manager ../modules/nixos + ../modules/nixos/impermanence.nix ]; nixpkgs = { diff --git a/hosts/library/ai.nix b/hosts/library/ai.nix index 7b53720..3f715da 100644 --- a/hosts/library/ai.nix +++ b/hosts/library/ai.nix @@ -36,4 +36,11 @@ modules.services.borgmatic.directories = [ "/var/lib/private/open-webui" ]; + + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/private/open-webui"; + mode = "0700"; + } + ]; } diff --git a/hosts/library/grafana.nix b/hosts/library/grafana.nix index 771f61c..b2478de 100644 --- a/hosts/library/grafana.nix +++ b/hosts/library/grafana.nix @@ -1,4 +1,5 @@ { + config, ... }: @@ -13,4 +14,13 @@ }; }; }; + + environment.persistence."/persist".directories = [ + { + directory = config.services.grafana.dataDir; + user = "grafana"; + group = "grafana"; + mode = "0700"; + } + ]; } diff --git a/hosts/library/jellyfin.nix b/hosts/library/jellyfin.nix index 16a00cb..40b6115 100644 --- a/hosts/library/jellyfin.nix +++ b/hosts/library/jellyfin.nix @@ -24,6 +24,22 @@ dataDir = "/var/lib/jellyfin"; }; + environment.persistence."/state".directories = [ + { + directory = config.services.jellyfin.cacheDir; + inherit (config.services.jellyfin) user group; + mode = "0700"; + } + ]; + + environment.persistence."/persist".directories = [ + { + directory = config.services.jellyfin.dataDir; + inherit (config.services.jellyfin) user group; + mode = "0700"; + } + ]; + modules.services.borgmatic.directories = [ config.services.jellyfin.dataDir ]; diff --git a/hosts/library/jellysearch.nix b/hosts/library/jellysearch.nix index 159a2fa..b11a892 100644 --- a/hosts/library/jellysearch.nix +++ b/hosts/library/jellysearch.nix @@ -55,4 +55,8 @@ MEILI_URL = "http://localhost:${toString config.services.meilisearch.listenPort}"; }; }; + + environment.persistence."/state".directories = [ + config.systemd.services.jellysearch.serviceConfig.WorkingDirectory + ]; } diff --git a/hosts/library/prometheus.nix b/hosts/library/prometheus.nix index 37509ba..b5dbfaf 100644 --- a/hosts/library/prometheus.nix +++ b/hosts/library/prometheus.nix @@ -32,4 +32,13 @@ } ]; }; + + environment.persistence."/state".directories = [ + { + directory = "/var/lib/${config.services.prometheus.stateDir}"; + user = "prometheus"; + group = "prometheus"; + mode = "0700"; + } + ]; } diff --git a/hosts/mail/mail.nix b/hosts/mail/mail.nix index 84aa2fb..213a90a 100644 --- a/hosts/mail/mail.nix +++ b/hosts/mail/mail.nix @@ -85,4 +85,52 @@ in smtp_destination_concurrency_limit = "20"; header_size_limit = "4096000"; }; + + environment.persistence."/persist".directories = [ + { + directory = "/var/dkim"; + user = "rspamd"; + group = "rspamd"; + mode = "0755"; + } + { + directory = "/var/sieve"; + user = "virtualMail"; + group = "virtualMail"; + mode = "0770"; + } + { + directory = "/var/vmail"; + user = "virtualMail"; + group = "virtualMail"; + mode = "0700"; + } + { + directory = "/var/lib/rspamd"; + user = "rspamd"; + group = "rspamd"; + mode = "0700"; + } + { + directory = "/var/lib/redis-rspamd"; + user = "redis-rspamd"; + group = "redis-rspamd"; + mode = "0700"; + } + { + directory = "/var/lib/opendkim"; + user = 221; + group = 221; + mode = "0700"; + } + { + directory = "/var/lib/knot-resolver"; + user = "knot-resolver"; + group = "knot-resolver"; + mode = "0770"; + } + "/var/lib/dhparams" + "/var/lib/dovecot" + "/var/lib/postfix" + ]; } diff --git a/hosts/pi/home-assistant/default.nix b/hosts/pi/home-assistant/default.nix index 7e6bdd2..f3493b1 100644 --- a/hosts/pi/home-assistant/default.nix +++ b/hosts/pi/home-assistant/default.nix @@ -276,6 +276,15 @@ lovelaceConfigWritable = true; }; + environment.persistence."/persist".directories = [ + { + directory = config.services.home-assistant.configDir; + user = "hass"; + group = "hass"; + mode = "0700"; + } + ]; + modules.services.borgmatic.directories = [ config.services.home-assistant.configDir ]; diff --git a/hosts/pi/home-assistant/mqtt.nix b/hosts/pi/home-assistant/mqtt.nix index 19a568d..8027f68 100644 --- a/hosts/pi/home-assistant/mqtt.nix +++ b/hosts/pi/home-assistant/mqtt.nix @@ -69,6 +69,21 @@ }; }; + environment.persistence."/persist".directories = [ + { + directory = config.services.zigbee2mqtt.dataDir; + user = "zigbee2mqtt"; + group = "zigbee2mqtt"; + mode = "0700"; + } + { + directory = config.services.mosquitto.dataDir; + user = "mosquitto"; + group = "mosquitto"; + mode = "0700"; + } + ]; + modules.services.borgmatic.directories = [ config.services.mosquitto.dataDir config.services.zigbee2mqtt.dataDir diff --git a/hosts/server.nix b/hosts/server.nix index 9b2cfe0..cb08f41 100644 --- a/hosts/server.nix +++ b/hosts/server.nix @@ -65,6 +65,13 @@ in ]; }; + environment.persistence."/state".directories = [ + { + directory = "/var/lib/fail2ban"; + mode = "0750"; + } + ]; + services.openssh.settings.PermitRootLogin = mkForce "prohibit-password"; modules.services.tailscale = { diff --git a/hosts/skycam/default.nix b/hosts/skycam/default.nix index 4dcebd3..b0ef928 100644 --- a/hosts/skycam/default.nix +++ b/hosts/skycam/default.nix @@ -79,6 +79,10 @@ }; }; + environment.persistence."/persist".directories = [ + "/var/lib/skycam-archiver" + ]; + modules.services.borgmatic = { enable = true; directories = [ diff --git a/hosts/vps1/gitea.nix b/hosts/vps1/gitea.nix index cf8c829..9fffc5d 100644 --- a/hosts/vps1/gitea.nix +++ b/hosts/vps1/gitea.nix @@ -86,4 +86,12 @@ in packages.CHUNKED_UPLOAD_PATH = lib.mkForce "${stateDir}/data/tmp/package-upload"; }; }; + + environment.persistence."/persist".directories = [ + { + directory = config.services.gitea.stateDir; + inherit (config.services.gitea) user group; + mode = "0700"; + } + ]; } diff --git a/hosts/vps1/headscale.nix b/hosts/vps1/headscale.nix index dfad72d..e2c659a 100644 --- a/hosts/vps1/headscale.nix +++ b/hosts/vps1/headscale.nix @@ -48,6 +48,13 @@ in }; }; + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/headscale"; + inherit (config.services.headscale) user group; + } + ]; + services.nginx.virtualHosts = { "${domain}" = { forceSSL = true; diff --git a/hosts/vps1/kanidm.nix b/hosts/vps1/kanidm.nix index af6128e..d2a2a9b 100644 --- a/hosts/vps1/kanidm.nix +++ b/hosts/vps1/kanidm.nix @@ -49,4 +49,13 @@ in postRun = "systemctl restart kanidm.service"; group = "acme"; }; + + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/kanidm"; + user = "kanidm"; + group = "kanidm"; + mode = "0700"; + } + ]; } diff --git a/hosts/vps1/matrix.nix b/hosts/vps1/matrix.nix index 92c0585..7fc3891 100644 --- a/hosts/vps1/matrix.nix +++ b/hosts/vps1/matrix.nix @@ -216,4 +216,23 @@ in } // commonBridgeSettings "mautrix-whatsapp"; }; + + environment.persistence."/persist".directories = [ + { + directory = config.services.matrix-synapse.dataDir; + user = "matrix-synapse"; + group = "matrix-synapse"; + mode = "0700"; + } + { + directory = "/var/lib/mautrix-signal"; + user = "mautrix-signal"; + group = "mautrix-signal"; + } + { + directory = "/var/lib/mautrix-whatsapp"; + user = "mautrix-whatsapp"; + group = "mautrix-whatsapp"; + } + ]; } diff --git a/hosts/vps1/photoprism.nix b/hosts/vps1/photoprism.nix index 9848859..55348a8 100644 --- a/hosts/vps1/photoprism.nix +++ b/hosts/vps1/photoprism.nix @@ -32,6 +32,14 @@ in file = "${inputs.secrets}/passwords/services/photoprism/admin.age"; }; + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/private/photoprism"; + user = "photoprism"; + group = "photoprism"; + } + ]; + services.photoprism = { enable = true; address = "localhost"; diff --git a/modules/nixos/impermanence.nix b/modules/nixos/impermanence.nix new file mode 100644 index 0000000..b1d5566 --- /dev/null +++ b/modules/nixos/impermanence.nix @@ -0,0 +1,144 @@ +{ + config, + pkgs, + lib, + ... +}: +let + inherit (lib) + attrNames + flip + isAttrs + mapAttrs + mkIf + mkMerge + mkOption + optionals + types + ; +in +{ + boot.zfs.forceImportRoot = false; + boot.initrd.systemd.enable = true; + boot.initrd.systemd.services.impermanence-rollback = + mkIf + (config.environment.persistence."/persist".enable || config.environment.persistence."/state".enable) + { + description = "Rollback root filesystem"; + wantedBy = [ "initrd.target" ]; + after = [ "zfs-import-rpool.service" ]; + before = [ "sysroot.mount" ]; + unitConfig.DefaultDependencies = "no"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.zfs}/bin/zfs rollback -r rpool/local/root@blank"; + }; + }; + + age.identityPaths = [ "/persist/etc/ssh/ssh_host_ed25519_key" ]; + + fileSystems."/state" = mkIf config.environment.persistence."/state".enable { + neededForBoot = true; + }; + environment.persistence."/state" = { + enable = false; + hideMounts = true; + directories = [ + "/var/lib/systemd" + "/var/log" + "/var/spool" + ]; + }; + + fileSystems."/persist" = mkIf config.environment.persistence."/persist".enable { + neededForBoot = true; + }; + environment.persistence."/persist" = { + enable = false; + hideMounts = true; + files = [ + (mkIf (!config.boot.isContainer) "/etc/machine-id") + "/etc/adjtime" + "/etc/ssh/ssh_host_ed25519_key" + "/etc/ssh/ssh_host_ed25519_key.pub" + ]; + directories = [ + "/var/lib/nixos" + ] + ++ optionals config.security.acme.acceptTerms [ + { + directory = "/var/lib/acme"; + user = "acme"; + group = "acme"; + mode = "0755"; + } + ] + ++ optionals config.services.printing.enable [ + { + directory = "/var/lib/cups"; + mode = "0700"; + } + ] + ++ optionals config.hardware.bluetooth.enable [ + "/var/lib/bluetooth" + ]; + }; + + users.mutableUsers = !config.environment.persistence."/persist".enable; + + # For each user that has a home-manager config, merge the locally defined + # persistence options that we defined above. + imports = + let + mkUserFiles = map ( + x: { parentDirectory.mode = "700"; } // (if isAttrs x then x else { file = x; }) + ); + mkUserDirs = map (x: { mode = "700"; } // (if isAttrs x then x else { directory = x; })); + in + [ + { + environment.persistence = mkMerge ( + flip map (attrNames config.home-manager.users) ( + user: + let + hmUserCfg = config.home-manager.users.${user}; + in + flip mapAttrs hmUserCfg.home.persistence ( + _: sourceCfg: { + users.${user} = { + files = mkUserFiles sourceCfg.files; + directories = mkUserDirs sourceCfg.directories; + }; + } + ) + ) + ); + } + ]; + + home-manager.sharedModules = [ + { + options.home.persistence = mkOption { + description = "Additional persistence config for the given source path"; + default = { }; + type = types.attrsOf ( + types.submodule { + options = { + files = mkOption { + description = "Additional files to persist via NixOS impermanence."; + type = types.listOf (types.either types.attrs types.str); + default = [ ]; + }; + + directories = mkOption { + description = "Additional directories to persist via NixOS impermanence."; + type = types.listOf (types.either types.attrs types.str); + default = [ ]; + }; + }; + } + ); + }; + } + ]; +} diff --git a/modules/nixos/podman.nix b/modules/nixos/podman.nix index 87752e1..ff1a7a1 100644 --- a/modules/nixos/podman.nix +++ b/modules/nixos/podman.nix @@ -40,6 +40,10 @@ in }; + environment.persistence."/persist".directories = [ + "/var/lib/containers/storage" + ]; + networking.firewall.interfaces."podman+" = { allowedUDPPorts = [ 53 ]; allowedTCPPorts = [ 53 ]; diff --git a/modules/nixos/services/postgresql.nix b/modules/nixos/services/postgresql.nix index 37d6412..eb7e955 100644 --- a/modules/nixos/services/postgresql.nix +++ b/modules/nixos/services/postgresql.nix @@ -30,6 +30,15 @@ in }; }; + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/postgresql"; + user = "postgres"; + group = "postgres"; + mode = "0700"; + } + ]; + services.borgmatic.settings = { postgresql_databases = [ { diff --git a/modules/nixos/services/tailscale.nix b/modules/nixos/services/tailscale.nix index e28e8cd..55bb0f3 100644 --- a/modules/nixos/services/tailscale.nix +++ b/modules/nixos/services/tailscale.nix @@ -56,5 +56,9 @@ in trustedInterfaces = [ "tailscale0" ]; allowedUDPPorts = [ config.services.tailscale.port ]; }; + + environment.persistence."/state".directories = [ + "/var/lib/tailscale" + ]; }; } diff --git a/modules/nixos/system/desktop/gnome.nix b/modules/nixos/system/desktop/gnome.nix index 69c19bb..22e966d 100644 --- a/modules/nixos/system/desktop/gnome.nix +++ b/modules/nixos/system/desktop/gnome.nix @@ -70,5 +70,11 @@ in gnomeExtensions.worksets gnomeExtensions.workspace-matrix ]; + + environment.persistence."/persist".directories = [ + "/etc/NetworkManager" + "/var/lib/AccountsService" + "/var/lib/NetworkManager" + ]; }; } diff --git a/nix/hosts.nix b/nix/hosts.nix index 0afea68..4e6b481 100644 --- a/nix/hosts.nix +++ b/nix/hosts.nix @@ -31,6 +31,7 @@ specialArgs = { inherit inputs; }; modules = [ + inputs.impermanence.nixosModules.impermanence { networking = { inherit domain; diff --git a/users/jordan/common/gpg.nix b/users/jordan/common/gpg.nix index 557307d..a234303 100644 --- a/users/jordan/common/gpg.nix +++ b/users/jordan/common/gpg.nix @@ -11,4 +11,8 @@ enable = true; enableSshSupport = true; }; + + home.persistence."/persist".directories = [ + ".gnupg" + ]; } diff --git a/users/jordan/common/neovim.nix b/users/jordan/common/neovim.nix index f7c52f0..9c5d4b0 100644 --- a/users/jordan/common/neovim.nix +++ b/users/jordan/common/neovim.nix @@ -130,4 +130,10 @@ }; home.sessionVariables.EDITOR = "nvim"; + + home.persistence."/state".directories = [ + ".local/share/nvim" + ".local/state/nvim" + ".cache/nvim" + ]; } diff --git a/users/jordan/common/optional/graphical/firefox.nix b/users/jordan/common/optional/graphical/firefox.nix index fce810b..d53ba09 100644 --- a/users/jordan/common/optional/graphical/firefox.nix +++ b/users/jordan/common/optional/graphical/firefox.nix @@ -207,4 +207,12 @@ }; }; }; + + home.persistence."/state".directories = [ + ".cache/mozilla" + ]; + + home.persistence."/persist".directories = [ + ".mozilla" + ]; } diff --git a/users/jordan/common/optional/graphical/thunderbird.nix b/users/jordan/common/optional/graphical/thunderbird.nix index 025da9f..5f65802 100644 --- a/users/jordan/common/optional/graphical/thunderbird.nix +++ b/users/jordan/common/optional/graphical/thunderbird.nix @@ -24,4 +24,12 @@ }; }; }; + + home.persistence."/state".directories = [ + ".cache/thunderbird" + ]; + + home.persistence."/persist".directories = [ + ".thunderbird" + ]; } diff --git a/users/jordan/common/pass.nix b/users/jordan/common/pass.nix index 6ddc9d7..ddfffcf 100644 --- a/users/jordan/common/pass.nix +++ b/users/jordan/common/pass.nix @@ -8,4 +8,8 @@ enable = true; package = pkgs.pass.withExtensions (exts: [ exts.pass-otp ]); }; + + home.persistence."/state".directories = [ + ".local/share/password-store" + ]; } diff --git a/users/jordan/common/shell.nix b/users/jordan/common/shell.nix index 0e68547..cd5cd12 100644 --- a/users/jordan/common/shell.nix +++ b/users/jordan/common/shell.nix @@ -176,6 +176,15 @@ in nix-index.enable = true; }; + home.persistence."/persist" = { + directories = [ + ".local/share/mcfly" + ]; + files = [ + ".zsh_history" + ]; + }; + home.packages = with pkgs; [ bat btop diff --git a/users/jordan/common/ssh.nix b/users/jordan/common/ssh.nix index d7d250c..b5bae23 100644 --- a/users/jordan/common/ssh.nix +++ b/users/jordan/common/ssh.nix @@ -9,4 +9,8 @@ enable = true; addKeysToAgent = "yes"; }; + + home.persistence."/state".files = [ + ".ssh/known_hosts" + ]; } diff --git a/users/jordan/default.nix b/users/jordan/default.nix index bb67882..0dd3683 100644 --- a/users/jordan/default.nix +++ b/users/jordan/default.nix @@ -15,6 +15,7 @@ in { age.secrets."passwords/users/jordan".file = "${inputs.secrets}/passwords/users/jordan.age"; + users.users.root.hashedPasswordFile = config.age.secrets."passwords/users/jordan".path; users.users.${name} = { description = "Jordan Holt"; extraGroups = [ @@ -42,6 +43,24 @@ in ./common/pass.nix ./common/shell.nix ./common/ssh.nix + { + home.persistence."/state" = { + directories = [ + "Downloads" + ".local/state/wireplumber" + ]; + }; + home.persistence."/persist" = { + directories = [ + "Desktop" + "Documents" + "Music" + "Pictures" + "projects" + "Videos" + ]; + }; + } ] ++ optional (builtins.pathExists hostFile) hostFile;