Compare commits

...

28 Commits

Author SHA1 Message Date
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
43 changed files with 758 additions and 403 deletions

24
flake.lock generated
View File

@@ -69,11 +69,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1671722432,
"narHash": "sha256-ojcZUekIQeOZkHHzR81st7qxX99dB1Eaaq6PU5MNeKc=",
"lastModified": 1672525397,
"narHash": "sha256-WASDnyxHKWVrEe0dIzkpH+jzKlCKAk0husv0f/9pyxg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "652e92b8064949a11bc193b90b74cb727f2a1405",
"rev": "8ba56d7c0d7490680f2d51ba46a141eca7c46afa",
"type": "github"
},
"original": {
@@ -84,11 +84,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1671883564,
"narHash": "sha256-C15oAtyupmLB3coZY7qzEHXjhtUx/+77olVdqVMruAg=",
"lastModified": 1672441588,
"narHash": "sha256-jx5kxOyeObnVD44HRebKYL3cjWrcKhhcDmEYm0/naDY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "dac57a4eccf1442e8bf4030df6fcbb55883cb682",
"rev": "6a0d2701705c3cf6f42c15aa92b7885f1f8a477f",
"type": "github"
},
"original": {
@@ -99,11 +99,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": {
@@ -132,11 +132,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

@@ -77,10 +77,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

@@ -54,45 +54,36 @@ 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; }
sane.impermanence.dirs.home.plaintext = [
".cargo"
".rustup"
# TODO: move this to ~/private!
".local/share/keyrings"
];
sane.impermanence.dirs.home.cryptClearOnBoot = [
# cache is probably too big to fit on the tmpfs
# ".cache"
".cache/mozilla"
];
sane.impermanence.dirs = mkIf cfg.guest.enable [
sane.impermanence.dirs.sys.plaintext = mkIf cfg.guest.enable [
{ user = "guest"; group = "users"; directory = "/home/guest"; }
];
users.users.guest = mkIf cfg.guest.enable {

View File

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

View File

@@ -1,4 +1,4 @@
{ config, pkgs, lib, mobile-nixos, ... }:
{ config, pkgs, lib, ... }:
{
imports = [
./firmware.nix

View File

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

View File

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

@@ -16,7 +16,7 @@
owner = config.users.users.freshrss.name;
mode = "400";
};
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "freshrss"; group = "freshrss"; directory = "/var/lib/freshrss"; }
];
@@ -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,7 +1,7 @@
{ ... }:
{
sane.impermanence.dirs = [
sane.impermanence.dirs.sys.plaintext = [
{ user = "navidrome"; group = "navidrome"; directory = "/var/lib/private/navidrome"; }
];
services.navidrome.enable = true;
@@ -22,5 +22,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

@@ -3,6 +3,7 @@
{
imports = [
./allocations.nix
./fs.nix
./gui
./home-manager
./packages.nix
@@ -10,5 +11,6 @@
./impermanence
./nixcache.nix
./services
./sops.nix
];
}

247
modules/fs.nix Normal file
View File

@@ -0,0 +1,247 @@
{ 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);
}

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,9 +48,10 @@ in
};
config = lib.mkIf cfg.enable {
sane.impermanence.home-dirs = [
sane.impermanence.dirs.home.plaintext = [
"archive"
"dev"
# TODO: records should be private
"records"
"ref"
"tmp"
@@ -61,7 +59,7 @@ in
"Music"
"Pictures"
"Videos"
] ++ (dir-list config.sane.packages.enabledUserPkgs);
];
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
@@ -91,15 +89,7 @@ in
};
home.file = let
privates = builtins.listToAttrs (
builtins.map (path: {
name = path;
value = { source = config.lib.file.mkOutOfStoreSymlink "/home/colin/private/${path}"; };
})
(private-list sysconfig.sane.packages.enabledUserPkgs)
);
in {
home.file = {
# convenience
"knowledge".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/private/knowledge";
"nixos".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/dev/nixos";
@@ -109,7 +99,7 @@ in
# used by password managers, e.g. unix `pass`
".password-store".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/knowledge/secrets/accounts";
} // privates;
};
# XDG defines things like ~/Desktop, ~/Downloads, etc.
# these clutter the home, so i mostly don't use them.

View File

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

View File

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

View File

@@ -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

@@ -7,41 +7,22 @@
with lib;
let
cfg = config.sane.impermanence;
# taken from sops-nix code: checks if any secrets are needed to create /etc/shadow
secrets-for-users = (lib.filterAttrs (_: v: v.neededForUsers) config.sops.secrets) != {};
getStore = { encryptedClearOnBoot, ... }: (
if encryptedClearOnBoot then {
device = "/mnt/impermanence/crypt/clearedonboot";
underlying = {
path = "/nix/persist/crypt/clearedonboot";
# TODO: consider moving this to /tmp, but that requires tmp be mounted first?
type = "gocryptfs";
key = "/mnt/impermanence/crypt/clearedonboot.key";
};
} else {
device = "/nix/persist";
# device = "/mnt/impermenanence/persist/plain";
# underlying = {
# path = "/nix/persist";
# type = "bind";
# };
}
);
home-dir-defaults = {
user = "colin";
group = "users";
mode = "0755";
relativeTo = "/home/colin";
};
sys-dir-defaults = {
user = "root";
group = "root";
mode = "0755";
relativeTo = "";
};
# turn a path into a name suitable for systemd
cleanName = utils.escapeSystemdPath;
storeType = types.submodule {
options = {
mountpt = mkOption {
type = types.str;
};
prefix = mkOption {
type = types.str;
default = "/";
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [];
};
};
};
# split the string path into a list of string components.
# root directory "/" becomes the empty list [].
@@ -52,89 +33,114 @@ let
# return a string path, with leading slash but no trailing slash
joinPathAbs = comps: "/" + (builtins.concatStringsSep "/" comps);
concatPaths = paths: joinPathAbs (builtins.concatLists (builtins.map (p: splitPath p) paths));
# 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));
# 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;
dirOptions = defaults: types.submodule {
# options for a single mountpoint / persistence
dirEntryOptions = {
options = {
encryptedClearOnBoot = mkOption {
default = false;
type = types.bool;
};
directory = mkOption {
type = types.str;
};
user = mkOption {
type = types.str;
default = defaults.user;
type = types.nullOr types.str;
default = null;
};
group = mkOption {
type = types.str;
default = defaults.group;
type = types.nullOr types.str;
default = null;
};
mode = mkOption {
type = types.str;
default = defaults.mode;
type = types.nullOr types.str;
default = null;
};
};
};
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);
};
contextualizedDir = types.submodule dirEntryOptions;
# allow "bar/baz" as shorthand for { directory = "bar/baz"; }
contextualizedDirOrShorthand = types.coercedTo
types.str
(d: { directory = d; })
contextualizedDir;
# expand user options with more context
ingestDirOption = defaults: opt: {
inherit (opt) user group mode;
directory = concatPaths [ defaults.relativeTo opt.directory ];
# entry whose `directory` is always an absolute fs path
# and has an associated `store`
contextFreeDir = types.submodule [
dirEntryOptions
{
options = {
store = mkOption {
type = storeType;
};
};
}
];
## 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 = types.submodule {
options = {
plaintext = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = "directories to persist in cleartext";
};
private = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = "directories to store encrypted to the user's login password and auto-decrypt on login";
};
cryptClearOnBoot = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = ''
directories to store encrypted to an auto-generated in-memory key and
wiped on boot. the main use is for sensitive cache dirs too large to fit in memory.
'';
};
};
};
};
sys = mkOption {
description = "directories to persist to disk, relative to the fs root /";
default = {};
type = types.submodule {
options = {
plaintext = mkOption {
default = [];
type = types.listOf contextualizedDirOrShorthand;
description = "list of directories (and optional config) to persist to disk in plaintext, relative to the fs root /";
};
};
};
};
all = mkOption {
type = types.listOf contextFreeDir;
description = "all directories known to the config. auto-computed: users should not set this directly.";
};
};
config = let
mapDirs = relativeTo: store: dirs: (map
(d: {
inherit (d) user group mode;
directory = concatPaths [ relativeTo d.directory ];
store = cfg.stores."${store}";
})
dirs
);
in {
all = (mapDirs "/home/colin" "plaintext" config.home.plaintext)
++ (mapDirs "/home/colin" "private" config.home.private)
++ (mapDirs "/home/colin" "cryptClearOnBoot" config.home.cryptClearOnBoot)
++ (mapDirs "/" "plaintext" config.sys.plaintext);
};
});
in
{
options = {
@@ -145,182 +151,74 @@ 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 = {};
};
sane.impermanence.home-dirs = mkDirsOption home-dir-defaults;
sane.impermanence.dirs = mkDirsOption sys-dir-defaults;
};
imports = [
./root-on-tmpfs.nix
./stores
];
config = mkIf cfg.enable (lib.mkMerge [
(lib.mkIf cfg.root-on-tmpfs {
fileSystems."/" = {
device = "none";
fsType = "tmpfs";
options = [
"mode=755"
"size=1G"
"defaults"
];
};
})
{
# without this, we get `fusermount: fuse device not found, try 'modprobe fuse' first`.
# - that only happens after a activation-via-boot -- not activation-after-rebuild-switch.
# it seems likely that systemd loads `fuse` by default. see:
# - </etc/systemd/system/sysinit.target.wants/sys-fs-fuse-connections.mount>
# - triggers: /etc/systemd/system/modprobe@.service
# - calls `modprobe`
# note: even `boot.kernelModules = ...` isn't enough: that option creates /etc/modules-load.d/, which is ingested only by systemd.
# note: `boot.initrd.availableKernelModules` ALSO isn't enough: idk why.
# TODO: might not be necessary now we're using fileSystems and systemd
boot.initrd.kernelModules = [ "fuse" ];
# TODO: convert this to a systemd unit file?
system.activationScripts.prepareEncryptedClearedOnBoot =
let
script = pkgs.writeShellApplication {
name = "prepareEncryptedClearedOnBoot";
runtimeInputs = with pkgs; [ gocryptfs ];
text = ''
backing="$1"
passfile="$2"
if ! test -e "$passfile"
then
tmpdir=$(dirname "$passfile")
mkdir -p "$backing" "$tmpdir"
# if the key doesn't exist, it's probably not mounted => delete the backing dir
rm -rf "''${backing:?}"/*
# generate key. we can "safely" keep it around for the lifetime of this boot
dd if=/dev/random bs=128 count=1 | base64 --wrap=0 > "$passfile"
# initialize the crypt store
gocryptfs -quiet -passfile "$passfile" -init "$backing"
fi
'';
};
store = getStore { encryptedClearOnBoot = true; };
in {
text = ''${script}/bin/prepareEncryptedClearedOnBoot ${store.underlying.path} ${store.underlying.key}'';
# 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;
};
fileSystems = let
store = getStore { encryptedClearOnBoot = true; };
in {
"${store.device}" = {
device = store.underlying.path;
fsType = "fuse.gocryptfs";
options = [
"nodev"
"nosuid"
"allow_other"
"passfile=${store.underlying.key}"
"defaults"
];
noCheck = true;
};
};
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
# 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
# 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;
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;
};
# 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" ];
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;
};
};
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;
cfgs = builtins.map cfgFor cfg.dirs.all;
in {
# fileSystems = myMerge (catAttrs "fileSystems" cfgs);
fileSystems = lib.mkMerge (builtins.catAttrs "fileSystems" cfgs);
systemd = lib.mkMerge (catAttrs "systemd" cfgs);
sane.fs = lib.mkMerge (catAttrs "fs" (catAttrs "sane" 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
));
})
]);
}

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,89 @@
{ config, lib, pkgs, utils, ... }:
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}
'';
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;
};
fileSystems."${store.device}" = {
device = store.underlying.path;
fsType = "fuse.gocryptfs";
options = [
"nodev"
"nosuid"
"allow_other"
"passfile=${store.underlying.key}"
"defaults"
];
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;
};
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 ];
};
# TODO: could add this *specifically* to the .mount file for the encrypted fs?
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
}

View File

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

View File

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

View File

@@ -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,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"