Compare commits
41 Commits
staging/im
...
staging/ni
Author | SHA1 | Date | |
---|---|---|---|
1a0f05bfd6 | |||
c18dd9636d | |||
0977721af5 | |||
122d3cd7e4 | |||
cd5f8054c0 | |||
3db388b105 | |||
2ba6116f10 | |||
592d17b725 | |||
4d9c15f9b8 | |||
abced7dd0d | |||
5c42365912 | |||
247ad326b2 | |||
170008f345 | |||
2c48e61854 | |||
f89f756489 | |||
c0da19951b | |||
5fb67306e4 | |||
5533b586d7 | |||
68c2eb7363 | |||
fd79026366 | |||
a76471cb1f | |||
c94b8299a6 | |||
175bc0709f | |||
7b02477486 | |||
d7c8638fea | |||
9d7d1acc80 | |||
787857d27f | |||
9c248a8a31 | |||
829680fb00 | |||
a9ee26388c | |||
2960b895b6 | |||
933063115b | |||
afe684ca2c | |||
93f1411522 | |||
01e44c1f7f | |||
618e9bd2fa | |||
fbc39d0584 | |||
2d7b3750cd | |||
e6ccd2e4f7 | |||
d4bf491e9c | |||
5a2bbcce3b |
28
flake.lock
generated
28
flake.lock
generated
@@ -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",
|
||||
|
@@ -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 = [
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
];
|
||||
|
||||
|
@@ -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 = {
|
||||
|
@@ -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"; }
|
||||
|
@@ -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 = {
|
||||
|
@@ -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; };
|
||||
};
|
||||
}
|
||||
|
247
modules/fs.nix
247
modules/fs.nix
@@ -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
353
modules/fs/default.nix
Normal 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);
|
||||
}
|
@@ -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 = {
|
||||
|
@@ -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=")
|
||||
|
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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;
|
||||
};
|
||||
}
|
||||
|
11
modules/impermanence/stores/plaintext.nix
Normal file
11
modules/impermanence/stores/plaintext.nix
Normal 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 = {};
|
||||
}
|
@@ -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
8
modules/lib/default.nix
Normal 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
30
modules/lib/path.nix
Normal 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
42
modules/lib/types.nix
Normal 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;
|
||||
}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user