impermanence: large refactor, and experimental bind mounting of things from ~/private

This commit is contained in:
colin 2023-01-03 07:04:49 +00:00
parent bace7403e7
commit 327e6b536f
30 changed files with 243 additions and 110 deletions

View File

@ -18,7 +18,7 @@
sane.packages.enableConsolePkgs = true;
sane.packages.enableSystemPkgs = true;
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
"/var/log"
"/var/backup" # for e.g. postgres dumps
# TODO: move elsewhere

View File

@ -71,17 +71,19 @@ in
security.pam.mount.enable = true;
sane.impermanence.home-dirs = [
# cache is probably too big to fit on the tmpfs
# { directory = ".cache"; store = "crypt-clearedonboot"; }
{ directory = ".cache/mozilla"; store = "crypt-clearedonboot"; }
sane.impermanence.dirs.home.plaintext = [
".cargo"
".rustup"
# TODO: move this to ~/private!
".local/share/keyrings"
];
sane.impermanence.dirs.home.cryptClearOnBoot = [
# cache is probably too big to fit on the tmpfs
# ".cache"
".cache/mozilla"
];
sane.impermanence.dirs = mkIf cfg.guest.enable [
sane.impermanence.dirs.sys.plaintext = mkIf cfg.guest.enable [
{ user = "guest"; group = "users"; directory = "/home/guest"; }
];
users.users.guest = mkIf cfg.guest.enable {

View File

@ -52,7 +52,7 @@
remotePlay.openFirewall = true; # Open ports in the firewall for Steam Remote Play
dedicatedServer.openFirewall = true; # Open ports in the firewall for Source Dedicated Server
};
sane.impermanence.home-dirs = [
sane.impermanence.dirs.home.plaintext = [
".steam"
".local/share/Steam"
];

View File

@ -36,11 +36,12 @@
];
};
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: this is overly broad; only need media and share directories to be persisted
{ user = "colin"; group = "users"; directory = "/var/lib/uninsane"; }
];
# direct these media directories to external storage
# TODO: convert to sane.fs
environment.persistence."/nix/persist/ext/persist" = {
directories = [
({

View File

@ -19,7 +19,7 @@
# XXX: avatar support works in MUCs but not DMs
# lib.mkIf false
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "ejabberd"; group = "ejabberd"; directory = "/var/lib/ejabberd"; }
];
networking.firewall.allowedTCPPorts = [

View File

@ -16,7 +16,7 @@
owner = config.users.users.freshrss.name;
mode = "400";
};
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "freshrss"; group = "freshrss"; directory = "/var/lib/freshrss"; }
];

View File

@ -1,7 +1,7 @@
{ config, pkgs, lib, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "git"; group = "gitea"; directory = "/var/lib/gitea"; }
];

View File

@ -10,7 +10,7 @@
lib.mkIf false # i don't actively use ipfs anymore
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "261"; group = "261"; directory = "/var/lib/ipfs"; }
];

View File

@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? we only need this to save Indexer creds ==> migrate to config?
{ user = "root"; group = "root"; directory = "/var/lib/jackett"; }
];

View File

@ -7,7 +7,7 @@ lib.mkIf false
networking.firewall.allowedUDPPorts = [
1900 7359 # DLNA: https://jellyfin.org/docs/general/networking/index.html
];
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "jellyfin"; group = "jellyfin"; directory = "/var/lib/jellyfin"; }
];

View File

@ -8,7 +8,7 @@
# ./irc.nix
];
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "matrix-synapse"; group = "matrix-synapse"; directory = "/var/lib/matrix-synapse"; }
];
services.matrix-synapse.enable = true;

View File

@ -1,6 +1,6 @@
{ lib, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "matrix-synapse"; group = "matrix-synapse"; directory = "/var/lib/mx-puppet-discord"; }
];

View File

@ -1,7 +1,7 @@
{ config, lib, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode?
# user and group are both "matrix-appservice-irc"
{ user = "993"; group = "992"; directory = "/var/lib/matrix-appservice-irc"; }

View File

@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "navidrome"; group = "navidrome"; directory = "/var/lib/private/navidrome"; }
];
services.navidrome.enable = true;

View File

@ -122,7 +122,7 @@ in
users.users.acme.uid = config.sane.allocations.acme-uid;
users.groups.acme.gid = config.sane.allocations.acme-gid;
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode?
{ user = "acme"; group = "acme"; directory = "/var/lib/acme"; }
{ user = "colin"; group = "users"; directory = "/var/www/sites"; }

View File

@ -6,7 +6,7 @@
{ config, pkgs, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "pleroma"; group = "pleroma"; directory = "/var/lib/pleroma"; }
];

View File

@ -16,7 +16,7 @@ let
};
in
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "opendkim"; group = "opendkim"; directory = "/var/lib/opendkim"; }
{ user = "root"; group = "root"; directory = "/var/lib/postfix"; }

View File

@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode?
{ user = "postgres"; group = "postgres"; directory = "/var/lib/postgresql"; }
];

View File

@ -9,7 +9,7 @@
# nixnet runs ejabberd, so revisiting that.
lib.mkIf false
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "prosody"; group = "prosody"; directory = "/var/lib/prosody"; }
];
networking.firewall.allowedTCPPorts = [

View File

@ -1,7 +1,7 @@
{ pkgs, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? we need this specifically for the stats tracking in .config/
{ user = "transmission"; group = "transmission"; directory = "/var/lib/transmission"; }
];

View File

@ -91,6 +91,11 @@ let
description = "fs path to bind-mount from";
default = null;
};
extraOptions = mkOption {
type = types.listOf types.str;
description = "extra fstab options for this mount";
default = [];
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which mounts this path";
@ -126,12 +131,14 @@ let
device = opt.mount.bind;
options = [
"bind"
# x-systemd options documented here:
# - <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
# we can't mount this until after the underlying path is prepared.
# if the underlying path disappears, this mount will be stopped.
"x-systemd.requires=${underlying.dir.unit}"
# the mount depends on its target directory being prepared
"x-systemd.requires=${opt.dir.unit}"
];
] ++ opt.mount.extraOptions;
noCheck = true;
};
});

View File

@ -11,8 +11,6 @@ let
cfg = config.sane.home-manager;
# extract `pkg` from `sane.packages.enabledUserPkgs`
pkg-list = pkgspec: builtins.map (e: e.pkg) pkgspec;
# extract `private` from `sane.packages.enabledUserPkgs`
private-list = pkgspec: builtins.concatLists (builtins.map (e: e.private) pkgspec);
feeds = import ./feeds.nix { inherit lib; };
in
{
@ -50,9 +48,10 @@ in
};
config = lib.mkIf cfg.enable {
sane.impermanence.home-dirs = [
sane.impermanence.dirs.home.plaintext = [
"archive"
"dev"
# TODO: records should be private
"records"
"ref"
"tmp"
@ -90,15 +89,7 @@ in
};
home.file = let
privates = builtins.listToAttrs (
builtins.map (path: {
name = path;
value = { source = config.lib.file.mkOutOfStoreSymlink "/home/colin/private/${path}"; };
})
(private-list sysconfig.sane.packages.enabledUserPkgs)
);
in {
home.file = {
# convenience
"knowledge".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/private/knowledge";
"nixos".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/dev/nixos";
@ -108,7 +99,7 @@ in
# used by password managers, e.g. unix `pass`
".password-store".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/knowledge/secrets/accounts";
} // privates;
};
# XDG defines things like ~/Desktop, ~/Downloads, etc.
# these clutter the home, so i mostly don't use them.

View File

@ -2,7 +2,8 @@
lib.mkIf config.sane.home-manager.enable
{
sane.impermanence.home-dirs = [ ".cache/vim-swap" ];
# private because there could be sensitive things in the swap
sane.impermanence.dirs.home.private = [ ".cache/vim-swap" ];
home-manager.users.colin.programs.neovim = {
# neovim: https://github.com/neovim/neovim

View File

@ -2,7 +2,7 @@
lib.mkIf config.sane.home-manager.enable
{
sane.impermanence.home-dirs = [
sane.impermanence.dirs.home.plaintext = [
# we don't need to full zsh dir -- just the history file --
# but zsh will sometimes backup the history file and we get fewer errors if we do proper mounts instead of symlinks.
# TODO: should be private?

View File

@ -8,9 +8,20 @@ with lib;
let
cfg = config.sane.impermanence;
stores = {
"crypt-clearedonboot" = "/mnt/impermanence/crypt/clearedonboot";
"persist" = "/nix/persist";
storeType = types.submodule {
options = {
mountpt = mkOption {
type = types.str;
};
prefix = mkOption {
type = types.str;
default = "/";
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [];
};
};
};
# split the string path into a list of string components.
@ -22,17 +33,18 @@ let
# 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));
# return the path from `from` to `to`, but generally in absolute form.
# e.g. `pathFrom "/home/colin" "/home/colin/foo/bar"` -> "/foo/bar"
pathFrom = from: to:
assert lib.hasPrefix from to;
lib.removePrefix from to;
# options for a single mountpoint / persistence
dirEntry = types.submodule {
dirEntryOptions = {
options = {
directory = mkOption {
type = types.str;
};
store = mkOption {
default = "persist";
type = types.enum (builtins.attrNames stores);
};
user = mkOption {
type = types.nullOr types.str;
default = null;
@ -47,22 +59,88 @@ let
};
};
};
contextualizedDir = types.submodule dirEntryOptions;
# allow "bar/baz" as shorthand for { directory = "bar/baz"; }
coercedDirEntry = types.coercedTo types.str (d: { directory = d; }) dirEntry;
contextualizedDirOrShorthand = types.coercedTo
types.str
(d: { directory = d; })
contextualizedDir;
# expand user options with more context
ingestDirEntry = relativeTo: opt: {
inherit (opt) user group mode;
directory = concatPaths [ relativeTo opt.directory ];
# entry whose `directory` is always an absolute fs path
# and has an associated `store`
contextFreeDir = types.submodule [
dirEntryOptions
{
options = {
store = mkOption {
type = storeType;
};
};
}
];
# resolve the store
store = stores."${opt.store}";
};
ingestDirEntries = relativeTo: opts: builtins.map (ingestDirEntry relativeTo) opts;
ingested-home-dirs = ingestDirEntries "/home/colin" cfg.home-dirs;
ingested-sys-dirs = ingestDirEntries "/" cfg.dirs;
ingested-dirs = ingested-home-dirs ++ ingested-sys-dirs;
dirsModule = types.submodule ({ config, ... }: {
options = {
home = mkOption {
description = "directories to persist to disk, relative to a user's home ~";
default = {};
type = types.submodule {
options = {
plaintext = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = "directories to persist in cleartext";
};
private = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = "directories to store encrypted to the user's login password and auto-decrypt on login";
};
cryptClearOnBoot = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = ''
directories to store encrypted to an auto-generated in-memory key and
wiped on boot. the main use is for sensitive cache dirs too large to fit in memory.
'';
};
};
};
};
sys = mkOption {
description = "directories to persist to disk, relative to the fs root /";
default = {};
type = types.submodule {
options = {
plaintext = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = "list of directories (and optional config) to persist to disk in plaintext, relative to the fs root /";
};
};
};
};
all = mkOption {
type = types.listOf contextFreeDir;
description = "all directories known to the config. auto-computed: users should not set this directly.";
};
};
config = let
mapDirs = relativeTo: store: dirs: (map
(d: {
inherit (d) user group mode;
directory = concatPaths [ relativeTo d.directory ];
store = cfg.stores."${store}";
})
dirs
);
in {
all = (mapDirs "/home/colin" "plaintext" config.home.plaintext)
++ (mapDirs "/home/colin" "private" config.home.private)
++ (mapDirs "/home/colin" "cryptClearOnBoot" config.home.cryptClearOnBoot)
++ (mapDirs "/" "plaintext" config.sys.plaintext);
};
});
in
{
options = {
@ -73,23 +151,21 @@ in
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 = mkOption {
default = [];
type = types.listOf coercedDirEntry;
description = "list of directories (and optional config) to persist to disk, relative to the user's home ~";
description = "define / fs root to be a tmpfs. make sure to mount some other device to /nix";
};
sane.impermanence.dirs = mkOption {
default = [];
type = types.listOf coercedDirEntry;
description = "list of directories (and optional config) to persist to disk, relative to the fs root /";
type = dirsModule;
default = {};
};
sane.impermanence.stores = mkOption {
type = types.attrsOf storeType;
default = {};
};
};
imports = [
./crypt.nix
./root-on-tmpfs.nix
./stores
];
config = mkIf cfg.enable (lib.mkMerge [
@ -100,6 +176,7 @@ in
group = config.users.users.colin.group;
mode = config.users.users.colin.homeMode;
};
# N.B.: we have a similar problem with all mounts:
# <crypt>/.cache/mozilla won't inherit <plain>/.cache perms.
# this is less of a problem though, since we don't really support overlapping mounts like that in the first place.
@ -113,7 +190,9 @@ in
(
let cfgFor = opt:
let
backing-path = concatPaths [ opt.store opt.directory ];
store = opt.store;
store-rel-path = pathFrom store.prefix opt.directory;
backing-path = concatPaths [ store.mountpt store-rel-path ];
# pass through the perm/mode overrides
dir-acl = {
@ -127,13 +206,14 @@ in
# inherit perms & make sure we don't mount until after the mount point is setup correctly.
dir.acl = dir-acl;
mount.bind = backing-path;
mount.extraOptions = store.extraOptions;
};
sane.fs."${backing-path}" = {
# ensure the backing path has same perms as the mount point
dir.acl = config.sane.fs."${opt.directory}".dir.acl;
};
};
cfgs = builtins.map cfgFor ingested-dirs;
cfgs = builtins.map cfgFor cfg.dirs.all;
in {
sane.fs = lib.mkMerge (catAttrs "fs" (catAttrs "sane" cfgs));
}

View File

@ -29,9 +29,13 @@ let
fi
'';
};
private-mount-unit = ''${utils.escapeSystemdPath "/home/colin/private"}.mount'';
in lib.mkIf config.sane.impermanence.enable
in
lib.mkIf config.sane.impermanence.enable
{
sane.impermanence.stores."cryptClearOnBoot" = {
mountpt = "/mnt/impermanence/crypt/clearedonboot";
};
systemd.services."prepareEncryptedClearedOnBoot" = rec {
description = "prepare keys for ${store.device}";
serviceConfig.ExecStart = ''
@ -80,39 +84,6 @@ in lib.mkIf config.sane.impermanence.enable
dir.reverseDepends = [ store.mount-unit ];
};
fileSystems."/home/colin/private" = {
device = "/nix/persist/home/colin/private";
fsType = "fuse.gocryptfs";
options = [
"noauto" # don't try to mount, until the user logs in!
"allow_other" # root ends up being the user that mounts this, so need to make it visible to `colin`.
"nodev"
"nosuid"
"quiet"
"defaults"
];
noCheck = true;
};
sane.fs."/home/colin/private" = {
dir.reverseDepends = [
# mounting relies on the mountpoint first being created.
private-mount-unit
# ensure the directory is created during boot, and before user logs in.
"multi-user.target"
];
# HACK: this fs entry is provided by the mount unit.
unit = private-mount-unit;
};
sane.fs."/nix/persist/home/colin/private" = {
dir.reverseDepends = [
# the mount unit relies on the source having first been created.
# (it also relies on the cryptfs having been seeded -- which we can't verify here).
private-mount-unit
# ensure the directory is created during boot, and before user logs in.
"multi-user.target"
];
};
# TODO: could add this *specifically* to the .mount file for the encrypted fs?
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
}

View File

@ -0,0 +1,17 @@
{ config, lib, ... }:
let
cfg = config.sane.impermanence;
in
{
imports = [
./crypt.nix
./private.nix
];
config = lib.mkIf cfg.enable {
sane.impermanence.stores."plaintext" = {
mountpt = "/nix/persist";
};
};
}

View File

@ -0,0 +1,61 @@
{ config, lib, pkgs, utils, ... }:
let
private-mount-unit = ''${utils.escapeSystemdPath "/home/colin/private"}.mount'';
in lib.mkIf config.sane.impermanence.enable
{
sane.impermanence.stores."private" = {
mountpt = "/home/colin/private";
# files stored under here *must* have the /home/colin prefix.
# internally, this prefix is removed so that e.g.
# /home/colin/foo/bar when stored in `private` is visible at
# /home/colin/private/foo/bar
prefix = "/home/colin";
# fstab options inherited by all members of the store
extraOptions = let
private-unit = config.sane.fs."/home/colin/private".unit;
in [
"noauto"
# auto mount when ~/private is mounted
"x-systemd.wanted-by=${private-unit}"
];
};
fileSystems."/home/colin/private" = {
device = "/nix/persist/home/colin/private";
fsType = "fuse.gocryptfs";
options = [
"noauto" # don't try to mount, until the user logs in!
"allow_other" # root ends up being the user that mounts this, so need to make it visible to `colin`.
"nodev"
"nosuid"
"quiet"
"defaults"
];
noCheck = true;
};
sane.fs."/home/colin/private" = {
dir.reverseDepends = [
# mounting relies on the mountpoint first being created.
private-mount-unit
# ensure the directory is created during boot, and before user logs in.
"multi-user.target"
];
# HACK: this fs entry is provided by the mount unit.
unit = private-mount-unit;
};
sane.fs."/nix/persist/home/colin/private" = {
dir.reverseDepends = [
# the mount unit relies on the source having first been created.
# (it also relies on the cryptfs having been seeded -- which we can't verify here).
private-mount-unit
# ensure the directory is created during boot, and before user logs in.
"multi-user.target"
];
};
# TODO: could add this *specifically* to the .mount file for the encrypted fs?
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
}

View File

@ -307,7 +307,8 @@ in
config = {
environment.systemPackages = mkIf cfg.enableSystemPkgs systemPkgs;
sane.impermanence.home-dirs = concatLists (map (p: p.dir) cfg.enabledUserPkgs);
sane.impermanence.dirs.home.plaintext = concatLists (map (p: p.dir) cfg.enabledUserPkgs);
sane.impermanence.dirs.home.private = concatLists (map (p: p.private) cfg.enabledUserPkgs);
# XXX: this might not be necessary. try removing this and cacert.unbundled?
environment.etc."ssl/certs".source = mkIf cfg.enableSystemPkgs "${pkgs.cacert.unbundled}/etc/ssl/certs/*";
};

View File

@ -15,7 +15,8 @@ in
config = mkIf cfg.enable {
# we need this mostly because of the size of duplicity's cache
sane.impermanence.dirs = [ "/var/lib/duplicity" ];
# TODO: move to cryptClearOnBoot and update perms
sane.impermanence.dirs.sys.plaintext = [ "/var/lib/duplicity" ];
services.duplicity.enable = true;
services.duplicity.targetUrl = "$DUPLICITY_URL";