Compare commits

...

69 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
327e6b536f impermanence: large refactor, and experimental bind mounting of things from ~/private 2023-01-03 07:22:37 +00:00
bace7403e7 Merge branch 'staging/nixpkgs-2022-12-31' 2023-01-03 03:05:21 +00:00
57f5521ef3 grpc: unpin (seems to build OK) 2023-01-03 03:05:07 +00:00
9e32211c12 impermanence: cange "encryptedClearOnBoot" to a broader "store" argument
in the future it can support ~/private as a backing store
2023-01-03 03:04:19 +00:00
edf6bd4455 fs: add a "mount.bind" option & use it for impermanence bind-mounts 2023-01-03 02:45:23 +00:00
a9a14786f9 packages: disable fractal (unused, slow build) 2023-01-02 23:35:43 +00:00
eade5fe16e flake update: 2022-12-22 -> 2022-12-31
```
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/652e92b8064949a11bc193b90b74cb727f2a1405' (2022-12-22)
  → 'github:NixOS/nixpkgs/8ba56d7c0d7490680f2d51ba46a141eca7c46afa' (2022-12-31)
• Updated input 'nixpkgs-stable':
    'github:NixOS/nixpkgs/dac57a4eccf1442e8bf4030df6fcbb55883cb682' (2022-12-24)
  → 'github:NixOS/nixpkgs/6a0d2701705c3cf6f42c15aa92b7885f1f8a477f' (2022-12-30)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/855b8d51fc3991bd817978f0f093aa6ae0fae738' (2022-12-25)
  → 'github:Mic92/sops-nix/b35586cc5abacd4eba9ead138b53e2a60920f781' (2023-01-01)
• Updated input 'sops-nix/nixpkgs-stable':
    'github:NixOS/nixpkgs/939c05a176b8485971463c18c44f48e56a7801c9' (2022-12-24)
  → 'github:NixOS/nixpkgs/feda52be1d59f13b9aa02f064b4f14784b9a06c8' (2022-12-31)
```
2023-01-02 22:34:22 +00:00
be222c1d70 trust-dns: allow shorthand assignment of record lists 2023-01-02 13:23:52 +00:00
88a33dd5de snippets: add private links 2023-01-02 13:23:29 +00:00
875e923197 declare ~/private in fileSystems and reuse for pamMount 2023-01-02 11:34:02 +00:00
54dd643cf0 trust-dns: make a note about another DNS library we could draw from 2023-01-02 11:33:32 +00:00
3c726f148b remove some stale references to mobile-nixos 2023-01-02 10:00:20 +00:00
e225e2e704 modules/packages: directly set impermanence.home-dirs instead of working through home-manager 2023-01-02 07:45:05 +00:00
cf0bf8190e modules/packages: clean up loose typing of sane.packages 2023-01-02 07:16:16 +00:00
b8f7f68d4c packages: telegram: persist data in private storage 2023-01-02 07:06:58 +00:00
7a3aae8c97 fs: tidy 2022-12-31 12:38:50 +00:00
89e519810d impermanence: clean up the bind mounts 2022-12-31 12:31:49 +00:00
0e920230ba impermanence: fix systemd service ordering for crypt mount 2022-12-31 12:18:27 +00:00
6ffae00e17 fs: rename "service" option to "unit" option 2022-12-31 11:31:16 +00:00
be19985440 impermanence: crypt: more robust perms and ordering of backing device 2022-12-31 10:45:43 +00:00
f7e3e7294a impermanence: transform gocryptfs key generation from activation script to systemd unit 2022-12-31 10:15:08 +00:00
d745e3c1ee impermanence: remove fuse module: we don't need it now that we're mounting after activation 2022-12-31 09:13:31 +00:00
c1890ce82b impermanence: cleanup some previously verbose code 2022-12-31 09:09:51 +00:00
53a0b621d8 impermanence: use sane.fs to inherit permissions instead of specifying defaults here 2022-12-31 01:04:49 +00:00
aeb2f63d65 impermanence: defer to fs.nix module for permissions & dir creation 2022-12-31 00:38:15 +00:00
528ffdb58e add a new 'fs.nix' file i'll use to factor the impermanence stuff better 2022-12-30 14:45:34 +00:00
b6887b305e impermanence: split out the root-on-tmpfs stuff 2022-12-30 04:35:34 +00:00
08dfc80c98 impermanence: split out sops setup 2022-12-30 04:31:24 +00:00
49 changed files with 1007 additions and 497 deletions

40
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": 1671722432,
"narHash": "sha256-ojcZUekIQeOZkHHzR81st7qxX99dB1Eaaq6PU5MNeKc=",
"lastModified": 1672791794,
"narHash": "sha256-mqGPpGmwap0Wfsf3o2b6qHJW1w2kk/I6cGCGIU+3t6o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "652e92b8064949a11bc193b90b74cb727f2a1405",
"rev": "9813adc7f7c0edd738c6bdd8431439688bb0cb3d",
"type": "github"
},
"original": {
@@ -84,11 +69,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1671883564,
"narHash": "sha256-C15oAtyupmLB3coZY7qzEHXjhtUx/+77olVdqVMruAg=",
"lastModified": 1672844754,
"narHash": "sha256-o26WabuHABQsaHxxmIrR3AQRqDFUEdLckLXkVCpIjSU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "dac57a4eccf1442e8bf4030df6fcbb55883cb682",
"rev": "e9ade2c8240e00a4784fac282a502efff2786bdc",
"type": "github"
},
"original": {
@@ -99,11 +84,11 @@
},
"nixpkgs-stable_2": {
"locked": {
"lastModified": 1671923641,
"narHash": "sha256-flPauiL5UrfRJD+1oAcEefpEIUqTqnyKScWe/UUU+lE=",
"lastModified": 1672500394,
"narHash": "sha256-yzwBzCoeRBoRzm7ySHhm72kBG0QjgFalLz2FY48iLI4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "939c05a176b8485971463c18c44f48e56a7801c9",
"rev": "feda52be1d59f13b9aa02f064b4f14784b9a06c8",
"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",
@@ -132,11 +116,11 @@
"nixpkgs-stable": "nixpkgs-stable_2"
},
"locked": {
"lastModified": 1671937829,
"narHash": "sha256-YtaNB+mLw0d67JFYNjRWM+/AL3JCXuD/DGlnTlyX1tY=",
"lastModified": 1672543202,
"narHash": "sha256-nlCUtcIZxaBqUBG1GyaXhZmfyG5WK4e6LqypP8llX9E=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "855b8d51fc3991bd817978f0f093aa6ae0fae738",
"rev": "b35586cc5abacd4eba9ead138b53e2a60920f781",
"type": "github"
},
"original": {

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 = [
@@ -77,10 +73,6 @@
# gocryptfs = cross.gocryptfs;
# pinned packages:
# 2022/12/13: grpc does not build on aarch64-linux. https://github.com/NixOS/nixpkgs/issues/205887
grpc = stable.grpc;
# depends on grpc, so pinned.
duplicity = stable.duplicity;
})
];
}

View File

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

View File

@@ -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.
@@ -54,46 +57,68 @@ in
shell = pkgs.zsh;
openssh.authorizedKeys.keys = builtins.attrValues (import ../../modules/pubkeys.nix).users;
# mount encrypted stuff at login
# some other nix pam users:
# - <https://github.com/g00pix/nixconf/blob/32c04f6fa843fed97639dd3f09e157668d3eea1f/profiles/sshfs.nix>
# - <https://github.com/lourkeur/distro/blob/11173454c6bb50f7ccab28cc2c757dca21446d1d/nixos/profiles/users/louis-full.nix>
# - <https://github.com/dnr/sample-nix-code/blob/03494480c1fae550c033aa54fd96aeb3827761c5/nixos/laptop.nix>
pamMount = {
# mount encrypted stuff at login
# requires that login password == fs encryption password
fstype = "fuse";
path = "gocryptfs#/nix/persist/home/colin/private";
# path = "${pkgs.gocryptfs}/bin/gocryptfs#/nix/persist/home/colin/private";
# fstype = "fuse.gocryptfs";
# path = "/nix/persist/home/colin/private";
mountpoint = "/home/colin/private";
# without allow_other, *root* isn't allowed to list anything in ~/private.
# which is weird (root can just `su colin`), but probably doesn't *hurt* anything -- right?
options="nodev,nosuid,quiet"; # allow_other
pamMount = let
priv = config.fileSystems."/home/colin/private";
in {
fstype = priv.fsType;
path = priv.device;
mountpoint = priv.mountPoint;
options = builtins.concatStringsSep "," priv.options;
};
};
# required for PAM to find gocryptfs
security.pam.mount.additionalSearchPaths = [ pkgs.gocryptfs ];
security.pam.mount.enable = true;
# security.pam.mount.debugLevel = 1;
# security.pam.enableSSHAgentAuth = true; # ??
# needed for `allow_other` in e.g. gocryptfs mounts
# or i guess going through mount.fuse sets suid so that's not necessary?
# programs.fuse.userAllowOther = true;
sane.impermanence.home-dirs = [
# cache is probably too big to fit on the tmpfs
# { directory = ".cache"; encryptedClearOnBoot = true; }
{ directory = ".cache/mozilla"; encryptedClearOnBoot = 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"
];
# 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"
config.sane.web-browser.cacheDir
];
sane.impermanence.dirs = mkIf cfg.guest.enable [
{ user = "guest"; group = "users"; directory = "/home/guest"; }
# 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 [
# 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

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

View File

@@ -1,4 +1,4 @@
{ config, pkgs, lib, mobile-nixos, ... }:
{ config, pkgs, lib, ... }:
{
imports = [
./firmware.nix
@@ -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,27 +36,31 @@
];
};
sane.impermanence.dirs = [
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
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

@@ -19,7 +19,7 @@
# XXX: avatar support works in MUCs but not DMs
# lib.mkIf false
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "ejabberd"; group = "ejabberd"; directory = "/var/lib/ejabberd"; }
];
networking.firewall.allowedTCPPorts = [
@@ -75,33 +75,33 @@
sane.services.trust-dns.zones."uninsane.org".inet = {
# XXX: SRV records have to point to something with a A/AAAA record; no CNAMEs
A."xmpp" = [ "%NATIVE%" ];
CNAME."muc.xmpp" = [ "xmpp" ];
CNAME."pubsub.xmpp" = [ "xmpp" ];
CNAME."upload.xmpp" = [ "xmpp" ];
CNAME."vjid.xmpp" = [ "xmpp" ];
A."xmpp" = "%NATIVE%";
CNAME."muc.xmpp" = "xmpp";
CNAME."pubsub.xmpp" = "xmpp";
CNAME."upload.xmpp" = "xmpp";
CNAME."vjid.xmpp" = "xmpp";
# _Service._Proto.Name TTL Class SRV Priority Weight Port Target
# - <https://xmpp.org/extensions/xep-0368.html>
# something's requesting the SRV records for muc.xmpp, so let's include it
# nothing seems to request XMPP SRVs for the other records (except @)
# lower numerical priority field tells clients to prefer this method
SRV."_xmpps-client._tcp.muc.xmpp" = [ "3 50 5223 xmpp" ];
SRV."_xmpps-server._tcp.muc.xmpp" = [ "3 50 5270 xmpp" ];
SRV."_xmpp-client._tcp.muc.xmpp" = [ "5 50 5222 xmpp" ];
SRV."_xmpp-server._tcp.muc.xmpp" = [ "5 50 5269 xmpp" ];
SRV."_xmpps-client._tcp.muc.xmpp" = "3 50 5223 xmpp";
SRV."_xmpps-server._tcp.muc.xmpp" = "3 50 5270 xmpp";
SRV."_xmpp-client._tcp.muc.xmpp" = "5 50 5222 xmpp";
SRV."_xmpp-server._tcp.muc.xmpp" = "5 50 5269 xmpp";
SRV."_xmpps-client._tcp" = [ "3 50 5223 xmpp" ];
SRV."_xmpps-server._tcp" = [ "3 50 5270 xmpp" ];
SRV."_xmpp-client._tcp" = [ "5 50 5222 xmpp" ];
SRV."_xmpp-server._tcp" = [ "5 50 5269 xmpp" ];
SRV."_xmpps-client._tcp" = "3 50 5223 xmpp";
SRV."_xmpps-server._tcp" = "3 50 5270 xmpp";
SRV."_xmpp-client._tcp" = "5 50 5222 xmpp";
SRV."_xmpp-server._tcp" = "5 50 5269 xmpp";
SRV."_stun._udp" = [ "5 50 3478 xmpp" ];
SRV."_stun._tcp" = [ "5 50 3478 xmpp" ];
SRV."_stuns._tcp" = [ "5 50 5349 xmpp" ];
SRV."_turn._udp" = [ "5 50 3478 xmpp" ];
SRV."_turn._tcp" = [ "5 50 3478 xmpp" ];
SRV."_turns._tcp" = [ "5 50 5349 xmpp" ];
SRV."_stun._udp" = "5 50 3478 xmpp";
SRV."_stun._tcp" = "5 50 3478 xmpp";
SRV."_stuns._tcp" = "5 50 5349 xmpp";
SRV."_turn._udp" = "5 50 3478 xmpp";
SRV."_turn._tcp" = "5 50 3478 xmpp";
SRV."_turns._tcp" = "5 50 5349 xmpp";
};
# TODO: allocate UIDs/GIDs ?

View File

@@ -14,9 +14,9 @@
sops.secrets.freshrss_passwd = {
sopsFile = ../../../secrets/servo.yaml;
owner = config.users.users.freshrss.name;
mode = "400";
mode = "0400";
};
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "freshrss"; group = "freshrss"; directory = "/var/lib/freshrss"; }
];
@@ -57,5 +57,5 @@
# the routing is handled by services.freshrss.virtualHost
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."rss" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."rss" = "native";
}

View File

@@ -1,7 +1,7 @@
{ config, pkgs, lib, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "git"; group = "gitea"; directory = "/var/lib/gitea"; }
];
@@ -85,5 +85,5 @@
};
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."git" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."git" = "native";
}

View File

@@ -64,5 +64,5 @@
};
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."sink" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."sink" = "native";
}

View File

@@ -10,7 +10,7 @@
lib.mkIf false # i don't actively use ipfs anymore
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "261"; group = "261"; directory = "/var/lib/ipfs"; }
];
@@ -34,7 +34,7 @@ lib.mkIf false # i don't actively use ipfs anymore
};
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."ipfs" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."ipfs" = "native";
# services.ipfs.enable = true;
services.kubo.localDiscovery = true;

View File

@@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? we only need this to save Indexer creds ==> migrate to config?
{ user = "root"; group = "root"; directory = "/var/lib/jackett"; }
];
@@ -27,6 +27,6 @@
};
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."jackett" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."jackett" = "native";
}

View File

@@ -7,7 +7,7 @@ lib.mkIf false
networking.firewall.allowedUDPPorts = [
1900 7359 # DLNA: https://jellyfin.org/docs/general/networking/index.html
];
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "jellyfin"; group = "jellyfin"; directory = "/var/lib/jellyfin"; }
];
@@ -61,7 +61,7 @@ lib.mkIf false
};
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."jelly" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."jelly" = "native";
# users.users.jellyfin.uid = config.sane.allocations.jellyfin-uid;
# users.groups.jellyfin.gid = config.sane.allocations.jellyfin-gid;

View File

@@ -13,5 +13,5 @@
locations."/".proxyPass = "http://127.0.0.1:8013";
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."w" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."w" = "native";
}

View File

@@ -8,7 +8,7 @@
# ./irc.nix
];
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "matrix-synapse"; group = "matrix-synapse"; directory = "/var/lib/matrix-synapse"; }
];
services.matrix-synapse.enable = true;
@@ -122,8 +122,8 @@
};
sane.services.trust-dns.zones."uninsane.org".inet = {
CNAME."matrix" = [ "native" ];
CNAME."web.matrix" = [ "native" ];
CNAME."matrix" = "native";
CNAME."web.matrix" = "native";
};

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
{ ... }:
{
sane.impermanence.dirs = [
{ user = "navidrome"; group = "navidrome"; directory = "/var/lib/private/navidrome"; }
sane.impermanence.dirs.sys.plaintext = [
# 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 = {
@@ -22,5 +25,5 @@
locations."/".proxyPass = "http://127.0.0.1:4533";
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."music" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."music" = "native";
}

View File

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

View File

@@ -14,7 +14,7 @@
'';
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."nixcache" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."nixcache" = "native";
sane.services.nixserve.enable = true;
sane.services.nixserve.sopsFile = ../../../secrets/servo.yaml;

View File

@@ -6,7 +6,7 @@
{ config, pkgs, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "pleroma"; group = "pleroma"; directory = "/var/lib/pleroma"; }
];
@@ -179,7 +179,7 @@
};
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."fed" = [ "native" ];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."fed" = "native";
sops.secrets.pleroma_secrets = {
sopsFile = ../../../secrets/servo.yaml;

View File

@@ -16,7 +16,7 @@ let
};
in
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "opendkim"; group = "opendkim"; directory = "/var/lib/opendkim"; }
{ user = "root"; group = "root"; directory = "/var/lib/postfix"; }
@@ -45,22 +45,22 @@ in
sane.services.trust-dns.zones."uninsane.org".inet = {
MX."@" = [ "10 mx.uninsane.org." ];
MX."@" = "10 mx.uninsane.org.";
# XXX: RFC's specify that the MX record CANNOT BE A CNAME
A."mx" = [ "185.157.162.178" ];
CNAME."imap" = [ "native" ];
A."mx" = "185.157.162.178";
CNAME."imap" = "native";
# Sender Policy Framework:
# +mx => mail passes if it originated from the MX
# +a => mail passes if it originated from the A address of this domain
# +ip4:.. => mail passes if it originated from this IP
# -all => mail fails if none of these conditions were met
TXT."@" = [ "v=spf1 a mx -all" ];
TXT."@" = "v=spf1 a mx -all";
# DKIM public key:
TXT."mx._domainkey" = [
TXT."mx._domainkey" =
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkSyMufc2KrRx3j17e/LyB+3eYSBRuEFT8PUka8EDX04QzCwDPdkwgnj3GNDvnB5Ktb05Cf2SJ/S1OLqNsINxJRWtkVfZd/C339KNh9wrukMKRKNELL9HLUw0bczOI4gKKFqyrRE9qm+4csCMAR79Te9FCjGV/jVnrkLdPT0GtFwIDAQAB"
];
;
# DMARC fields <https://datatracker.ietf.org/doc/html/rfc7489>:
# p=none|quarantine|reject: what to do with failures
@@ -75,9 +75,9 @@ in
# pct = sampling ratio for punishing failures (default 100 for 100%)
# rf = report format
# ri = report interval
TXT."_dmarc" = [
TXT."_dmarc" =
"v=DMARC1;p=quarantine;sp=reject;rua=mailto:admin+mail@uninsane.org;ruf=mailto:admin+mail@uninsane.org;fo=1:d:s"
];
;
};
services.postfix.enable = true;

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{ pkgs, ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
# TODO: mode? we need this specifically for the stats tracking in .config/
{ user = "transmission"; group = "transmission"; directory = "/var/lib/transmission"; }
];
@@ -75,6 +75,6 @@
};
};
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."bt" = ["native"];
sane.services.trust-dns.zones."uninsane.org".inet.CNAME."bt" = "native";
}

View File

@@ -21,25 +21,25 @@
# Retry = how long secondary NS should wait until re-querying master after a failure (must be < Refresh)
# Expire = how long secondary NS should continue to reply to queries after master fails (> Refresh + Retry)
sane.services.trust-dns.zones."uninsane.org".inet = {
SOA."@" = [''
SOA."@" = ''
ns1.uninsane.org. admin-dns.uninsane.org. (
2022122101 ; Serial
4h ; Refresh
30m ; Retry
7d ; Expire
5m) ; Negative response TTL
''];
TXT."rev" = [ "2022122101" ];
'';
TXT."rev" = "2022122101";
# XXX NS records must also not be CNAME
# it's best that we keep this identical, or a superset of, what org. lists as our NS.
# so, org. can specify ns2/ns3 as being to the VPN, with no mention of ns1. we provide ns1 here.
A."ns1" = [ "%NATIVE%" ];
A."ns2" = [ "185.157.162.178" ];
A."ns3" = [ "185.157.162.178" ];
A."ovpns" = [ "185.157.162.178" ];
A."native" = [ "%NATIVE%" ];
A."@" = [ "%NATIVE%" ];
A."ns1" = "%NATIVE%";
A."ns2" = "185.157.162.178";
A."ns3" = "185.157.162.178";
A."ovpns" = "185.157.162.178";
A."native" = "%NATIVE%";
A."@" = "%NATIVE%";
NS."@" = [
"ns1.uninsane.org."
"ns2.uninsane.org."

View File

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

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

@@ -9,11 +9,8 @@
with lib;
let
cfg = config.sane.home-manager;
# extract package from `sane.packages.enabledUserPkgs`
pkg-list = pkgspec: builtins.map (e: e.pkg or e) pkgspec;
# extract `dir` from `sane.packages.enabledUserPkgs`
dir-list = pkgspec: builtins.concatLists (builtins.map (e: e.dir or []) pkgspec);
private-list = pkgspec: builtins.concatLists (builtins.map (e: e.private or []) pkgspec);
# extract `pkg` from `sane.packages.enabledUserPkgs`
pkg-list = pkgspec: builtins.map (e: e.pkg) pkgspec;
feeds = import ./feeds.nix { inherit lib; };
in
{
@@ -51,18 +48,6 @@ in
};
config = lib.mkIf cfg.enable {
sane.impermanence.home-dirs = [
"archive"
"dev"
"records"
"ref"
"tmp"
"use"
"Music"
"Pictures"
"Videos"
] ++ (dir-list config.sane.packages.enabledUserPkgs);
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
@@ -90,27 +75,6 @@ in
};
};
home.file = let
privates = builtins.listToAttrs (
builtins.map (path: {
name = path;
value = { source = config.lib.file.mkOutOfStoreSymlink "/home/colin/private/${path}"; };
})
(private-list sysconfig.sane.packages.enabledUserPkgs)
);
in {
# 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";
} // privates;
# 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,7 +2,8 @@
lib.mkIf config.sane.home-manager.enable
{
sane.impermanence.home-dirs = [ ".cache/vim-swap" ];
# private because there could be sensitive things in the swap
sane.impermanence.dirs.home.private = [ ".cache/vim-swap" ];
home-manager.users.colin.programs.neovim = {
# neovim: https://github.com/neovim/neovim

View File

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

View File

@@ -1,4 +1,5 @@
{ config, lib, pkgs, mobile-nixos, utils, ... }:
{ config, lib, pkgs, utils, ... }:
# TODO: replace mobile-nixos parts with Disko <https://github.com/nix-community/disko>
with lib;
let
@@ -9,7 +10,7 @@ in
sane.image.enable = mkOption {
default = true;
type = types.bool;
description = "whether to enable image targets. this doesn't mean they'll be built unless you specifically reference the target.";
description = "whether to enable image targets. even so they won't be built unless you specifically reference the `system.build.img` target.";
};
# packages whose contents should be copied directly into the /boot partition.
# e.g. EFI loaders, u-boot bootloader, etc.

View File

@@ -2,139 +2,132 @@
# 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;
# taken from sops-nix code: checks if any secrets are needed to create /etc/shadow
secrets-for-users = (lib.filterAttrs (_: v: v.neededForUsers) config.sops.secrets) != {};
getStore = { encryptedClearOnBoot, ... }: (
if encryptedClearOnBoot then {
device = "/mnt/impermanence/crypt/clearedonboot";
underlying = {
path = "/nix/persist/crypt/clearedonboot";
# TODO: consider moving this to /tmp, but that requires tmp be mounted first?
type = "gocryptfs";
key = "/mnt/impermanence/crypt/clearedonboot.key";
};
} else {
device = "/nix/persist";
# device = "/mnt/impermenanence/persist/plain";
# underlying = {
# path = "/nix/persist";
# type = "bind";
# };
}
);
home-dir-defaults = {
user = "colin";
group = "users";
mode = "0755";
relativeTo = "/home/colin";
};
sys-dir-defaults = {
user = "root";
group = "root";
mode = "0755";
relativeTo = "";
};
# turn a path into a name suitable for systemd
cleanName = utils.escapeSystemdPath;
# split the string path into a list of string components.
# root directory "/" becomes the empty list [].
# implicitly performs normalization so that:
# splitPath "a//b/" => ["a" "b"]
# splitPath "/a/b" => ["a" "b"]
splitPath = str: builtins.filter (seg: (builtins.isString seg) && seg != "" ) (builtins.split "/" str);
# return a string path, with leading slash but no trailing slash
joinPathAbs = comps: "/" + (builtins.concatStringsSep "/" comps);
concatPaths = paths: joinPathAbs (builtins.concatLists (builtins.map (p: splitPath p) paths));
# normalize the given path
normPath = str: joinPathAbs (splitPath str);
# return the parent directory. doesn't care about leading/trailing slashes.
parentDir = str: normPath (builtins.dirOf (normPath str));
dirOptions = defaults: types.submodule {
storeType = types.submodule {
options = {
encryptedClearOnBoot = mkOption {
default = false;
type = types.bool;
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".
'';
};
directory = mkOption {
origin = mkOption {
type = types.str;
};
user = mkOption {
prefix = mkOption {
type = types.str;
default = defaults.user;
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.
'';
};
group = mkOption {
type = types.str;
default = defaults.group;
defaultOrdering.wantedBeforeBy = mkOption {
type = types.listOf types.str;
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.
'';
};
mode = mkOption {
type = types.str;
default = defaults.mode;
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.
'';
};
};
};
mkDirsOption = defaults: mkOption {
default = [];
type = types.listOf (types.coercedTo types.str (d: { directory = d; }) (dirOptions defaults));
# apply = map (d: if isString d then { directory = d; } else d);
# options for a single mountpoint / persistence
dirEntryOptions = {
options = {
directory = mkOption {
type = types.str;
};
inherit (sane-types.aclOverrideMod.options) user group mode;
};
};
contextualizedDir = types.submodule dirEntryOptions;
# allow "bar/baz" as shorthand for { directory = "bar/baz"; }
contextualizedDirOrShorthand = types.coercedTo
types.str
(d: { directory = d; })
contextualizedDir;
# entry whose `directory` is always an absolute fs path
# and has an associated `store`
contextFreeDir = types.submodule [
dirEntryOptions
{
options = {
store = mkOption {
type = storeType;
};
};
}
];
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;
};
# expand user options with more context
ingestDirOption = defaults: opt: {
inherit (opt) user group mode;
directory = concatPaths [ defaults.relativeTo opt.directory ];
## helpful context
store = builtins.addErrorContext ''while ingestDirOption on ${opt.directory} with attrs ${builtins.concatStringsSep " " (attrNames opt)}''
(getStore opt);
};
ingestDirOptions = defaults: opts: builtins.map (ingestDirOption defaults) opts;
ingested-home-dirs = ingestDirOptions home-dir-defaults cfg.home-dirs;
ingested-sys-dirs = ingestDirOptions sys-dir-defaults cfg.dirs;
ingested-dirs = ingested-home-dirs ++ ingested-sys-dirs;
# include these anchor points as "virtual" nodes in below fs tree.
home-dir = {
inherit (home-dir-defaults) user group mode;
directory = normPath home-dir-defaults.relativeTo;
};
root-dir = {
inherit (sys-dir-defaults) user group mode;
directory = normPath sys-dir-defaults.relativeTo;
};
unexpanded-tree = builtins.listToAttrs (builtins.map
(dir: {
name = dir.directory;
value = dir;
})
(ingested-dirs ++ [ home-dir root-dir ])
);
# ensures the provided node and all parent nodes exist
ensureNode = tree: path: (
let
parent-path = parentDir path;
tree-with-parent = if parent-path == "/"
then tree
else ensureNode tree parent-path;
parent = tree-with-parent."${parent-path}";
# how to initialize this node if it doesn't exist explicitly.
default-node = parent // { directory = path; };
in
{ "${path}" = default-node; } // tree-with-parent
);
# finally, this tree has no orphan nodes
expanded-tree = foldl' ensureNode unexpanded-tree (builtins.attrNames unexpanded-tree);
dirsModule = types.submodule ({ config, ... }: {
options = {
home = mkOption {
description = "directories to persist to disk, relative to a user's home ~";
default = {};
type = dirsSubModule;
};
sys = mkOption {
description = "directories to persist to disk, relative to the fs root /";
default = {};
type = dirsSubModule;
};
all = mkOption {
type = types.listOf contextFreeDir;
description = "all directories known to the config. auto-computed: users should not set this directly.";
};
};
config = let
mapDirs = relativeTo: store: dirs: (map
(d: {
inherit (d) user group mode;
directory = 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 = (mapDirSets "/home/colin" config.home) ++ (mapDirSets "/" config.sys);
};
});
in
{
options = {
@@ -145,182 +138,54 @@ in
sane.impermanence.root-on-tmpfs = mkOption {
default = false;
type = types.bool;
description = "define / to be a tmpfs. make sure to mount some other device to /nix";
description = "define / fs root to be a tmpfs. make sure to mount some other device to /nix";
};
sane.impermanence.dirs = mkOption {
type = dirsModule;
default = {};
};
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.
'';
};
sane.impermanence.home-dirs = mkDirsOption home-dir-defaults;
sane.impermanence.dirs = mkDirsOption sys-dir-defaults;
};
config = mkIf cfg.enable (lib.mkMerge [
(lib.mkIf cfg.root-on-tmpfs {
fileSystems."/" = {
device = "none";
fsType = "tmpfs";
options = [
"mode=755"
"size=1G"
"defaults"
];
};
})
imports = [
./root-on-tmpfs.nix
./stores
];
{
# without this, we get `fusermount: fuse device not found, try 'modprobe fuse' first`.
# - that only happens after a activation-via-boot -- not activation-after-rebuild-switch.
# it seems likely that systemd loads `fuse` by default. see:
# - </etc/systemd/system/sysinit.target.wants/sys-fs-fuse-connections.mount>
# - triggers: /etc/systemd/system/modprobe@.service
# - calls `modprobe`
# note: even `boot.kernelModules = ...` isn't enough: that option creates /etc/modules-load.d/, which is ingested only by systemd.
# note: `boot.initrd.availableKernelModules` ALSO isn't enough: idk why.
# TODO: might not be necessary now we're using fileSystems and systemd
boot.initrd.kernelModules = [ "fuse" ];
# TODO: convert this to a systemd unit file?
system.activationScripts.prepareEncryptedClearedOnBoot =
config = let
cfgFor = opt:
let
script = pkgs.writeShellApplication {
name = "prepareEncryptedClearedOnBoot";
runtimeInputs = with pkgs; [ gocryptfs ];
text = ''
backing="$1"
passfile="$2"
if ! test -e "$passfile"
then
tmpdir=$(dirname "$passfile")
mkdir -p "$backing" "$tmpdir"
# if the key doesn't exist, it's probably not mounted => delete the backing dir
rm -rf "''${backing:?}"/*
# generate key. we can "safely" keep it around for the lifetime of this boot
dd if=/dev/random bs=128 count=1 | base64 --wrap=0 > "$passfile"
# initialize the crypt store
gocryptfs -quiet -passfile "$passfile" -init "$backing"
fi
'';
};
store = getStore { encryptedClearOnBoot = true; };
in {
text = ''${script}/bin/prepareEncryptedClearedOnBoot ${store.underlying.path} ${store.underlying.key}'';
};
store = opt.store;
store-rel-path = path.from store.prefix opt.directory;
backing-path = path.concat [ store.origin store-rel-path ];
fileSystems = let
store = getStore { encryptedClearOnBoot = true; };
# pass through the perm/mode overrides
dir-acl = sane-lib.filterNonNull {
inherit (opt) user group mode;
};
in {
"${store.device}" = {
device = store.underlying.path;
fsType = "fuse.gocryptfs";
options = [
"nodev"
"nosuid"
"allow_other"
"passfile=${store.underlying.key}"
"defaults"
];
noCheck = true;
# 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;
};
};
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
}
(
let cfgFor = opt:
let
# systemd creates <path>.mount services for every fileSystems entry.
# <path> gets escaped as part of that: this code tries to guess that escaped name here.
backing-mount = cleanName opt.store.device;
mount-service = cleanName opt.directory;
perms-service = "impermanence-perms-${mount-service}";
parent-mount-service = cleanName (parentDir opt.directory);
parent-perms-service = "impermanence-perms-${parent-mount-service}";
is-mount = opt ? store;
backing-path = if is-mount then
concatPaths [ opt.store.device opt.directory ]
else
opt.directory;
in {
fileSystems."${opt.directory}" = lib.mkIf is-mount {
device = concatPaths [ opt.store.device opt.directory ];
options = [
"bind"
# "x-systemd.requires=${backing-mount}.mount" # this should be implicit
"x-systemd.after=${perms-service}.service"
# `wants` doesn't seem to make it to the service file here :-(
"x-systemd.wants=${perms-service}.service"
];
# fsType = "bind";
noCheck = true;
};
# create services which ensure the source directories exist and have correct ownership/perms before mounting
systemd.services."${perms-service}" = let
perms-script = pkgs.writeShellScript "impermanence-prepare-perms" ''
backing="$1"
path="$2"
user="$3"
group="$4"
mode="$5"
mkdir "$path" || test -d "$path"
chmod "$mode" "$path"
chown "$user:$group" "$path"
# XXX: fix up the permissions of the origin, otherwise it overwrites the mountpoint with defaults.
# TODO: apply to the full $backing path? like, construct it entirely in parallel?
if [ "$backing" != "$path" ]
then
mkdir -p "$backing"
chmod "$mode" "$backing"
chown "$user:$group" "$backing"
fi
'';
in {
description = "prepare permissions for ${opt.directory}";
serviceConfig = {
ExecStart = ''${perms-script} ${backing-path} ${opt.directory} ${opt.user} ${opt.group} ${opt.mode}'';
Type = "oneshot";
};
unitConfig = {
# prevent systemd making this unit implicitly dependent on sysinit.target.
# see: <https://www.freedesktop.org/software/systemd/man/systemd.special.html>
DefaultDependencies = "no";
};
wantedBy = lib.mkIf is-mount [ "${mount-service}.mount" ];
after = lib.mkIf (opt.directory != "/") [ "${parent-perms-service}.service" ];
wants = lib.mkIf (opt.directory != "/") [ "${parent-perms-service}.service" ];
};
};
cfgs = builtins.map cfgFor (builtins.attrValues expanded-tree);
# cfgs = builtins.map cfgFor ingested-dirs;
# cfgs = [ (cfgFor (ingestDirOption home-dir-defaults ".cache")) ];
# myMerge = items: builtins.foldl' (acc: new: acc // new) {} items;
in {
# fileSystems = myMerge (catAttrs "fileSystems" cfgs);
fileSystems = lib.mkMerge (builtins.catAttrs "fileSystems" cfgs);
systemd = lib.mkMerge (catAttrs "systemd" cfgs);
}
)
(lib.mkIf secrets-for-users {
# secret decoding depends on /etc/ssh keys, so make sure those are present.
system.activationScripts.setupSecretsForUsers = lib.mkIf secrets-for-users {
deps = [ "etc" ];
};
system.activationScripts.etc.deps = lib.mkForce [];
assertions = builtins.concatLists (builtins.attrValues (
builtins.mapAttrs
(path: value: [
{
assertion = (builtins.substring 0 1 value.user) == "+";
message = "non-numeric user for /etc/${path}: ${value.user} prevents early /etc linking";
}
{
assertion = (builtins.substring 0 1 value.group) == "+";
message = "non-numeric group for /etc/${path}: ${value.group} prevents early /etc linking";
}
])
config.environment.etc
));
})
]);
in mkIf cfg.enable {
sane.fs = lib.mkMerge (map (d: (cfgFor d).sane.fs) cfg.dirs.all);
};
}

View File

@@ -0,0 +1,16 @@
{ config, lib, ... }:
let
cfg = config.sane.impermanence;
in
{
fileSystems."/" = lib.mkIf (cfg.enable && cfg.root-on-tmpfs) {
device = "none";
fsType = "tmpfs";
options = [
"mode=755"
"size=1G"
"defaults"
];
};
}

View File

@@ -0,0 +1,74 @@
{ config, lib, pkgs, utils, ... }:
let
store = rec {
device = "/mnt/impermanence/crypt/clearedonboot";
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";
};
};
in
lib.mkIf config.sane.impermanence.enable
{
sane.impermanence.stores."cryptClearOnBoot" = {
storeDescription = ''
stored to disk, but encrypted to an in-memory key and cleared on every boot
so that it's unreadable after power-off
'';
origin = store.device;
};
fileSystems."${store.device}" = {
device = store.underlying.path;
fsType = "fuse.gocryptfs";
options = [
"nodev"
"nosuid"
"allow_other"
"passfile=${store.underlying.key}"
"defaults"
];
noCheck = true;
};
# 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 ];
};
# 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?
system.fsPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
}

View File

@@ -0,0 +1,31 @@
{ 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 {
# 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

@@ -0,0 +1,48 @@
{ config, lib, pkgs, utils, ... }:
lib.mkIf config.sane.impermanence.enable
{
sane.impermanence.stores."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";
defaultOrdering = let
private-unit = config.sane.fs."/home/colin/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" = {
device = "/nix/persist/home/colin/private";
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"
"quiet"
"defaults"
];
noCheck = true;
};
# 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?
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

@@ -85,7 +85,7 @@ let
# XXX by default fractal stores its state in ~/.local/share/<UUID>.
# after logging in, manually change ~/.local/share/keyrings/... to point it to some predictable subdir.
# then reboot (so that libsecret daemon re-loads the keyring...?)
{ pkg = fractal-latest; private = [ ".local/share/fractal" ]; }
# { pkg = fractal-latest; private = [ ".local/share/fractal" ]; }
# { pkg = fractal-next; private = [ ".local/share/fractal" ]; }
gajim # XMPP client
@@ -144,7 +144,7 @@ let
# possible to pass config as a CLI arg (sublime-music -c config.json)
# { pkg = sublime-music; dir = [ ".local/share/sublime-music" ]; }
{ pkg = sublime-music-mobile; dir = [ ".local/share/sublime-music" ]; }
tdesktop # broken on phosh
{ pkg = tdesktop; private = [ ".local/share/TelegramDesktop" ]; } # broken on phosh
{ pkg = tokodon; private = [ ".cache/KDE/tokodon" ]; }
@@ -300,13 +300,15 @@ in
++ (if cfg.enableGuiPkgs then guiPkgs else [])
++ (if cfg.enableDevPkgs then devPkgs else [])
;
type = types.listOf (types.either types.package types.attrs);
type = types.listOf (types.coercedTo types.package (p: { pkg = p; }) pkgSpec);
description = "generated from other config options";
};
};
config = {
environment.systemPackages = mkIf cfg.enableSystemPkgs systemPkgs;
sane.impermanence.dirs.home.plaintext = concatLists (map (p: p.dir) cfg.enabledUserPkgs);
sane.impermanence.dirs.home.private = concatLists (map (p: p.private) cfg.enabledUserPkgs);
# XXX: this might not be necessary. try removing this and cacert.unbundled?
environment.etc."ssl/certs".source = mkIf cfg.enableSystemPkgs "${pkgs.cacert.unbundled}/etc/ssl/certs/*";
};

View File

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

View File

@@ -1,5 +1,8 @@
{ config, lib, pkgs, ... }:
# TODO: consider using this library for .zone file generation:
# - <https://github.com/kirelagin/dns.nix>
with lib;
let
cfg = config.sane.services.trust-dns;
@@ -49,6 +52,13 @@ let
}) cfg.zones
);
};
# (listOf ty) type which also accepts single-assignment of `ty`.
# it's used to allow the user to write:
# CNAME."foo" = "bar";
# as shorthand for
# CNAME."foo" = [ "bar" ];
listOrUnit = ty: types.coercedTo ty (elem: [ elem ]) (types.listOf ty);
in
{
options = {
@@ -88,37 +98,37 @@ in
};
inet = {
SOA = mkOption {
type = types.attrsOf (types.listOf types.str);
type = types.attrsOf (listOrUnit types.str);
description = "Start of Authority record(s)";
default = {};
};
A = mkOption {
type = types.attrsOf (types.listOf types.str);
type = types.attrsOf (listOrUnit types.str);
description = "IPv4 address record(s)";
default = {};
};
CNAME = mkOption {
type = types.attrsOf (types.listOf types.str);
type = types.attrsOf (listOrUnit types.str);
description = "canonical name record(s)";
default = {};
};
MX = mkOption {
type = types.attrsOf (types.listOf types.str);
type = types.attrsOf (listOrUnit types.str);
description = "mail exchanger record(s)";
default = {};
};
NS = mkOption {
type = types.attrsOf (types.listOf types.str);
type = types.attrsOf (listOrUnit types.str);
description = "name server record(s)";
default = {};
};
SRV = mkOption {
type = types.attrsOf (types.listOf types.str);
type = types.attrsOf (listOrUnit types.str);
description = "service record(s)";
default = {};
};
TXT = mkOption {
type = types.attrsOf (types.listOf types.str);
type = types.attrsOf (listOrUnit types.str);
description = "text record(s)";
default = {};
};

32
modules/sops.nix Normal file
View File

@@ -0,0 +1,32 @@
{ config, lib, ... }:
let
# taken from sops-nix code: checks if any secrets are needed to create /etc/shadow
secrets-for-users = (lib.filterAttrs (_: v: v.neededForUsers) config.sops.secrets) != {};
sops-files = config.sops.age.sshKeyPaths ++ config.sops.gnupg.sshKeyPaths ++ [ config.sops.age.keyFile ];
keys-in-etc = builtins.any (p: builtins.substring 0 5 p == "/etc/") sops-files;
in
{
config = lib.mkIf (secrets-for-users && keys-in-etc) {
# secret decoding depends on keys in /etc/ (like the ssh host key), so make sure those are present.
system.activationScripts.setupSecretsForUsers = lib.mkIf secrets-for-users {
deps = [ "etc" ];
};
# TODO: we should selectively remove "users" and "groups", but keep manually specified deps?
system.activationScripts.etc.deps = lib.mkForce [];
assertions = builtins.concatLists (builtins.attrValues (
builtins.mapAttrs
(path: value: [
{
assertion = (builtins.substring 0 1 value.user) == "+";
message = "non-numeric user for /etc/${path}: ${value.user} prevents early /etc linking";
}
{
assertion = (builtins.substring 0 1 value.group) == "+";
message = "non-numeric group for /etc/${path}: ${value.group} prevents early /etc linking";
}
])
config.environment.etc
));
};
}

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

View File

@@ -1,5 +1,5 @@
{
"data": "ENC[AES256_GCM,data:6DbXAd9wFIdEBBdiesGiJ8ddyQ5p65XpnitIqItIBcR6taZ20HwrwAmCmDbsxPJ0FSDUnIzzsEdN3ad44e4tQW/o8iLNqRBMMB2rXLJyOiOFDg==,iv:ocfbDt0nLB+1CGSMh82XzLZEDHV3tZD6qCKDR//nIk8=,tag:S2hJR3rK2G6WJCQTBO61sw==,type:str]",
"data": "ENC[AES256_GCM,data:xyD4tqHo7IUxPvJnZi0tiFXeTXVCnFJlCTtz1YUxcDh6pXYhUmsxudDM9/V/1FsUQHCrq/TtccdjPrPWChv+ty1/dIdUeGNyEZ73nOUamahmvfEtvXuTP0KOLy68BQHRImkomXlqaYRamEyPkMwaqUABQ3XD5UwwFZWZ+mhdbQsOPxUHpgFz2kL1nYPlueQG1XzSy+ZhWH5GPfu3GRN2XNOMeTlKhy9q,iv:yrwtQz+K9UHIvPT099uyJFrnAvfVzqYrT8mTEYUaJPQ=,tag:fYd4b7wwbfKEylISUsyajQ==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
@@ -39,8 +39,8 @@
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByTXlSVVhxczNIRGIwZEdW\nSm14aFYzTEFoSGt2SzZKc21OaVpTVmNrSXd3Cis5UTRQMzJSaVdwTkdrQmxLSlRp\nUXBGZ0huUUJnVHVHaUtyUGI4cXdrTVkKLS0tIHVWeEVsOXRRTFRZalI4bWdwcy9a\nV1EwTHhqemRFVHlZR3N4SGRibDhWZzAKVfqqfrKPWtxnIgdvgo7yTe24dleOZAIZ\nZKFCZ3NqibMaRI324E2PrJSAij0lNJyulxpLx4chA7yN84v4vuQToA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2022-12-26T09:13:29Z",
"mac": "ENC[AES256_GCM,data:/bKnjVzoiyqz+HD+rT76tQiT8+bqmJfEonFK9z+c+6uDFGCLeockZ5WIHcULU3VU1kfgmkr9R8vlArIYN5vrEm8g6jS8iQgcehjGiqbF5KQHDIarHzBJdqa3ca3G98BF3HlaMYR/hpWquR7sLBcsayf6LcHdGCqiP5TnERd0TzY=,iv:TanC7jAdbH1UXNFbNN6dAOL4hiJY1U0GRWdPmaiY/Sg=,tag:gNsXTb2BTZiOhBoQmcJVDw==,type:str]",
"lastmodified": "2023-01-02T12:37:44Z",
"mac": "ENC[AES256_GCM,data:VXycD0JG1nPGFefI6gsG2zQh7NjG+bKCyMjyfWkRJyjomJlGaLMDF/8iUAhRHGgBuAmhZuu8nyZHky8F9CEgtktpY4/b/b3eH4NVuWlQ04MrpO24RrRgwyN+WrtG4FWEnbA4QtOLu64pTMQlQgRseL30u+RNQ6eT+ycx71/6r6A=,iv:YtRe37O4Zht148zbjplIKbUfVvghYDH2ErDbKJN2qdc=,tag:AKjzatu7Iy49Dg8lkwiWpA==,type:str]",
"pgp": null,
"unencrypted_suffix": "_unencrypted",
"version": "3.7.3"