WIP: impermanence rework (gut 3rd-party lib)
This commit is contained in:
@@ -18,6 +18,16 @@
|
|||||||
sane.packages.enableConsolePkgs = true;
|
sane.packages.enableConsolePkgs = true;
|
||||||
sane.packages.enableSystemPkgs = true;
|
sane.packages.enableSystemPkgs = true;
|
||||||
|
|
||||||
|
sane.impermanence.dirs = [
|
||||||
|
"/var/log"
|
||||||
|
"/var/backup" # for e.g. postgres dumps
|
||||||
|
# TODO: move elsewhere
|
||||||
|
"/var/lib/alsa" # preserve output levels, default devices
|
||||||
|
"/var/lib/bluetooth" # preserve bluetooth handshakes
|
||||||
|
"/var/lib/colord" # preserve color calibrations (?)
|
||||||
|
"/var/lib/machines" # maybe not needed, but would be painful to add a VM and forget.
|
||||||
|
];
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
|
||||||
# time.timeZone = "America/Los_Angeles";
|
# time.timeZone = "America/Los_Angeles";
|
||||||
|
@@ -6,6 +6,8 @@
|
|||||||
# since that also depends on `users`.
|
# since that also depends on `users`.
|
||||||
# previously we manually `mount --bind` the host_keys here, but it's difficult to make that idempotent.
|
# previously we manually `mount --bind` the host_keys here, but it's difficult to make that idempotent.
|
||||||
# symlinking seems to work just as well, and is easier to make idempotent
|
# symlinking seems to work just as well, and is easier to make idempotent
|
||||||
|
#
|
||||||
|
# TODO: this is just a symlink: can we define this the same way we would `environment.etc.<blah> = <text>`?
|
||||||
system.activationScripts.persist-ssh-host-keys.text = ''
|
system.activationScripts.persist-ssh-host-keys.text = ''
|
||||||
mkdir -p /etc/ssh
|
mkdir -p /etc/ssh
|
||||||
ln -sf /nix/persist/etc/ssh/host_keys /etc/ssh/
|
ln -sf /nix/persist/etc/ssh/host_keys /etc/ssh/
|
||||||
|
@@ -84,7 +84,8 @@ in
|
|||||||
|
|
||||||
sane.impermanence.home-dirs = [
|
sane.impermanence.home-dirs = [
|
||||||
# cache is probably too big to fit on the tmpfs
|
# cache is probably too big to fit on the tmpfs
|
||||||
{ directory = ".cache"; encryptedClearOnBoot = true; }
|
# { directory = ".cache"; encryptedClearOnBoot = true; }
|
||||||
|
{ directory = ".cache/mozilla"; encryptedClearOnBoot = true; }
|
||||||
".cargo"
|
".cargo"
|
||||||
".rustup"
|
".rustup"
|
||||||
# TODO: move this to ~/private!
|
# TODO: move this to ~/private!
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
./home-manager
|
./home-manager
|
||||||
./packages.nix
|
./packages.nix
|
||||||
./image.nix
|
./image.nix
|
||||||
./impermanence.nix
|
./impermanence
|
||||||
./nixcache.nix
|
./nixcache.nix
|
||||||
./services
|
./services
|
||||||
];
|
];
|
||||||
|
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
lib.mkIf config.sane.home-manager.enable
|
lib.mkIf config.sane.home-manager.enable
|
||||||
{
|
{
|
||||||
sane.impermanence.home-dirs = [ ".cache/vim-swap" ];
|
# TODO(impermanence): re-enable!
|
||||||
|
# sane.impermanence.home-dirs = [ ".cache/vim-swap" ];
|
||||||
|
|
||||||
home-manager.users.colin.programs.neovim = {
|
home-manager.users.colin.programs.neovim = {
|
||||||
# neovim: https://github.com/neovim/neovim
|
# neovim: https://github.com/neovim/neovim
|
||||||
|
@@ -1,264 +0,0 @@
|
|||||||
# borrows from:
|
|
||||||
# https://xeiaso.net/blog/paranoid-nixos-2021-07-18
|
|
||||||
# https://elis.nu/blog/2020/05/nixos-tmpfs-as-root/
|
|
||||||
# https://github.com/nix-community/impermanence
|
|
||||||
{ config, impermanence, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
let
|
|
||||||
cfg = config.sane.impermanence;
|
|
||||||
# taken from sops-nix code: checks if any secrets are needed to create /etc/shadow
|
|
||||||
secretsForUsers = (lib.filterAttrs (_: v: v.neededForUsers) config.sops.secrets) != {};
|
|
||||||
persist-base = "/nix/persist";
|
|
||||||
encrypted-clear-on-boot-base = "/mnt/crypt/clearedonboot";
|
|
||||||
encrypted-clear-on-boot-store = "/nix/persist/crypt/clearedonboot";
|
|
||||||
encrypted-clear-on-boot-key = "/mnt/crypt/clearedonboot.key"; # TODO: move this to /tmp, but that requires tmp be mounted first?
|
|
||||||
home-dir-defaults = {
|
|
||||||
user = "colin";
|
|
||||||
group = "users";
|
|
||||||
mode = "0755";
|
|
||||||
relativeTo = "/home/colin/";
|
|
||||||
};
|
|
||||||
sys-dir-defaults = {
|
|
||||||
user = "root";
|
|
||||||
group = "root";
|
|
||||||
mode = "0755";
|
|
||||||
relativeTo = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
# turn a path into a name suitable for systemd
|
|
||||||
clean-name = path: let
|
|
||||||
dashes = builtins.replaceStrings ["/"] ["-"] path;
|
|
||||||
startswith = builtins.substring 0 1 dashes;
|
|
||||||
in if startswith == "-"
|
|
||||||
then substring 1 255 dashes
|
|
||||||
else dashes
|
|
||||||
;
|
|
||||||
|
|
||||||
dir-options = defaults: types.submodule {
|
|
||||||
options = {
|
|
||||||
encryptedClearOnBoot = mkOption {
|
|
||||||
default = false;
|
|
||||||
type = types.bool;
|
|
||||||
};
|
|
||||||
directory = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
};
|
|
||||||
user = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = defaults.user;
|
|
||||||
};
|
|
||||||
group = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = defaults.group;
|
|
||||||
};
|
|
||||||
mode = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = defaults.mode;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
ingest-dir-option = defaults: opt:
|
|
||||||
if isString opt then
|
|
||||||
ingest-dir-option defaults { directory = opt; }
|
|
||||||
else
|
|
||||||
rec {
|
|
||||||
encryptedClearOnBoot = opt.encryptedClearOnBoot or false;
|
|
||||||
srcDevice = if encryptedClearOnBoot
|
|
||||||
then encrypted-clear-on-boot-base
|
|
||||||
else persist-base
|
|
||||||
;
|
|
||||||
srcPath = "${srcDevice}${directory}";
|
|
||||||
directory = defaults.relativeTo + opt.directory;
|
|
||||||
user = opt.user or defaults.user;
|
|
||||||
group = opt.group or defaults.group;
|
|
||||||
mode = opt.mode or defaults.mode;
|
|
||||||
}
|
|
||||||
;
|
|
||||||
ingest-dir-options = defaults: opts: builtins.map (ingest-dir-option defaults) opts;
|
|
||||||
ingested-home-dirs = ingest-dir-options home-dir-defaults cfg.home-dirs;
|
|
||||||
ingested-sys-dirs = ingest-dir-options sys-dir-defaults cfg.dirs;
|
|
||||||
ingested-default-dirs = ingest-dir-options sys-dir-defaults [
|
|
||||||
"/var/log"
|
|
||||||
"/var/backup" # for e.g. postgres dumps
|
|
||||||
# TODO: move elsewhere
|
|
||||||
"/var/lib/alsa" # preserve output levels, default devices
|
|
||||||
"/var/lib/bluetooth" # preserve bluetooth handshakes
|
|
||||||
"/var/lib/colord" # preserve color calibrations (?)
|
|
||||||
"/var/lib/machines" # maybe not needed, but would be painful to add a VM and forget.
|
|
||||||
];
|
|
||||||
ingested-dirs = ingested-home-dirs ++ ingested-sys-dirs ++ ingested-default-dirs;
|
|
||||||
ingested-crypt-dirs = builtins.filter (o: o.encryptedClearOnBoot) ingested-dirs;
|
|
||||||
ingested-plain-dirs = builtins.filter (o: !o.encryptedClearOnBoot) ingested-dirs;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
sane.impermanence.enable = mkOption {
|
|
||||||
default = false;
|
|
||||||
type = types.bool;
|
|
||||||
};
|
|
||||||
sane.impermanence.root-on-tmpfs = mkOption {
|
|
||||||
default = false;
|
|
||||||
type = types.bool;
|
|
||||||
description = "define / to be a tmpfs. make sure to mount some other device to /nix";
|
|
||||||
};
|
|
||||||
sane.impermanence.encrypted-clear-on-boot = mkOption {
|
|
||||||
default = builtins.any (opt: opt.encryptedClearOnBoot) ingested-dirs;
|
|
||||||
type = types.bool;
|
|
||||||
description = "define ${encrypted-clear-on-boot-base} to be an encrypted filesystem which is unreadable after power-off";
|
|
||||||
};
|
|
||||||
sane.impermanence.home-dirs = mkOption {
|
|
||||||
default = [];
|
|
||||||
type = types.listOf (types.either types.str (dir-options home-dir-defaults));
|
|
||||||
};
|
|
||||||
sane.impermanence.dirs = mkOption {
|
|
||||||
default = [];
|
|
||||||
type = types.listOf (types.either types.str (dir-options sys-dir-defaults));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = mkIf cfg.enable (lib.mkMerge [
|
|
||||||
(lib.mkIf cfg.root-on-tmpfs {
|
|
||||||
fileSystems."/" = {
|
|
||||||
device = "none";
|
|
||||||
fsType = "tmpfs";
|
|
||||||
options = [
|
|
||||||
"mode=755"
|
|
||||||
"size=1G"
|
|
||||||
"defaults"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
|
|
||||||
(lib.mkIf cfg.encrypted-clear-on-boot {
|
|
||||||
# without this, we get `fusermount: fuse device not found, try 'modprobe fuse' first`.
|
|
||||||
# - that only happens after a activation-via-boot -- not activation-after-rebuild-switch.
|
|
||||||
# it seems likely that systemd loads `fuse` by default. see:
|
|
||||||
# - </etc/systemd/system/sysinit.target.wants/sys-fs-fuse-connections.mount>
|
|
||||||
# - triggers: /etc/systemd/system/modprobe@.service
|
|
||||||
# - calls `modprobe`
|
|
||||||
# note: even `boot.kernelModules = ...` isn't enough: that option creates /etc/modules-load.d/, which is ingested only by systemd.
|
|
||||||
# note: `boot.initrd.availableKernelModules` ALSO isn't enough: idk why.
|
|
||||||
# TODO: might not be necessary now we're using fileSystems and systemd
|
|
||||||
boot.initrd.kernelModules = [ "fuse" ];
|
|
||||||
|
|
||||||
system.activationScripts.prepareEncryptedClearedOnBoot =
|
|
||||||
let
|
|
||||||
script = pkgs.writeShellApplication {
|
|
||||||
name = "prepareEncryptedClearedOnBoot";
|
|
||||||
runtimeInputs = with pkgs; [ gocryptfs ];
|
|
||||||
text = ''
|
|
||||||
backing="$1"
|
|
||||||
passfile="$2"
|
|
||||||
if ! test -e "$passfile"
|
|
||||||
then
|
|
||||||
tmpdir=$(dirname "$passfile")
|
|
||||||
mkdir -p "$backing" "$tmpdir"
|
|
||||||
# if the key doesn't exist, it's probably not mounted => delete the backing dir
|
|
||||||
rm -rf "''${backing:?}"/*
|
|
||||||
# generate key. we can "safely" keep it around for the lifetime of this boot
|
|
||||||
dd if=/dev/random bs=128 count=1 | base64 --wrap=0 > "$passfile"
|
|
||||||
# initialize the crypt store
|
|
||||||
gocryptfs -quiet -passfile "$passfile" -init "$backing"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
text = ''${script}/bin/prepareEncryptedClearedOnBoot ${encrypted-clear-on-boot-store} ${encrypted-clear-on-boot-key}'';
|
|
||||||
};
|
|
||||||
|
|
||||||
fileSystems."${encrypted-clear-on-boot-base}" = {
|
|
||||||
device = encrypted-clear-on-boot-store;
|
|
||||||
fsType = "fuse.gocryptfs";
|
|
||||||
options = [
|
|
||||||
"nodev"
|
|
||||||
"nosuid"
|
|
||||||
"allow_other"
|
|
||||||
"passfile=${encrypted-clear-on-boot-key}"
|
|
||||||
"defaults"
|
|
||||||
];
|
|
||||||
noCheck = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
|
|
||||||
|
|
||||||
system.activationScripts.createPersistentStorageDirs.deps = [ "prepareEncryptedClearedOnBoot" ];
|
|
||||||
})
|
|
||||||
|
|
||||||
(
|
|
||||||
let cfgFor = opt:
|
|
||||||
let
|
|
||||||
parent-mount = "mnt-crypt-clearedonboot";
|
|
||||||
mount-service = clean-name opt.directory;
|
|
||||||
perms-service = "impermanence-perms-${mount-service}";
|
|
||||||
in {
|
|
||||||
fileSystems = {
|
|
||||||
name = opt.directory;
|
|
||||||
value = {
|
|
||||||
device = opt.srcPath;
|
|
||||||
options = [
|
|
||||||
"bind"
|
|
||||||
"x-systemd.requires=${parent-mount}.mount"
|
|
||||||
"x-systemd.after=${perms-service}.service"
|
|
||||||
# `wants` doesn't seem to make it to the service file here :-(
|
|
||||||
"x-systemd.wants=${perms-service}.service"
|
|
||||||
];
|
|
||||||
# fsType = "bind";
|
|
||||||
noCheck = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# create services which ensure the source directories exist and have correct ownership/perms before mounting
|
|
||||||
systemd.services = {
|
|
||||||
name = "${perms-service}";
|
|
||||||
value = {
|
|
||||||
description = "prepare permissions for ${opt.directory}";
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStart = pkgs.writeShellScript "${perms-service}" ''
|
|
||||||
mkdir -p ${opt.srcPath}
|
|
||||||
chmod ${opt.mode} ${opt.srcPath}
|
|
||||||
chown ${opt.user}:${opt.group} ${opt.srcPath}
|
|
||||||
'';
|
|
||||||
Type = "oneshot";
|
|
||||||
};
|
|
||||||
after = [ "${parent-mount}.mount" ];
|
|
||||||
wants = [ "${parent-mount}.mount" ];
|
|
||||||
wantedBy = [ "${mount-service}.mount" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
fileSystems = builtins.listToAttrs (builtins.map (opt: (cfgFor opt).fileSystems) ingested-crypt-dirs);
|
|
||||||
systemd.services = builtins.listToAttrs (builtins.map (opt: (cfgFor opt).systemd.services) ingested-crypt-dirs);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
({
|
|
||||||
# make sure logs from initrd can be persisted to disk -- i think?
|
|
||||||
sane.image.extraDirectories = [ "/nix/persist/var/log" ];
|
|
||||||
|
|
||||||
environment.persistence."${persist-base}".directories = builtins.map (opt: {
|
|
||||||
inherit (opt) directory user group mode;
|
|
||||||
}) ingested-plain-dirs;
|
|
||||||
|
|
||||||
# for each edge in a mount path, impermanence gives that target directory the same permissions
|
|
||||||
# as the matching folder in /nix/persist.
|
|
||||||
# /nix/persist is often created with poor permissions. so patch them to get the desired directory permissions.
|
|
||||||
system.activationScripts.fixImpermanencePerms = {
|
|
||||||
text = "chmod ${config.users.users.colin.homeMode} /nix/persist/home/colin";
|
|
||||||
deps = [ "users" ];
|
|
||||||
};
|
|
||||||
system.activationScripts.createPersistentStorageDirs.deps = [ "fixImpermanencePerms" ];
|
|
||||||
|
|
||||||
# secret decoding depends on /etc/ssh keys, which may be persisted
|
|
||||||
system.activationScripts.setupSecrets.deps = [ "persist-ssh-host-keys" ];
|
|
||||||
system.activationScripts.setupSecretsForUsers = lib.mkIf secretsForUsers {
|
|
||||||
deps = [ "persist-ssh-host-keys" ];
|
|
||||||
};
|
|
||||||
# populated by ssh.nix, which persists /etc/ssh/host_keys
|
|
||||||
system.activationScripts.persist-ssh-host-keys.text = lib.mkDefault "";
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
303
modules/impermanence/default.nix
Normal file
303
modules/impermanence/default.nix
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# borrows from:
|
||||||
|
# https://xeiaso.net/blog/paranoid-nixos-2021-07-18
|
||||||
|
# https://elis.nu/blog/2020/05/nixos-tmpfs-as-root/
|
||||||
|
# https://github.com/nix-community/impermanence
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.sane.impermanence;
|
||||||
|
# taken from sops-nix code: checks if any secrets are needed to create /etc/shadow
|
||||||
|
secrets-for-users = (lib.filterAttrs (_: v: v.neededForUsers) config.sops.secrets) != {};
|
||||||
|
getStore = { encryptedClearOnBoot, ... }: (
|
||||||
|
if encryptedClearOnBoot then {
|
||||||
|
device = "/mnt/impermanence/crypt/clearedonboot";
|
||||||
|
underlying = {
|
||||||
|
path = "/nix/persist/crypt/clearedonboot";
|
||||||
|
# TODO: consider moving this to /tmp, but that requires tmp be mounted first?
|
||||||
|
type = "gocryptfs";
|
||||||
|
key = "/mnt/impermanence/crypt/clearedonboot.key";
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
device = "/nix/persist";
|
||||||
|
# device = "/mnt/impermenanence/persist/plain";
|
||||||
|
# underlying = {
|
||||||
|
# path = "/nix/persist";
|
||||||
|
# type = "bind";
|
||||||
|
# };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
home-dir-defaults = {
|
||||||
|
user = "colin";
|
||||||
|
group = "users";
|
||||||
|
mode = "0755";
|
||||||
|
relativeTo = "/home/colin";
|
||||||
|
};
|
||||||
|
sys-dir-defaults = {
|
||||||
|
user = "root";
|
||||||
|
group = "root";
|
||||||
|
mode = "0755";
|
||||||
|
relativeTo = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
# turn a path into a name suitable for systemd
|
||||||
|
cleanName = path: let
|
||||||
|
dashes = builtins.replaceStrings ["/"] ["-"] path;
|
||||||
|
startswith = builtins.substring 0 1 dashes;
|
||||||
|
in if startswith == "-"
|
||||||
|
then substring 1 255 dashes
|
||||||
|
else dashes
|
||||||
|
;
|
||||||
|
|
||||||
|
# split the string path into a list of string components.
|
||||||
|
# root directory "/" becomes the empty list [].
|
||||||
|
# implicitly performs normalization so that:
|
||||||
|
# splitPath "a//b/" => ["a" "b"]
|
||||||
|
# splitPath "/a/b" => ["a" "b"]
|
||||||
|
splitPath = str: builtins.filter (seg: (builtins.isString seg) && seg != "" ) (builtins.split "/" str);
|
||||||
|
# return a string path, with leading slash but no trailing slash
|
||||||
|
joinPathAbs = comps: "/" + (builtins.concatStringsSep "/" comps);
|
||||||
|
concatPaths = paths: joinPathAbs (builtins.concatLists (builtins.map (p: splitPath p) paths));
|
||||||
|
# normalize the given path
|
||||||
|
normPath = str: joinPathAbs (splitPath str);
|
||||||
|
# return the parent directory. doesn't care about leading/trailing slashes.
|
||||||
|
parentDir = str: normPath (builtins.dirOf (normPath str));
|
||||||
|
|
||||||
|
dirOptions = defaults: types.submodule {
|
||||||
|
options = {
|
||||||
|
encryptedClearOnBoot = mkOption {
|
||||||
|
default = false;
|
||||||
|
type = types.bool;
|
||||||
|
};
|
||||||
|
directory = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaults.user;
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaults.group;
|
||||||
|
};
|
||||||
|
mode = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaults.mode;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
mkDirsOption = defaults: mkOption {
|
||||||
|
default = [];
|
||||||
|
type = types.listOf (types.coercedTo types.str (d: { directory = d; }) (dirOptions defaults));
|
||||||
|
# apply = map (d: if isString d then { directory = d; } else d);
|
||||||
|
};
|
||||||
|
|
||||||
|
# expand user options with more context
|
||||||
|
ingestDirOption = defaults: opt: {
|
||||||
|
inherit (opt) user group mode;
|
||||||
|
directory = concatPaths [ defaults.relativeTo opt.directory ];
|
||||||
|
# directory = throw (builtins.toString opt.directory);
|
||||||
|
# directory = builtins.traceVerbose opt.directory (concatPaths [ defaults.relativeTo opt.directory ]);
|
||||||
|
|
||||||
|
## helpful context
|
||||||
|
store = builtins.addErrorContext ''while ingestDirOption on ${opt.directory} with attrs ${builtins.concatStringsSep " " (attrNames opt)}''
|
||||||
|
(getStore opt);
|
||||||
|
};
|
||||||
|
|
||||||
|
ingestDirOptions = defaults: opts: builtins.map (ingestDirOption defaults) opts;
|
||||||
|
ingested-home-dirs = ingestDirOptions home-dir-defaults cfg.home-dirs;
|
||||||
|
ingested-sys-dirs = ingestDirOptions sys-dir-defaults cfg.dirs;
|
||||||
|
ingested-dirs = ingested-home-dirs ++ ingested-sys-dirs;
|
||||||
|
|
||||||
|
# include these anchor points as "virtual" nodes in below fs tree.
|
||||||
|
home-dir = {
|
||||||
|
inherit (home-dir-defaults) user group mode;
|
||||||
|
directory = normPath home-dir-defaults.relativeTo;
|
||||||
|
};
|
||||||
|
root-dir = {
|
||||||
|
inherit (sys-dir-defaults) user group mode;
|
||||||
|
directory = normPath sys-dir-defaults.relativeTo;
|
||||||
|
};
|
||||||
|
|
||||||
|
unexpanded-tree = builtins.listToAttrs (builtins.map
|
||||||
|
(dir: {
|
||||||
|
name = dir.directory;
|
||||||
|
value = dir;
|
||||||
|
})
|
||||||
|
(ingested-dirs ++ [ home-dir root-dir ])
|
||||||
|
);
|
||||||
|
|
||||||
|
# ensures the provided node and all parent nodes exist
|
||||||
|
ensureNode = tree: path: (
|
||||||
|
let
|
||||||
|
parent-path = parentDir path;
|
||||||
|
tree-with-parent = if parent-path == "/"
|
||||||
|
then tree
|
||||||
|
else ensureNode tree parent-path;
|
||||||
|
parent = tree-with-parent."${parent-path}";
|
||||||
|
# how to initialize this node if it doesn't exist explicitly.
|
||||||
|
default-node = parent // { directory = path; };
|
||||||
|
in
|
||||||
|
{ "${path}" = default-node; } // tree-with-parent
|
||||||
|
);
|
||||||
|
|
||||||
|
# finally, this tree has no orphan nodes
|
||||||
|
expanded-tree = foldl' ensureNode unexpanded-tree (builtins.attrNames unexpanded-tree);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
sane.impermanence.enable = mkOption {
|
||||||
|
default = false;
|
||||||
|
type = types.bool;
|
||||||
|
};
|
||||||
|
sane.impermanence.root-on-tmpfs = mkOption {
|
||||||
|
default = false;
|
||||||
|
type = types.bool;
|
||||||
|
description = "define / to be a tmpfs. make sure to mount some other device to /nix";
|
||||||
|
};
|
||||||
|
sane.impermanence.home-dirs = mkDirsOption home-dir-defaults;
|
||||||
|
sane.impermanence.dirs = mkDirsOption sys-dir-defaults;
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable (lib.mkMerge [
|
||||||
|
(lib.mkIf cfg.root-on-tmpfs {
|
||||||
|
fileSystems."/" = {
|
||||||
|
device = "none";
|
||||||
|
fsType = "tmpfs";
|
||||||
|
options = [
|
||||||
|
"mode=755"
|
||||||
|
"size=1G"
|
||||||
|
"defaults"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
{
|
||||||
|
# without this, we get `fusermount: fuse device not found, try 'modprobe fuse' first`.
|
||||||
|
# - that only happens after a activation-via-boot -- not activation-after-rebuild-switch.
|
||||||
|
# it seems likely that systemd loads `fuse` by default. see:
|
||||||
|
# - </etc/systemd/system/sysinit.target.wants/sys-fs-fuse-connections.mount>
|
||||||
|
# - triggers: /etc/systemd/system/modprobe@.service
|
||||||
|
# - calls `modprobe`
|
||||||
|
# note: even `boot.kernelModules = ...` isn't enough: that option creates /etc/modules-load.d/, which is ingested only by systemd.
|
||||||
|
# note: `boot.initrd.availableKernelModules` ALSO isn't enough: idk why.
|
||||||
|
# TODO: might not be necessary now we're using fileSystems and systemd
|
||||||
|
boot.initrd.kernelModules = [ "fuse" ];
|
||||||
|
|
||||||
|
# TODO: convert this to a systemd unit file?
|
||||||
|
system.activationScripts.prepareEncryptedClearedOnBoot =
|
||||||
|
let
|
||||||
|
script = pkgs.writeShellApplication {
|
||||||
|
name = "prepareEncryptedClearedOnBoot";
|
||||||
|
runtimeInputs = with pkgs; [ gocryptfs ];
|
||||||
|
text = ''
|
||||||
|
backing="$1"
|
||||||
|
passfile="$2"
|
||||||
|
if ! test -e "$passfile"
|
||||||
|
then
|
||||||
|
tmpdir=$(dirname "$passfile")
|
||||||
|
mkdir -p "$backing" "$tmpdir"
|
||||||
|
# if the key doesn't exist, it's probably not mounted => delete the backing dir
|
||||||
|
rm -rf "''${backing:?}"/*
|
||||||
|
# generate key. we can "safely" keep it around for the lifetime of this boot
|
||||||
|
dd if=/dev/random bs=128 count=1 | base64 --wrap=0 > "$passfile"
|
||||||
|
# initialize the crypt store
|
||||||
|
gocryptfs -quiet -passfile "$passfile" -init "$backing"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
store = getStore { encryptedClearOnBoot = true; };
|
||||||
|
in {
|
||||||
|
text = ''${script}/bin/prepareEncryptedClearedOnBoot ${store.underlying.path} ${store.underlying.key}'';
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems = let
|
||||||
|
store = getStore { encryptedClearOnBoot = true; };
|
||||||
|
in {
|
||||||
|
"${store.device}" = {
|
||||||
|
device = store.underlying.path;
|
||||||
|
fsType = "fuse.gocryptfs";
|
||||||
|
options = [
|
||||||
|
"nodev"
|
||||||
|
"nosuid"
|
||||||
|
"allow_other"
|
||||||
|
"passfile=${store.underlying.key}"
|
||||||
|
"defaults"
|
||||||
|
];
|
||||||
|
noCheck = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
let cfgFor = opt:
|
||||||
|
let
|
||||||
|
# systemd creates <path>.mount services for every fileSystems entry.
|
||||||
|
# <path> gets escaped as part of that: this code tries to guess that escaped name here.
|
||||||
|
backing-mount = cleanName opt.store.device;
|
||||||
|
mount-service = cleanName opt.directory;
|
||||||
|
perms-service = "impermanence-perms-${mount-service}";
|
||||||
|
parent-mount-service = cleanName (parentDir opt.directory);
|
||||||
|
parent-perms-service = "impermanence-perms-${parent-mount-service}";
|
||||||
|
is-mount = opt ? store;
|
||||||
|
in {
|
||||||
|
fileSystems."${opt.directory}" = lib.mkIf is-mount {
|
||||||
|
device = concatPaths [ opt.store.device opt.directory ];
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"x-systemd.requires=${backing-mount}.mount" # this should be implicit
|
||||||
|
"x-systemd.after=${perms-service}.service"
|
||||||
|
# `wants` doesn't seem to make it to the service file here :-(
|
||||||
|
"x-systemd.wants=${perms-service}.service"
|
||||||
|
];
|
||||||
|
# fsType = "bind";
|
||||||
|
noCheck = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# create services which ensure the source directories exist and have correct ownership/perms before mounting
|
||||||
|
systemd.services."${perms-service}" = let
|
||||||
|
perms-script = pkgs.writeShellScript "impermanence-prepare-perms" ''
|
||||||
|
path="$1"
|
||||||
|
user="$2"
|
||||||
|
group="$3"
|
||||||
|
mode="$4"
|
||||||
|
mkdir "$path" || test -d "$path"
|
||||||
|
chmod "$mode" "$src"
|
||||||
|
chown "$user:$group" "$src"
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
description = "prepare permissions for ${opt.directory}";
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = ''${perms-script} ${opt.directory} ${opt.user} ${opt.group} ${opt.mode}'';
|
||||||
|
Type = "oneshot";
|
||||||
|
};
|
||||||
|
wantedBy = lib.mkIf is-mount [ "${mount-service}.mount" ];
|
||||||
|
after = lib.mkIf (opt.directory != "/") [ "${parent-perms-service}.service" ];
|
||||||
|
wants = lib.mkIf (opt.directory != "/") [ "${parent-perms-service}.service" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
cfgs = builtins.map cfgFor (builtins.attrValues expanded-tree);
|
||||||
|
# cfgs = builtins.map cfgFor ingested-dirs;
|
||||||
|
# cfgs = [ (cfgFor (ingestDirOption home-dir-defaults ".cache")) ];
|
||||||
|
# myMerge = items: builtins.foldl' (acc: new: acc // new) {} items;
|
||||||
|
in {
|
||||||
|
# fileSystems = myMerge (catAttrs "fileSystems" cfgs);
|
||||||
|
fileSystems = lib.mkMerge (builtins.catAttrs "fileSystems" cfgs);
|
||||||
|
systemd = lib.mkMerge (catAttrs "systemd" cfgs);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
({
|
||||||
|
# secret decoding depends on /etc/ssh keys, which may be persisted
|
||||||
|
system.activationScripts.setupSecrets.deps = [ "persist-ssh-host-keys" ];
|
||||||
|
system.activationScripts.setupSecretsForUsers = lib.mkIf secrets-for-users {
|
||||||
|
deps = [ "persist-ssh-host-keys" ];
|
||||||
|
};
|
||||||
|
# populated by ssh.nix, which persists /etc/ssh/host_keys
|
||||||
|
system.activationScripts.persist-ssh-host-keys.text = lib.mkDefault "";
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@@ -115,12 +115,14 @@ let
|
|||||||
lollypop
|
lollypop
|
||||||
mesa-demos
|
mesa-demos
|
||||||
|
|
||||||
{ pkg = mpv; dir = [ ".config/mpv/watch_later" ]; }
|
# TODO(impermanence): re-enable!
|
||||||
|
# { pkg = mpv; dir = [ ".config/mpv/watch_later" ]; }
|
||||||
|
|
||||||
networkmanagerapplet
|
networkmanagerapplet
|
||||||
|
|
||||||
# not strictly necessary, but allows caching articles; offline use, etc.
|
# not strictly necessary, but allows caching articles; offline use, etc.
|
||||||
{ pkg = newsflash; dir = [ ".local/share/news-flash" ]; }
|
# TODO(impermanence): re-enable!
|
||||||
|
# { pkg = newsflash; dir = [ ".local/share/news-flash" ]; }
|
||||||
|
|
||||||
{ pkg = nheko; private = [
|
{ pkg = nheko; private = [
|
||||||
".config/nheko" # config file (including client token)
|
".config/nheko" # config file (including client token)
|
||||||
@@ -143,7 +145,8 @@ let
|
|||||||
# config (e.g. server connection details) is persisted in ~/.config/sublime-music/config.json
|
# config (e.g. server connection details) is persisted in ~/.config/sublime-music/config.json
|
||||||
# possible to pass config as a CLI arg (sublime-music -c config.json)
|
# possible to pass config as a CLI arg (sublime-music -c config.json)
|
||||||
# { pkg = sublime-music; dir = [ ".local/share/sublime-music" ]; }
|
# { pkg = sublime-music; dir = [ ".local/share/sublime-music" ]; }
|
||||||
{ pkg = sublime-music-mobile; dir = [ ".local/share/sublime-music" ]; }
|
# TODO(impermanence): re-enable!
|
||||||
|
# { pkg = sublime-music-mobile; dir = [ ".local/share/sublime-music" ]; }
|
||||||
tdesktop # broken on phosh
|
tdesktop # broken on phosh
|
||||||
|
|
||||||
{ pkg = tokodon; private = [ ".cache/KDE/tokodon" ]; }
|
{ pkg = tokodon; private = [ ".cache/KDE/tokodon" ]; }
|
||||||
|
Reference in New Issue
Block a user