Compare commits

...

41 Commits

Author SHA1 Message Date
1a0f05bfd6 flake update: nixpkgs 2022-12-31 -> 2023-01-04
```
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/8ba56d7c0d7490680f2d51ba46a141eca7c46afa' (2022-12-31)
  → 'github:NixOS/nixpkgs/9813adc7f7c0edd738c6bdd8431439688bb0cb3d' (2023-01-04)
• Updated input 'nixpkgs-stable':
    'github:NixOS/nixpkgs/6a0d2701705c3cf6f42c15aa92b7885f1f8a477f' (2022-12-30)
  → 'github:NixOS/nixpkgs/e9ade2c8240e00a4784fac282a502efff2786bdc' (2023-01-04)
```
2023-01-06 08:57:39 +00:00
c18dd9636d fs: symlinking: fix recursive links (don't follow links when creating new ones) 2023-01-06 05:32:31 +00:00
0977721af5 moby: fix to preserve browser cache across boots 2023-01-04 13:27:20 +00:00
122d3cd7e4 impermanence: fix home perms in ~/private dir 2023-01-04 13:24:23 +00:00
cd5f8054c0 fs: rename "mountpt" -> "origin" to reflect that it doesnt have to be a device 2023-01-04 12:19:32 +00:00
3db388b105 servo: relocate ext device to /mnt/impermanence/ext and fixup deps 2023-01-04 12:12:30 +00:00
2ba6116f10 fs/impermanence: more precisely control unit dependencies/ordering 2023-01-04 11:22:26 +00:00
592d17b725 impermanence: crypt: simplify setup (experimental) 2023-01-04 09:28:59 +00:00
4d9c15f9b8 fs: fix file mode even if not newly created 2023-01-04 08:12:53 +00:00
abced7dd0d navidrome: don't try to chown to an invalid user 2023-01-04 08:00:04 +00:00
5c42365912 sane-stop-all-servo: add missing services 2023-01-04 07:49:21 +00:00
247ad326b2 freshrss: be conservative and use explicit octal mode bits 2023-01-04 07:14:54 +00:00
170008f345 home.files symlinks: port to sane.fs 2023-01-04 07:14:38 +00:00
2c48e61854 fs: fix mode of deployed files; simplify 2023-01-04 07:14:01 +00:00
f89f756489 fs: create symlinks at multi-user target by default 2023-01-04 06:32:53 +00:00
c0da19951b fs: fix symlink ownership/perms 2023-01-04 06:28:44 +00:00
5fb67306e4 fs: rework for dir to not be mandatory 2023-01-04 06:17:34 +00:00
5533b586d7 fs: lift depends out of dir, to toplevel 2023-01-04 04:32:20 +00:00
68c2eb7363 impermanence: clean up the deps for the crypt store 2023-01-04 04:22:17 +00:00
fd79026366 fs: fix loose mount dependency 2023-01-04 04:06:19 +00:00
a76471cb1f fs: simplify the mount configuration 2023-01-04 03:57:24 +00:00
c94b8299a6 fs: add experimental support for symlink entries 2023-01-04 02:51:07 +00:00
175bc0709f remove external impermanence library 2023-01-04 02:18:03 +00:00
7b02477486 servo: define /etc/persist via sane impermanence module 2023-01-04 02:15:43 +00:00
d7c8638fea impermanence: add a storeDescription field per store 2023-01-04 01:54:13 +00:00
9d7d1acc80 sane.impermanence.dirs.home.<store> is now auto-generated and options can vary across deployments 2023-01-04 01:45:05 +00:00
787857d27f firefox: update plugin hashes 2023-01-04 01:39:42 +00:00
9c248a8a31 impermanence: cleanup the dirs submodule 2023-01-04 01:34:19 +00:00
829680fb00 impermanence: simplify dir-acl handling by using a helper 2023-01-04 01:19:22 +00:00
a9ee26388c guest account: make home-dir writable by other users 2023-01-04 01:09:23 +00:00
2960b895b6 lib: lift acl type into sane-lib/types 2023-01-04 00:59:52 +00:00
933063115b moby: fix home-dirs for newer impermanence module 2023-01-04 00:47:48 +00:00
afe684ca2c modules: impermanence: use sane-lib.path 2023-01-03 14:55:27 +00:00
93f1411522 fs: split helpers out into sane-lib module 2023-01-03 14:20:02 +00:00
01e44c1f7f flake.nix: remove unused specialArgs 2023-01-03 14:18:57 +00:00
618e9bd2fa gocryptfs: place package in system.fsPackages instead of environment.systemPackages (it propagates anyway) 2023-01-03 12:00:49 +00:00
fbc39d0584 modules: fs: move to subdir 2023-01-03 08:27:28 +00:00
2d7b3750cd impermanence: split the /home/colin perms fix into more appropriate places 2023-01-03 08:25:43 +00:00
e6ccd2e4f7 impermanence: split plaintext store to own file (this will bypass some recursion in the next patch) 2023-01-03 08:02:03 +00:00
d4bf491e9c impermanence: improve docs 2023-01-03 07:45:19 +00:00
5a2bbcce3b move plaintext home-dirs out of home-manager module into users module 2023-01-03 07:35:42 +00:00
21 changed files with 691 additions and 536 deletions

28
flake.lock generated
View File

@@ -36,21 +36,6 @@
"type": "github"
}
},
"impermanence": {
"locked": {
"lastModified": 1668668915,
"narHash": "sha256-QjY4ZZbs9shwO4LaLpvlU2bO9J1juYhO9NtV3nrbnYQ=",
"owner": "nix-community",
"repo": "impermanence",
"rev": "5df9108b346f8a42021bf99e50de89c9caa251c3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "impermanence",
"type": "github"
}
},
"mobile-nixos": {
"flake": false,
"locked": {
@@ -69,11 +54,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1672525397,
"narHash": "sha256-WASDnyxHKWVrEe0dIzkpH+jzKlCKAk0husv0f/9pyxg=",
"lastModified": 1672791794,
"narHash": "sha256-mqGPpGmwap0Wfsf3o2b6qHJW1w2kk/I6cGCGIU+3t6o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8ba56d7c0d7490680f2d51ba46a141eca7c46afa",
"rev": "9813adc7f7c0edd738c6bdd8431439688bb0cb3d",
"type": "github"
},
"original": {
@@ -84,11 +69,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1672441588,
"narHash": "sha256-jx5kxOyeObnVD44HRebKYL3cjWrcKhhcDmEYm0/naDY=",
"lastModified": 1672844754,
"narHash": "sha256-o26WabuHABQsaHxxmIrR3AQRqDFUEdLckLXkVCpIjSU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6a0d2701705c3cf6f42c15aa92b7885f1f8a477f",
"rev": "e9ade2c8240e00a4784fac282a502efff2786bdc",
"type": "github"
},
"original": {
@@ -116,7 +101,6 @@
"root": {
"inputs": {
"home-manager": "home-manager",
"impermanence": "impermanence",
"mobile-nixos": "mobile-nixos",
"nixpkgs": "nixpkgs",
"nixpkgs-stable": "nixpkgs-stable",

View File

@@ -18,7 +18,6 @@
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
impermanence.url = "github:nix-community/impermanence";
uninsane = {
url = "git+https://git.uninsane.org/colin/uninsane";
inputs.nixpkgs.follows = "nixpkgs";
@@ -32,7 +31,6 @@
mobile-nixos,
home-manager,
sops-nix,
impermanence,
uninsane
}: let
patchedPkgs = system: nixpkgs.legacyPackages.${system}.applyPatches {
@@ -54,12 +52,10 @@
in (nixosSystem {
# by default the local system is the same as the target, employing emulation when they differ
system = target;
specialArgs = { inherit mobile-nixos home-manager impermanence; };
modules = [
./modules
(import ./hosts/instantiate.nix name)
home-manager.nixosModule
impermanence.nixosModule
sops-nix.nixosModules.sops
{
nixpkgs.overlays = [

View File

@@ -7,7 +7,10 @@ let
# see nixpkgs/nixos/modules/services/networking/dhcpcd.nix
hasDHCP = config.networking.dhcpcd.enable &&
(config.networking.useDHCP || any (i: i.useDHCP == true) (attrValues config.networking.interfaces));
mkSymlink = target: {
symlink.target = target;
wantedBeforeBy = [ "multi-user.target" ];
};
in
{
options = {
@@ -28,7 +31,7 @@ in
isNormalUser = true;
home = "/home/colin";
createHome = true;
homeMode = "700";
homeMode = "0700";
uid = config.sane.allocations.colin-uid;
# i don't get exactly what this is, but nixos defaults to this non-deterministically
# in /var/lib/nixos/auto-subuid-map and i don't want that.
@@ -71,20 +74,51 @@ in
security.pam.mount.enable = true;
# ensure ~ perms are known to sane.fs module.
# TODO: this is generic enough to be lifted up into sane.fs itself.
sane.fs."/home/colin".dir.acl = {
user = "colin";
group = config.users.users.colin.group;
mode = config.users.users.colin.homeMode;
};
sane.impermanence.dirs.home.plaintext = [
"archive"
"dev"
# TODO: records should be private
"records"
"ref"
"tmp"
"use"
"Music"
"Pictures"
"Videos"
".cargo"
".rustup"
# TODO: move this to ~/private!
".local/share/keyrings"
];
sane.impermanence.dirs.home.cryptClearOnBoot = [
# TODO: fix this ugly solution that allows moby to have firefox cache not erased every boot.
sane.impermanence.dirs.home.cryptClearOnBoot = lib.mkIf (config.networking.hostName != "moby") [
# cache is probably too big to fit on the tmpfs
# ".cache"
".cache/mozilla"
config.sane.web-browser.cacheDir
];
# convenience
sane.fs."/home/colin/knowledge" = mkSymlink "/home/colin/private/knowledge";
sane.fs."/home/colin/nixos" = mkSymlink "/home/colin/dev/nixos";
sane.fs."/home/colin/Videos/servo" = mkSymlink "/mnt/servo-media/Videos";
sane.fs."/home/colin/Videos/servo-incomplete" = mkSymlink "/mnt/servo-media/incomplete";
sane.fs."/home/colin/Music/servo" = mkSymlink "/mnt/servo-media/Music";
# used by password managers, e.g. unix `pass`
sane.fs."/home/colin/.password-store" = mkSymlink "/home/colin/knowledge/secrets/accounts";
sane.impermanence.dirs.sys.plaintext = mkIf cfg.guest.enable [
{ user = "guest"; group = "users"; directory = "/home/guest"; }
# intentionally allow other users to write to the guest folder
{ directory = "/home/guest"; user = "guest"; group = "users"; mode = "0775"; }
];
users.users.guest = mkIf cfg.guest.enable {
isNormalUser = true;

View File

@@ -24,8 +24,11 @@
};
# usability compromises
sane.impermanence.home-dirs = [
sane.impermanence.dirs.home.private = [
config.sane.web-browser.dotDir
config.sane.web-browser.cacheDir
];
sane.impermanence.dirs.home.plaintext = [
".config/pulse" # persist pulseaudio volume
];

View File

@@ -27,7 +27,7 @@
};
# slow, external storage (for archiving, etc)
fileSystems."/nix/persist/ext" = {
fileSystems."/mnt/impermanence/ext" = {
device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
fsType = "btrfs";
options = [
@@ -36,28 +36,31 @@
];
};
sane.impermanence.stores."ext" = {
origin = "/mnt/impermanence/ext/persist";
storeDescription = "external HDD storage";
};
sane.fs."/mnt/impermanence/ext".mount = {};
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 = [
({
user = "colin";
group = "users";
mode = "0777";
directory = "/var/lib/uninsane/media/Videos";
})
({
user = "colin";
group = "users";
mode = "0777";
directory = "/var/lib/uninsane/media/freeleech";
})
];
};
# make sure large media is stored to the HDD
sane.impermanence.dirs.sys.ext = [
{
user = "colin";
group = "users";
mode = "0777";
directory = "/var/lib/uninsane/media/Videos";
}
{
user = "colin";
group = "users";
mode = "0777";
directory = "/var/lib/uninsane/media/freeleech";
}
];
# in-memory compressed RAM (seems to be dynamically sized)
# zramSwap = {

View File

@@ -14,7 +14,7 @@
sops.secrets.freshrss_passwd = {
sopsFile = ../../../secrets/servo.yaml;
owner = config.users.users.freshrss.name;
mode = "400";
mode = "0400";
};
sane.impermanence.dirs.sys.plaintext = [
{ user = "freshrss"; group = "freshrss"; directory = "/var/lib/freshrss"; }

View File

@@ -2,7 +2,10 @@
{
sane.impermanence.dirs.sys.plaintext = [
{ user = "navidrome"; group = "navidrome"; directory = "/var/lib/private/navidrome"; }
# TODO: we don't have a static user allocated for navidrome!
# the chown would happen too early for us to set static perms
"/var/lib/private/navidrome"
# { user = "navidrome"; group = "navidrome"; directory = "/var/lib/private/navidrome"; }
];
services.navidrome.enable = true;
services.navidrome.settings = {

View File

@@ -1,9 +1,9 @@
{ ... }:
{ lib, utils, ... }:
{
imports = [
./allocations.nix
./fs.nix
./fs
./gui
./home-manager
./packages.nix
@@ -13,4 +13,8 @@
./services
./sops.nix
];
_module.args = {
sane-lib = import ./lib { inherit lib utils; };
};
}

View File

@@ -1,247 +0,0 @@
{ config, lib, pkgs, utils, ... }:
with lib;
let
cfg = config.sane.fs;
mountNameFor = path: "${utils.escapeSystemdPath path}.mount";
serviceNameFor = path: "ensure-${utils.escapeSystemdPath path}";
# sane.fs."<path>" top-level options
fsEntry = types.submodule ({ name, config, ...}: let
parent = parentDir name;
has-parent = hasParent name;
parent-cfg = if has-parent then cfg."${parent}" else {};
parent-dir = parent-cfg.dir or {};
parent-acl = parent-dir.acl or {};
in {
options = {
dir = mkOption {
type = dirEntry;
};
mount = mkOption {
type = types.nullOr (mountEntryFor name);
default = null;
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which ensures this entry";
};
};
config = {
dir.acl.user = lib.mkDefault (parent-acl.user or "root");
dir.acl.group = lib.mkDefault (parent-acl.group or "root");
dir.acl.mode = lib.mkDefault (parent-acl.mode or "0755");
# we put this here instead of as a `default` to ensure that users who specify additional
# dependencies still get a dep on the parent (unless they assign with `mkForce`).
dir.depends = if has-parent then [ parent-cfg.unit ] else [];
# if defaulted, this module is responsible for creating the directory
dir.unit = lib.mkDefault ((serviceNameFor name) + ".service");
# if defaulted, this module is responsible for finalizing the entry.
# the user could override this if, say, they finalize some aspect of the entry
# with a custom service.
unit = lib.mkDefault (if config.mount != null then
config.mount.unit
else config.dir.unit);
};
});
acl = types.submodule {
options = {
user = mkOption {
type = types.str; # TODO: use uid?
};
group = mkOption {
type = types.str;
};
mode = mkOption {
type = types.str;
};
};
};
# sane.fs."<path>".dir sub-options
dirEntry = types.submodule {
options = {
acl = mkOption {
type = acl;
};
depends = mkOption {
type = types.listOf types.str;
description = "list of systemd units needed to be run before this directory can be made";
default = [];
};
reverseDepends = mkOption {
type = types.listOf types.str;
description = "list of systemd units which should be made to depend on this unit (controls `wantedBy` and `before`)";
default = [];
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which ensures this directory";
};
};
};
# sane.fs."<path>".mount sub-options
mountEntryFor = path: types.submodule {
options = {
bind = mkOption {
type = types.nullOr types.str;
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";
default = mountNameFor path;
};
};
};
# given a fsEntry definition, output the `config` attrs it generates.
mkDirConfig = path: opt: {
systemd.services."${serviceNameFor path}" = {
description = "prepare ${path}";
serviceConfig.Type = "oneshot";
script = ensure-dir-script;
scriptArgs = "${path} ${opt.dir.acl.user} ${opt.dir.acl.group} ${opt.dir.acl.mode}";
after = opt.dir.depends;
wants = opt.dir.depends;
# prevent systemd making this unit implicitly dependent on sysinit.target.
# see: <https://www.freedesktop.org/software/systemd/man/systemd.special.html>
unitConfig.DefaultDependencies = "no";
wantedBy = opt.dir.reverseDepends;
before = opt.dir.reverseDepends;
};
};
mkMountConfig = path: opt: (let
underlying = cfg."${opt.mount.bind}";
in {
fileSystems."${path}" = lib.mkIf (opt.mount.bind != null) {
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;
};
});
mkFsConfig = path: opt: mergeTopLevel [
(mkDirConfig path opt)
(lib.mkIf (opt.mount != null) (mkMountConfig path opt))
];
# act as `config = lib.mkMerge [ a b ]` but in a way which avoids infinite recursion,
# by extracting only specific options which are known to not be options in this module.
mergeTopLevel = items: let
# if one of the items is `lib.mkIf cond attrs`, we won't be able to index it until
# after we "push down" the mkIf to each attr.
indexable = lib.pushDownProperties (lib.mkMerge items);
# transform (listOf attrs) to (attrsOf list) by grouping each toplevel attr across lists.
top = lib.zipAttrsWith (name: lib.mkMerge) indexable;
# extract known-good top-level items in a way which errors if a module tries to define something extra.
extract = { fileSystems ? {}, systemd ? {} }@attrs: attrs;
in {
inherit (extract top) fileSystems systemd;
};
# systemd/shell script used to create and set perms for a specific dir
ensure-dir-script = ''
path="$1"
user="$2"
group="$3"
mode="$4"
if ! test -d "$path"
then
# if the directory *doesn't* exist, try creating it
# if we fail to create it, ensure we raced with something else and that it's actually a directory
mkdir "$path" || test -d "$path"
fi
chmod "$mode" "$path"
chown "$user:$group" "$path"
'';
# 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.
# the parent of "/" is "/".
parentDir = str: normPath (builtins.dirOf (normPath str));
hasParent = str: (parentDir str) != (normPath str);
# return all ancestors of this path.
# e.g. ancestorsOf "/foo/bar/baz" => [ "/" "/foo" "/foo/bar" ]
ancestorsOf = path: if hasParent path then
ancestorsOf (parentDir path) ++ [ (parentDir path) ]
else
[ ]
;
# attrsOf fsEntry type which for every entry ensures that all ancestor entries are created.
# we do this with a custom type to ensure that users can access `config.sane.fs."/parent/path"`
# when inferred.
fsTree = let
baseType = types.attrsOf fsEntry;
# merge is called once, with all collected `sane.fs` definitions passed and we coalesce those
# into a single value `x` as if the user had wrote simply `sane.fs = x` in a single location.
# so option defaulting and such happens *after* `merge` is called.
merge = loc: defs: let
# loc is the location of the option holding this type, e.g. ["sane" "fs"].
# each def is an { value = attrsOf fsEntry instance; file = "..."; }
pathsForDef = def: attrNames def.value;
origPaths = concatLists (builtins.map pathsForDef defs);
extraPaths = concatLists (builtins.map ancestorsOf origPaths);
extraDefs = builtins.map (p: {
file = ./.;
value = {
"${p}".dir = {};
};
}) extraPaths;
in
baseType.merge loc (defs ++ extraDefs);
in
lib.mkOptionType {
inherit merge;
name = "fsTree";
description = "attrset representation of a file-system tree";
# ensure that every path is in canonical form, else we might get duplicates and subtle errors
check = tree: builtins.all (p: p == normPath p) (builtins.attrNames tree);
};
in {
options = {
sane.fs = mkOption {
# type = types.attrsOf fsEntry;
type = fsTree;
default = {};
};
};
config = mergeTopLevel (lib.mapAttrsToList mkFsConfig cfg);
}

353
modules/fs/default.nix Normal file
View File

@@ -0,0 +1,353 @@
{ config, lib, pkgs, utils, sane-lib, ... }:
with lib;
let
path-lib = sane-lib.path;
sane-types = sane-lib.types;
cfg = config.sane.fs;
mountNameFor = path: "${utils.escapeSystemdPath path}.mount";
serviceNameFor = path: "ensure-${utils.escapeSystemdPath path}";
# sane.fs."<path>" top-level options
fsEntry = types.submodule ({ name, config, ...}: let
parent = path-lib.parent name;
has-parent = path-lib.hasParent name;
parent-cfg = if has-parent then cfg."${parent}" else {};
parent-acl = if has-parent then parent-cfg.generated.acl else {};
in {
options = {
dir = mkOption {
type = types.nullOr dirEntry;
default = null;
};
symlink = mkOption {
type = types.nullOr symlinkEntry;
default = null;
};
generated = mkOption {
type = generatedEntry;
default = {};
};
mount = mkOption {
type = types.nullOr (mountEntryFor name);
default = null;
};
wantedBy = mkOption {
type = types.listOf types.str;
default = [];
description = ''
list of units or targets which, when activated, should trigger this fs entry to be created.
'';
};
wantedBeforeBy = mkOption {
type = types.listOf types.str;
default = [];
description = ''
list of units or targets which, when activated, should first start and wait for this fs entry to be created.
if this unit fails, it will not block the targets in this list.
'';
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which ensures this entry";
};
};
config = let
default-acl = {
user = lib.mkDefault (parent-acl.user or "root");
group = lib.mkDefault (parent-acl.group or "root");
mode = lib.mkDefault (parent-acl.mode or "0755");
};
in {
# we put this here instead of as a `default` to ensure that users who specify additional
# dependencies still get a dep on the parent (unless they assign with `mkForce`).
generated.depends = if has-parent then [ parent-cfg.unit ] else [];
# populate generated items from `dir` or `symlink` shorthands
generated.acl = lib.mkMerge [
default-acl
(lib.mkIf (config.dir != null)
(sane-lib.filterNonNull config.dir.acl))
(lib.mkIf (config.symlink != null)
(sane-lib.filterNonNull config.symlink.acl))
];
# actually generate the item
generated.script = lib.mkMerge [
(lib.mkIf (config.dir != null) (ensureDirScript name config.dir))
(lib.mkIf (config.symlink != null) (ensureSymlinkScript name config.symlink))
];
# make the unit file which generates the underlying thing available so that `mount` can use it.
generated.unit = (serviceNameFor name) + ".service";
# if defaulted, this module is responsible for finalizing the entry.
# the user could override this if, say, they finalize some aspect of the entry
# with a custom service.
unit = lib.mkDefault (
if config.mount != null then
config.mount.unit
else
config.generated.unit
);
};
});
# options which can be set in dir/symlink generated items,
# with intention that they just propagate down
propagatedGenerateMod = {
options = {
acl = mkOption {
type = sane-types.aclOverride;
default = {};
};
};
};
# sane.fs."<path>".dir sub-options
# takes no special options
dirEntry = types.submodule propagatedGenerateMod;
symlinkEntry = types.submodule {
options = {
inherit (propagatedGenerateMod.options) acl;
target = mkOption {
type = types.str;
description = "fs path to link to";
};
};
};
generatedEntry = types.submodule {
options = {
acl = mkOption {
type = sane-types.acl;
};
depends = mkOption {
type = types.listOf types.str;
description = ''
list of systemd units needed to be run before this item can be generated.
'';
default = [];
};
script.script = mkOption {
type = types.lines;
};
script.scriptArgs = mkOption {
type = types.listOf types.str;
default = [];
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which ensures this directory";
};
};
};
# sane.fs."<path>".mount sub-options
mountEntryFor = path: types.submodule {
options = {
bind = mkOption {
type = types.nullOr types.str;
description = "fs path to bind-mount from";
default = null;
};
depends = mkOption {
type = types.listOf types.str;
description = ''
list of systemd units needed to be run before this entry can be mounted
'';
default = [];
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which mounts this path";
default = mountNameFor path;
};
};
};
mkGeneratedConfig = path: opt: let
gen-opt = opt.generated;
wrapper = generateWrapperScript path gen-opt;
in {
systemd.services."${serviceNameFor path}" = {
description = "prepare ${path}";
serviceConfig.Type = "oneshot";
script = wrapper.script;
scriptArgs = builtins.concatStringsSep " " wrapper.scriptArgs;
after = gen-opt.depends;
wants = gen-opt.depends;
# prevent systemd making this unit implicitly dependent on sysinit.target.
# see: <https://www.freedesktop.org/software/systemd/man/systemd.special.html>
unitConfig.DefaultDependencies = "no";
before = opt.wantedBeforeBy;
wantedBy = opt.wantedBy ++ opt.wantedBeforeBy;
};
};
# given a mountEntry definition, evaluate its toplevel `config` output.
mkMountConfig = path: opt: (let
device = config.fileSystems."${path}".device;
underlying = cfg."${device}";
isBind = opt.mount.bind != null;
ifBind = lib.mkIf isBind;
# before mounting:
# - create the target directory
# - prepare the source directory -- assuming it's not an external device
# - satisfy any user-specified prerequisites ("depends")
requires = [ opt.generated.unit ]
++ (if lib.hasPrefix "/dev/disk/" device then [] else [ underlying.unit ])
++ opt.mount.depends;
in {
fileSystems."${path}" = {
device = ifBind opt.mount.bind;
options = (if isBind then ["bind"] else [])
++ [
# disable defaults: don't require this to be mount as part of local-fs.target
# we'll handle that stuff precisely.
"noauto"
"nofail"
# x-systemd options documented here:
# - <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
]
++ (builtins.map (unit: "x-systemd.requires=${unit}") requires)
++ (builtins.map (unit: "x-systemd.before=${unit}") opt.wantedBeforeBy)
++ (builtins.map (unit: "x-systemd.wanted-by=${unit}") (opt.wantedBy ++ opt.wantedBeforeBy));
noCheck = ifBind true;
};
});
mkFsConfig = path: opt: mergeTopLevel [
(mkGeneratedConfig path opt)
(lib.mkIf (opt.mount != null) (mkMountConfig path opt))
];
# act as `config = lib.mkMerge [ a b ]` but in a way which avoids infinite recursion,
# by extracting only specific options which are known to not be options in this module.
mergeTopLevel = items: let
# if one of the items is `lib.mkIf cond attrs`, we won't be able to index it until
# after we "push down" the mkIf to each attr.
indexable = lib.pushDownProperties (lib.mkMerge items);
# transform (listOf attrs) to (attrsOf list) by grouping each toplevel attr across lists.
top = lib.zipAttrsWith (name: lib.mkMerge) indexable;
# extract known-good top-level items in a way which errors if a module tries to define something extra.
extract = { fileSystems ? {}, systemd ? {} }@attrs: attrs;
in {
inherit (extract top) fileSystems systemd;
};
generateWrapperScript = path: gen-opt: {
script = ''
fspath="$1"
acluser="$2"
aclgroup="$3"
aclmode="$4"
shift 4
# ensure any things created by the user script have the desired mode.
# chmod doesn't work on symlinks, so we *have* to use this umask approach.
decmask=$(( 0777 - "$aclmode" ))
octmask=$(printf "%o" "$decmask")
umask "$octmask"
# try to chmod/chown the result even if the user script errors
_status=0
trap "_status=\$?" ERR
${gen-opt.script.script}
# claim ownership of the new thing (DON'T traverse symlinks)
chown --no-dereference "$acluser:$aclgroup" "$fspath"
# AS LONG AS IT'S NOT A SYMLINK, try to fix perms in case the entity existed before this script was called
if ! test -L "$fspath"
then
chmod "$aclmode" "$fspath"
fi
exit "$_status"
'';
scriptArgs = [ path gen-opt.acl.user gen-opt.acl.group gen-opt.acl.mode ] ++ gen-opt.script.scriptArgs;
};
# systemd/shell script used to create and set perms for a specific dir
ensureDirScript = path: dir-cfg: {
script = ''
dirpath="$1"
if ! test -d "$dirpath"
then
# if the directory *doesn't* exist, try creating it
# if we fail to create it, ensure we raced with something else and that it's actually a directory
mkdir "$dirpath" || test -d "$dirpath"
fi
'';
scriptArgs = [ path ];
};
# systemd/shell script used to create a symlink
ensureSymlinkScript = path: link-cfg: {
script = ''
lnfrom="$1"
lnto="$2"
ln -sf --no-dereference "$lnto" "$lnfrom"
'';
scriptArgs = [ path link-cfg.target ];
};
# return all ancestors of this path.
# e.g. ancestorsOf "/foo/bar/baz" => [ "/" "/foo" "/foo/bar" ]
# TODO: move this to path-lib?
ancestorsOf = path: if path-lib.hasParent path then
ancestorsOf (path-lib.parent path) ++ [ (path-lib.parent path) ]
else
[ ]
;
# attrsOf fsEntry type which for every entry ensures that all ancestor entries are created.
# we do this with a custom type to ensure that users can access `config.sane.fs."/parent/path"`
# when inferred.
fsTree = let
baseType = types.attrsOf fsEntry;
# merge is called once, with all collected `sane.fs` definitions passed and we coalesce those
# into a single value `x` as if the user had wrote simply `sane.fs = x` in a single location.
# so option defaulting and such happens *after* `merge` is called.
merge = loc: defs: let
# loc is the location of the option holding this type, e.g. ["sane" "fs"].
# each def is an { value = attrsOf fsEntry instance; file = "..."; }
pathsForDef = def: attrNames def.value;
origPaths = concatLists (builtins.map pathsForDef defs);
extraPaths = concatLists (builtins.map ancestorsOf origPaths);
extraDefs = builtins.map (p: {
file = ./.;
value = {
"${p}".dir = {};
};
}) extraPaths;
in
baseType.merge loc (defs ++ extraDefs);
in
lib.mkOptionType {
inherit merge;
name = "fsTree";
description = "attrset representation of a file-system tree";
# ensure that every path is in canonical form, else we might get duplicates and subtle errors
check = tree: builtins.all (p: p == path-lib.norm p) (builtins.attrNames tree);
};
in {
options = {
sane.fs = mkOption {
# type = types.attrsOf fsEntry;
type = fsTree;
default = {};
};
};
config = mergeTopLevel (lib.mapAttrsToList mkFsConfig cfg);
}

View File

@@ -48,19 +48,6 @@ in
};
config = lib.mkIf cfg.enable {
sane.impermanence.dirs.home.plaintext = [
"archive"
"dev"
# TODO: records should be private
"records"
"ref"
"tmp"
"use"
"Music"
"Pictures"
"Videos"
];
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
@@ -88,19 +75,6 @@ in
};
};
home.file = {
# convenience
"knowledge".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/private/knowledge";
"nixos".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/dev/nixos";
"Videos/servo".source = config.lib.file.mkOutOfStoreSymlink "/mnt/servo-media/Videos";
"Videos/servo-incomplete".source = config.lib.file.mkOutOfStoreSymlink "/mnt/servo-media/incomplete";
"Music/servo".source = config.lib.file.mkOutOfStoreSymlink "/mnt/servo-media/Music";
# used by password managers, e.g. unix `pass`
".password-store".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/knowledge/secrets/accounts";
};
# XDG defines things like ~/Desktop, ~/Downloads, etc.
# these clutter the home, so i mostly don't use them.
xdg.userDirs = {

View File

@@ -19,12 +19,14 @@ let
# });
libName = "librewolf";
dotDir = ".librewolf";
cacheDir = ".cache/librewolf"; # TODO: is it?
desktop = "librewolf.desktop";
};
firefoxSettings = {
browser = pkgs.firefox-esr-unwrapped;
libName = "firefox";
dotDir = ".mozilla/firefox";
cacheDir = ".cache/mozilla";
desktop = "firefox.desktop";
};
defaultSettings = firefoxSettings;
@@ -55,9 +57,9 @@ let
# get names from:
# - ~/ref/nix-community/nur-combined/repos/rycee/pkgs/firefox-addons/generated-firefox-addons.nix
# `wget ...xpi`; `unar ...xpi`; `cat */manifest.json | jq '.browser_specific_settings.gecko.id'`
(addon "ublock-origin" "uBlock0@raymondhill.net" "sha256-+xc4lcdsOwXxMsr4enFsdePbIb6GHq0bFLpqvH5xXos=")
(addon "sponsorblock" "sponsorBlocker@ajay.app" "sha256-30F8oDIgshXVY7YKgnfoc1tUTHfgeFbzXISJuVJs0AM=")
(addon "bypass-paywalls-clean" "{d133e097-46d9-4ecc-9903-fa6a722a6e0e}" "sha256-7ZDkG8O1rEYdh/La0PLi9tp92JxYeQvaOFt/BmnDv3U=")
(addon "ublock-origin" "uBlock0@raymondhill.net" "sha256-a/ivUmY1P6teq9x0dt4CbgHt+3kBsEMMXlOfZ5Hx7cg=")
(addon "sponsorblock" "sponsorBlocker@ajay.app" "sha256-d2K3ufvurWnYVzqLbyR//MgejybkY9exitAf9RdLNRo=")
(addon "bypass-paywalls-clean" "{d133e097-46d9-4ecc-9903-fa6a722a6e0e}" "sha256-t6Q335Nq60mDILPmzem+DT5KflleAPVJL3bsaA+UL0g=")
(addon "sidebery" "{3c078156-979c-498b-8990-85f7987dd929}" "sha256-YONfK/rIjlsrTgRHIt3km07Q7KnpIW89Z9r92ZSCc6w=")
(addon "ether-metamask" "webextension@metamask.io" "sha256-G+MwJDOcsaxYSUXjahHJmkWnjLeQ0Wven8DU/lGeMzA=")
(addon "ublacklist" "@ublacklist" "sha256-vHe/7EYOzcKeAbTElmt0Rb4E2rX0f3JgXThJaUmaz+M=")

View File

@@ -2,61 +2,63 @@
# 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, utils, ... }:
{ config, lib, pkgs, utils, sane-lib, ... }:
with lib;
let
path = sane-lib.path;
sane-types = sane-lib.types;
cfg = config.sane.impermanence;
storeType = types.submodule {
options = {
mountpt = mkOption {
storeDescription = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
an optional description of the store, which is rendered like
{store.name}: {store.storeDescription}
for example, a store named "private" could have description "ecnrypted to the user's password and decrypted on login".
'';
};
origin = mkOption {
type = types.str;
};
prefix = mkOption {
type = types.str;
default = "/";
description = ''
optional prefix to strip from children when stored here.
for example, prefix="/var/private" and mountpoint="/mnt/crypt/private"
would cause /var/private/www/root to be stored at /mnt/crypt/private/www/root instead of
/mnt/crypt/private/var/private/www/root.
'';
};
extraOptions = mkOption {
defaultOrdering.wantedBeforeBy = mkOption {
type = types.listOf types.str;
default = [];
default = [ "local-fs.target" ];
description = ''
list of units or targets which would prefer that everything in this store
be initialized before they run, but failing to do so should not error the items in this list.
'';
};
defaultOrdering.wantedBy = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
list of units or targets which, upon activation, should activate all units in this store.
'';
};
};
};
# 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));
# 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
dirEntryOptions = {
options = {
directory = mkOption {
type = types.str;
};
user = mkOption {
type = types.nullOr types.str;
default = null;
};
group = mkOption {
type = types.nullOr types.str;
default = null;
};
mode = mkOption {
type = types.nullOr types.str;
default = null;
};
inherit (sane-types.aclOverrideMod.options) user group mode;
};
};
contextualizedDir = types.submodule dirEntryOptions;
@@ -79,46 +81,29 @@ let
}
];
dirsSubModule = types.submodule {
options = mapAttrs (store: store-cfg: mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = let
suffix = if store-cfg.storeDescription != null then
": ${store-cfg.storeDescription}"
else "";
in "directories to persist in ${store}${suffix}";
}) cfg.stores;
};
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.
'';
};
};
};
type = dirsSubModule;
};
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 /";
};
};
};
type = dirsSubModule;
};
all = mkOption {
type = types.listOf contextFreeDir;
@@ -129,16 +114,18 @@ let
mapDirs = relativeTo: store: dirs: (map
(d: {
inherit (d) user group mode;
directory = concatPaths [ relativeTo d.directory ];
directory = path.concat [ relativeTo d.directory ];
store = cfg.stores."${store}";
})
dirs
);
mapDirSets = relativeTo: dirsSubOptions: let
# list where each elem is a list from calling mapDirs on one store at a time
contextFreeDirSets = lib.mapAttrsToList (mapDirs relativeTo) dirsSubOptions;
in
builtins.concatLists contextFreeDirSets;
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);
all = (mapDirSets "/home/colin" config.home) ++ (mapDirSets "/" config.sys);
};
});
in
@@ -160,6 +147,9 @@ in
sane.impermanence.stores = mkOption {
type = types.attrsOf storeType;
default = {};
description = ''
map from human-friendly name to a fs sub-tree from which files are linked into the logical fs.
'';
};
};
@@ -168,57 +158,34 @@ in
./stores
];
config = mkIf cfg.enable (lib.mkMerge [
{
# TODO: move to sane.fs, to auto-ensure all user dirs?
sane.fs."/home/colin".dir.acl = {
user = "colin";
group = config.users.users.colin.group;
mode = config.users.users.colin.homeMode;
};
config = let
cfgFor = opt:
let
store = opt.store;
store-rel-path = path.from store.prefix opt.directory;
backing-path = path.concat [ store.origin store-rel-path ];
# 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.
# what is a problem is if the user specified some other dir we don't know about here.
# like "/var", and then "/nix/persist/var" has different perms and something mounts funny.
# TODO: just add assertions that sane.fs."${backing}/${dest}".dir == sane.fs."${dest}" for each mount point?
sane.fs."/nix/persist/home/colin".dir.acl = config.sane.fs."/home/colin".dir.acl;
sane.fs."/mnt/impermanence/crypt/clearedonboot/home/colin".dir.acl = config.sane.fs."/home/colin".dir.acl;
}
(
let cfgFor = opt:
let
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 = {
user = lib.mkIf (opt.user != null) opt.user;
group = lib.mkIf (opt.group != null) opt.group;
mode = lib.mkIf (opt.mode != null) opt.mode;
};
in {
# create destination and backing directory, with correct perms
sane.fs."${opt.directory}" = {
# 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;
};
# pass through the perm/mode overrides
dir-acl = sane-lib.filterNonNull {
inherit (opt) user group mode;
};
cfgs = builtins.map cfgFor cfg.dirs.all;
in {
sane.fs = lib.mkMerge (catAttrs "fs" (catAttrs "sane" cfgs));
}
)
]);
# create destination and backing directory, with correct perms
sane.fs."${opt.directory}" = {
# inherit perms & make sure we don't mount until after the mount point is setup correctly.
dir.acl = dir-acl;
mount.bind = backing-path;
inherit (store.defaultOrdering) wantedBy wantedBeforeBy;
};
sane.fs."${backing-path}" = {
# ensure the backing path has same perms as the mount point.
# TODO: maybe we want to do this, crawling all the way up to the store base?
# that would simplify (remove) the code in stores/default.nix
dir.acl = config.sane.fs."${opt.directory}".generated.acl;
};
};
in mkIf cfg.enable {
sane.fs = lib.mkMerge (map (d: (cfgFor d).sane.fs) cfg.dirs.all);
};
}

View File

@@ -3,61 +3,24 @@
let
store = rec {
device = "/mnt/impermanence/crypt/clearedonboot";
mount-unit = "${utils.escapeSystemdPath device}.mount";
underlying = {
path = "/nix/persist/crypt/clearedonboot";
# TODO: consider moving this to /tmp, but that requires tmp be mounted first?
key = "/mnt/impermanence/crypt/clearedonboot.key";
};
};
prepareEncryptedClearedOnBoot = pkgs.writeShellApplication {
name = "prepareEncryptedClearedOnBoot";
runtimeInputs = with pkgs; [ gocryptfs ];
text = ''
backing="$1"
passfile="$2"
if ! test -e "$passfile"
then
# 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
umask 266
dd if=/dev/random bs=128 count=1 | base64 --wrap=0 > "$passfile"
umask 022
# initialize the crypt store
gocryptfs -quiet -passfile "$passfile" -init "$backing"
fi
'';
};
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 = ''
${prepareEncryptedClearedOnBoot}/bin/prepareEncryptedClearedOnBoot ${store.underlying.path} ${store.underlying.key}
storeDescription = ''
stored to disk, but encrypted to an in-memory key and cleared on every boot
so that it's unreadable after power-off
'';
serviceConfig.Type = "oneshot";
# remove implicit dep on sysinit.target
unitConfig.DefaultDependencies = "no";
# we need the key directory to be created, and the backing directory to exist
after = [
config.sane.fs."${store.underlying.path}".unit
# TODO: "${parentDir store.device}"
config.sane.fs."/mnt/impermanence/crypt".unit
];
wants = after;
# make sure the encrypted file system is mounted *after* its keys have been generated.
before = [ store.mount-unit ];
wantedBy = before;
origin = store.device;
};
fileSystems."${store.device}" = {
device = store.underlying.path;
fsType = "fuse.gocryptfs";
@@ -70,20 +33,42 @@ lib.mkIf config.sane.impermanence.enable
];
noCheck = true;
};
sane.fs."${store.device}" = {
# ensure the fs is mounted only after the mountpoint directory is created
dir.reverseDepends = [ store.mount-unit ];
# HACK: this fs entry is provided by our mount unit.
mount.unit = store.mount-unit;
# let sane.fs know about our fileSystem and automatically add the appropriate dependencies
sane.fs."${store.device}".mount = {
# technically the dependency on the keyfile is extraneous because that *happens* to
# be needed to init the store.
depends = let
cryptfile = config.sane.fs."${store.underlying.path}/gocryptfs.conf";
keyfile = config.sane.fs."${store.underlying.key}";
in [ keyfile.unit cryptfile.unit ];
};
sane.fs."${store.underlying.path}" = {
# don't mount until after the backing dir is setup correctly.
# TODO: this isn't necessary? the mount-unit already depends on prepareEncryptedClearOnBoot
# which depends on the underlying path?
dir.reverseDepends = [ store.mount-unit ];
# let sane.fs know how to initialize the gocryptfs store,
# and that it MUST do so
sane.fs."${store.underlying.path}/gocryptfs.conf".generated = {
script.script = ''
backing="$1"
passfile="$2"
# clear the backing store
# TODO: we should verify that it's not mounted anywhere...
rm -rf "''${backing:?}"/*
${pkgs.gocryptfs}/bin/gocryptfs -quiet -passfile "$passfile" -init "$backing"
'';
script.scriptArgs = [ store.underlying.path store.underlying.key ];
# we need the key in order to initialize the store
depends = [ config.sane.fs."${store.underlying.key}".unit ];
};
# let sane.fs know how to generate the key for gocryptfs
sane.fs."${store.underlying.key}".generated = {
script.script = ''
dd if=/dev/random bs=128 count=1 | base64 --wrap=0 > "$1"
'';
script.scriptArgs = [ store.underlying.key ];
# no need for anyone else to be able to read the key
acl.mode = "0400";
};
# TODO: could add this *specifically* to the .mount file for the encrypted fs?
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
system.fsPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
}

View File

@@ -1,17 +1,31 @@
{ config, lib, ... }:
{ config, lib, sane-lib, ... }:
let
cfg = config.sane.impermanence;
path = sane-lib.path;
in
{
imports = [
./crypt.nix
./plaintext.nix
./private.nix
];
config = lib.mkIf cfg.enable {
sane.impermanence.stores."plaintext" = {
mountpt = "/nix/persist";
};
# make sure that the store has the same acl as the main filesystem,
# particularly for /home/colin.
#
# 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.
# what is a problem is if the user specified some other dir we don't know about here.
# like "/var", and then "/nix/persist/var" has different perms and something mounts funny.
# TODO: just add assertions that sane.fs."${backing}/${dest}".dir == sane.fs."${dest}" for each mount point?
sane.fs = lib.mapAttrs' (_name: store: let
home-in-store = path.from store.prefix "/home/colin";
in {
name = path.concat [ store.origin home-in-store ];
value.dir.acl = config.sane.fs."/home/colin".generated.acl;
}) cfg.stores;
};
}

View File

@@ -0,0 +1,11 @@
{ config, lib, ... }:
let
cfg = config.sane.impermanence;
in lib.mkIf cfg.enable {
sane.impermanence.stores."plaintext" = {
origin = "/nix/persist";
};
# TODO: needed?
# sane.fs."/nix".mount = {};
}

View File

@@ -1,24 +1,25 @@
{ config, lib, pkgs, utils, ... }:
let
private-mount-unit = ''${utils.escapeSystemdPath "/home/colin/private"}.mount'';
in lib.mkIf config.sane.impermanence.enable
lib.mkIf config.sane.impermanence.enable
{
sane.impermanence.stores."private" = {
mountpt = "/home/colin/private";
storeDescription = ''
encrypted to the user's password and auto-unlocked at login
'';
origin = "/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
defaultOrdering = let
private-unit = config.sane.fs."/home/colin/private".unit;
in [
"noauto"
# auto mount when ~/private is mounted
"x-systemd.wanted-by=${private-unit}"
];
in {
# auto create only after ~/private is mounted
wantedBy = [ private-unit ];
# we can't create things in private before local-fs.target
wantedBeforeBy = [ ];
};
};
fileSystems."/home/colin/private" = {
@@ -26,6 +27,7 @@ in lib.mkIf config.sane.impermanence.enable
fsType = "fuse.gocryptfs";
options = [
"noauto" # don't try to mount, until the user logs in!
"nofail"
"allow_other" # root ends up being the user that mounts this, so need to make it visible to `colin`.
"nodev"
"nosuid"
@@ -35,27 +37,12 @@ in lib.mkIf config.sane.impermanence.enable
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"
];
};
# let sane.fs know about the mount
sane.fs."/home/colin/private".mount = {};
# it also needs to know that the underlying device is an ordinary folder
sane.fs."/nix/persist/home/colin/private".dir = {};
# TODO: could add this *specifically* to the .mount file for the encrypted fs?
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
system.fsPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
}

8
modules/lib/default.nix Normal file
View File

@@ -0,0 +1,8 @@
{ lib, ... }@moduleArgs:
{
path = import ./path.nix moduleArgs;
types = import ./types.nix moduleArgs;
filterNonNull = attrs: lib.filterAttrsRecursive (n: v: v != null) attrs;
}

30
modules/lib/path.nix Normal file
View File

@@ -0,0 +1,30 @@
{ lib, utils, ... }:
let path = rec {
# split the string path into a list of string components.
# root directory "/" becomes the empty list [].
# implicitly performs normalization so that:
# split "a//b/" => ["a" "b"]
# split "/a/b" => ["a" "b"]
split = str: builtins.filter (seg: seg != "") (lib.splitString "/" str);
# given an array of components, returns the equivalent string path
join = comps: "/" + (builtins.concatStringsSep "/" comps);
# given an a sequence of string paths, concatenates them into one long string path
concat = paths: path.join (builtins.concatLists (builtins.map path.split paths));
# normalize the given path
norm = str: path.join (path.split str);
# return the parent directory. doesn't care about leading/trailing slashes.
# the parent of "/" is "/".
parent = str: path.norm (builtins.dirOf (path.norm str));
hasParent = str: (path.parent str) != (path.norm str);
# return the path from `from` to `to`, but keeping absolute form
# e.g. `pathFrom "/home/colin" "/home/colin/foo/bar"` -> "/foo/bar"
from = start: end: let
s = path.norm start;
e = path.norm end;
in (
assert lib.hasPrefix s e;
"/" + (lib.removePrefix s e)
);
};
in path

42
modules/lib/types.nix Normal file
View File

@@ -0,0 +1,42 @@
{ lib, ... }:
with lib;
rec {
# "Access Control List", only it's just a user:group and file mode
# compatible with `chown` and `chmod`
aclMod = {
options = {
user = mkOption {
type = types.str; # TODO: use uid?
};
group = mkOption {
type = types.str;
};
mode = mkOption {
type = types.str;
};
};
};
acl = types.submodule aclMod;
# this is acl, but doesn't require to be fully specified.
# a typical use case is when there's a complete acl, and the user
# wants to override just one attribute of it.
aclOverrideMod = {
options = {
user = mkOption {
type = types.nullOr types.str;
default = null;
};
group = mkOption {
type = types.nullOr types.str;
default = null;
};
mode = mkOption {
type = types.nullOr types.str;
default = null;
};
};
};
aclOverride = types.submodule aclOverrideMod;
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
sudo systemctl stop matrix-appservice-irc mx-puppet-discord
sudo systemctl stop pleroma gitea matrix-synapse jellyfin transmission jackett
sudo systemctl stop ejabberd goaccess i2p kiwix-serve navidrome
# TODO: stop the freshrss timer
sudo systemctl stop phpfpm-freshrss
sudo systemctl stop dovecot2 opendkin postfix
@@ -8,4 +9,5 @@ sudo systemctl stop nginx
sudo systemctl stop postgresql
sudo systemctl stop duplicity.timer
sudo systemctl stop duplicity
sudo systemctl stop trust-dns
sudo systemctl stop wireguard-wg0