Compare commits

...

75 Commits

Author SHA1 Message Date
cdc881e887 feeds: write the basis for a module which reads feed metadata from disk and can (in the future) update it 2023-01-10 03:52:33 +00:00
33967554a5 servo: fix missing "lib" in nginx file 2023-01-09 13:25:56 +00:00
5af55ecdbf merge: cleanup/document 2023-01-09 11:47:39 +00:00
6ca3e7086e merge: simplify the implementation and make fully compatible with lib.mkMerge 2023-01-09 11:14:59 +00:00
ca62f1b62f rename flattenAttrsets -> joinAttrsets to disambiguate 2023-01-09 09:52:37 +00:00
eef66df36d lib: split merge out of the toplevel 2023-01-09 09:51:35 +00:00
9ca6a1c907 way overcomplicated way to merge toplevel config 2023-01-09 09:42:17 +00:00
dbb78088f4 refactor: cleanup instances where we map to attrs to be more resilient against duplicate names 2023-01-09 03:48:07 +00:00
f17ae1ca7b refactor: avoid using // where we know the sets should be disjoint 2023-01-09 03:11:14 +00:00
b2774a4004 move pubkeys out a modules/data/ directory 2023-01-09 02:40:25 +00:00
0ae548d47c flake update: nixpkgs 2023-01-04 -> 2023-01-05; sops
vim was segfaulting?? i'm hoping this fixes it, we'll see.

```
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/9813adc7f7c0edd738c6bdd8431439688bb0cb3d' (2023-01-04)
  → 'github:NixOS/nixpkgs/a518c77148585023ff56022f09c4b2c418a51ef5' (2023-01-05)
• Updated input 'nixpkgs-stable':
    'github:NixOS/nixpkgs/e9ade2c8240e00a4784fac282a502efff2786bdc' (2023-01-04)
  → 'github:NixOS/nixpkgs/8c54d842d9544361aac5f5b212ba04e4089e8efe' (2023-01-08)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/b35586cc5abacd4eba9ead138b53e2a60920f781' (2023-01-01)
  → 'github:Mic92/sops-nix/2253120d2a6147e57bafb5c689e086221df8032f' (2023-01-08)
• Updated input 'sops-nix/nixpkgs-stable':
    'github:NixOS/nixpkgs/feda52be1d59f13b9aa02f064b4f14784b9a06c8' (2022-12-31)
  → 'github:NixOS/nixpkgs/9f11a2df77cb945c115ae2a65f53f38121597d73' (2023-01-07)
```
2023-01-08 23:52:40 +00:00
760505db20 snippets: add NUR package search 2023-01-08 14:16:06 +00:00
71fc1a2fd7 ssh: define system-wide knownHosts 2023-01-08 08:51:06 +00:00
a457fc1416 ssh: move sys config out of hosts/common 2023-01-08 08:43:23 +00:00
2c0b0f6947 ssh: explain why we specify host_keys the way we do instead of through sane.persist 2023-01-08 08:41:48 +00:00
f10de6c2c4 ids: improve docs 2023-01-08 06:54:29 +00:00
a6be200a82 ids: define the assertions more idiomatically 2023-01-08 06:51:25 +00:00
fb57e9aa5b cleanup the 'every user/group has an id' enforcement 2023-01-08 06:46:07 +00:00
f5acbbd830 image.nix: feed bug where enable flag wasnt actually being read 2023-01-08 05:37:25 +00:00
af77417531 feeds: add Perry Bible Fellowship comic 2023-01-08 05:30:36 +00:00
eea80b575d feeds: disable dilbert (it doesn't embed well) 2023-01-08 05:28:15 +00:00
6a209d27fd freshrss: only show text and image feeds 2023-01-08 05:27:45 +00:00
e8f778fecd feeds: convert to module 2023-01-08 05:24:56 +00:00
488036beb3 ssh: add git.uninsane.org host key back 2023-01-08 03:22:05 +00:00
00b681eca5 ssh: manager ourself instead of using home-manager 2023-01-08 03:14:47 +00:00
72d589cb2d ssh: port to modules system 2023-01-08 03:07:57 +00:00
ea5552daa7 bluetooth: accept that LinkKeys are device/host-specific and stop trying to share them across machines 2023-01-07 11:31:35 +00:00
fb7d94209c bluetooth: update key for portable speaker
i was having difficulty connecting from lappy.
i re-paired: the old LinkKey doesn't seem to work...?
this new key gave a file without `PublicAddress=true`: i don't *think*
that actually matters, though the device *does* appear to be a public
address on first glance (00: prefix, and last 2 bits aren't 11).
2023-01-07 10:18:36 +00:00
8f5b92685b install-bluetooth: just copy the keys, dont bother symlinking 2023-01-07 09:59:06 +00:00
32a4cb19fd sway: start pipewire early, to support bluetooth 2023-01-07 09:58:27 +00:00
031cfa2bcd get bluetooth working in gnome-control-center 2023-01-07 08:35:51 +00:00
e93fbea1e6 phosh: reorder the users defs 2023-01-07 08:08:49 +00:00
85a2fbc38a bluetooth: dont persist /var/lib/bluetooth 2023-01-07 08:08:29 +00:00
9e902c8eb2 preserve backlight settings across reboots 2023-01-07 05:17:43 +00:00
dc15091ea7 install-bluetooth: disable verbosity 2023-01-07 03:44:45 +00:00
c063ecd047 bluetooth keys: use sane.fs instead of activationScripts
also auto-determines the device ID, which was previously broken
2023-01-07 03:43:31 +00:00
70a43c770d net: fix a iwd error by not encoding a network name which didn't need encoding 2023-01-07 03:11:12 +00:00
cc9e2d8e15 net: simplify the iwd psk setup 2023-01-07 03:10:39 +00:00
bb41fb95fe iwd: populate net config with systemd service, not activationScript 2023-01-07 03:03:19 +00:00
d852adf806 move keyring to private store 2023-01-07 02:04:28 +00:00
5443542cba move keyring activation out of home-manager 2023-01-07 01:41:56 +00:00
81effb01a3 new script: sane-shutdown, validates host 2023-01-06 16:40:41 +00:00
83f416999f splatmoji: persist history file 2023-01-06 16:35:31 +00:00
dd34883246 move feed consumers out of home-manager 2023-01-06 16:27:05 +00:00
e47f9e38ce remove old nb module 2023-01-06 16:15:49 +00:00
0f0b728911 splatmoji: store config with sane.fs instead of home-manager 2023-01-06 16:13:51 +00:00
1839f87a4e vlc: handle the config file with sane.fs 2023-01-06 16:11:56 +00:00
53edf4e6af firefox: handle config files manually, instead of leveraging home-manager 2023-01-06 16:11:06 +00:00
fb6e0ddb34 convert some home-manager files to be manually managed 2023-01-06 15:48:51 +00:00
0a48d79174 fs: introduce some helpers to make writing symlinks easier 2023-01-06 15:38:29 +00:00
b6208e1a19 fs: allow specifying text for a symlink directly 2023-01-06 15:26:39 +00:00
e46ab4ec14 ssh: use sane.persist/sane.fs instead of home-manager to ensure keys 2023-01-06 15:05:01 +00:00
19c254c266 fs: make symlinking more resilient when something's already at the location 2023-01-06 14:51:25 +00:00
1d0cadce85 persist: configure the private store to symlink everyting by default 2023-01-06 14:44:32 +00:00
e8342b8044 persist: clean up the "byPath" conversions 2023-01-06 14:20:30 +00:00
40e642bfc3 persist: add a 'method' option to allow symlinking in favor of binding 2023-01-06 14:05:49 +00:00
f008565e22 persist: for options common to entries specified by both path and store, move to a common submodule 2023-01-06 13:58:36 +00:00
4ea2835d9d persist: handle inline acl options more cleanly 2023-01-06 13:47:59 +00:00
493d317bb1 moby: override browser-cache persistence more cleanly 2023-01-06 13:28:18 +00:00
e446bfba58 fs: fix eval error when told about a mount but not told about anything *in* that mount 2023-01-06 13:27:27 +00:00
a7bac5de18 persist: convert the sane.persist.home.<store> => mappings back to a strongly-typed module & add a byPath shorthand 2023-01-06 13:06:39 +00:00
b0950e90f4 persist: prefer mkMerge instead of manually folding attrsets 2023-01-06 12:44:29 +00:00
d8cd0e1f57 persist: fold redundant lines 2023-01-06 12:39:55 +00:00
fd7d67ee05 persist: simplify & remove dead code 2023-01-06 12:28:55 +00:00
1a712b4d47 rename sane.persist.{all -> byPath} 2023-01-06 12:19:03 +00:00
4520e1d1f5 persist: auto-map user-provided store values earlier 2023-01-06 11:56:22 +00:00
841a2a3bcb persist: change sane.persist.all to be an attrsOf that maps path to settings 2023-01-06 11:52:28 +00:00
fe816e9110 persist: lift sane.persist.dirs.{home,sys} up one level 2023-01-06 11:29:13 +00:00
426e0c3ae2 persist: lift sane.persist.dirs.all up to sane.persist.all 2023-01-06 11:24:11 +00:00
a95b91a556 refactor the dirsSubModule type so that we don't reference 'config.sane.persist' while creating options 2023-01-06 10:35:32 +00:00
837e5438c3 persist: document the dirsSubModule type better 2023-01-06 10:31:01 +00:00
8217b22c86 rename impermanence -> persist 2023-01-06 10:04:51 +00:00
0b35ce4dec Merge branch 'staging/nixpkgs-2023-01-04' 2023-01-06 10:00:37 +00:00
413f9a171b impermanence: remove /home perms hack 2023-01-06 09:59:29 +00:00
43a46af43b impermanence: cleanup backing directory creation. this should let me remove the per-store /home/<user> perms hack 2023-01-06 09:56:06 +00:00
87 changed files with 1337 additions and 820 deletions

24
flake.lock generated
View File

@@ -54,11 +54,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1672791794,
"narHash": "sha256-mqGPpGmwap0Wfsf3o2b6qHJW1w2kk/I6cGCGIU+3t6o=",
"lastModified": 1672953546,
"narHash": "sha256-oz757DnJ1ITvwyTovuwG3l9cX6j9j6/DH9eH+cXFJmc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9813adc7f7c0edd738c6bdd8431439688bb0cb3d",
"rev": "a518c77148585023ff56022f09c4b2c418a51ef5",
"type": "github"
},
"original": {
@@ -69,11 +69,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1672844754,
"narHash": "sha256-o26WabuHABQsaHxxmIrR3AQRqDFUEdLckLXkVCpIjSU=",
"lastModified": 1673163619,
"narHash": "sha256-B33PFBL64ZgTWgMnhFL3jgheAN/DjHPsZ1Ih3z0VE5I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9ade2c8240e00a4784fac282a502efff2786bdc",
"rev": "8c54d842d9544361aac5f5b212ba04e4089e8efe",
"type": "github"
},
"original": {
@@ -84,11 +84,11 @@
},
"nixpkgs-stable_2": {
"locked": {
"lastModified": 1672500394,
"narHash": "sha256-yzwBzCoeRBoRzm7ySHhm72kBG0QjgFalLz2FY48iLI4=",
"lastModified": 1673100377,
"narHash": "sha256-mT76pTd0YFxT6CwtPhDgHJhuIgLY+ZLSMiQpBufwMG4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "feda52be1d59f13b9aa02f064b4f14784b9a06c8",
"rev": "9f11a2df77cb945c115ae2a65f53f38121597d73",
"type": "github"
},
"original": {
@@ -116,11 +116,11 @@
"nixpkgs-stable": "nixpkgs-stable_2"
},
"locked": {
"lastModified": 1672543202,
"narHash": "sha256-nlCUtcIZxaBqUBG1GyaXhZmfyG5WK4e6LqypP8llX9E=",
"lastModified": 1673147300,
"narHash": "sha256-gR9OEfTzWfL6vG0qkbn1TlBAOlg4LuW8xK/u0V41Ihc=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "b35586cc5abacd4eba9ead138b53e2a60920f781",
"rev": "2253120d2a6147e57bafb5c689e086221df8032f",
"type": "github"
},
"original": {

View File

@@ -1,18 +1,16 @@
{ lib, pkgs, ... }:
{
# TODO: don't need to depend on binsh if we were to use a nix-style shebang
system.activationScripts.linkBluetoothKeys = let
unwrapped = ../../scripts/install-bluetooth;
install-bluetooth = pkgs.writeShellApplication {
name = "install-bluetooth";
runtimeInputs = with pkgs; [ coreutils gnused ];
text = ''${unwrapped} "$@"'';
};
in (lib.stringAfter
[ "setupSecrets" "binsh" ]
''
${install-bluetooth}/bin/install-bluetooth /run/secrets/bt
''
);
# persist external pairings by default
sane.persist.sys.plaintext = [ "/var/lib/bluetooth" ];
sane.fs."/var/lib/bluetooth".generated.acl.mode = "0700";
sane.fs."/var/lib/bluetooth/.secrets.stamp" = {
wantedBeforeBy = [ "bluetooth.service" ];
# XXX: install-bluetooth uses sed, but that's part of the default systemd unit path, it seems
generated.script.script = builtins.readFile ../../scripts/install-bluetooth + ''
touch "/var/lib/bluetooth/.secrets.stamp"
'';
generated.script.scriptArgs = [ "/run/secrets/bt" ];
};
}

View File

@@ -2,9 +2,11 @@
{
imports = [
./bluetooth.nix
./feeds.nix
./fs.nix
./hardware
./i2p.nix
./ids.nix
./machine-id.nix
./net.nix
./secrets.nix
@@ -18,12 +20,11 @@
sane.packages.enableConsolePkgs = true;
sane.packages.enableSystemPkgs = true;
sane.impermanence.dirs.sys.plaintext = [
sane.persist.sys.plaintext = [
"/var/log"
"/var/backup" # for e.g. postgres dumps
# TODO: move elsewhere
"/var/lib/alsa" # preserve output levels, default devices
"/var/lib/bluetooth" # preserve bluetooth handshakes
"/var/lib/colord" # preserve color calibrations (?)
"/var/lib/machines" # maybe not needed, but would be painful to add a VM and forget.
];

View File

@@ -1,5 +1,4 @@
{ lib }:
{ ... }:
let
hourly = { freq = "hourly"; };
daily = { freq = "daily"; };
@@ -13,24 +12,15 @@ let
tech = { cat = "tech"; };
uncat = { cat = "uncat"; };
text = { format = "text"; };
image = { format = "image"; };
podcast = { format = "podcast"; };
mkRss = format: url: { inherit url format; } // uncat // infrequent;
# format-specific helpers
mkText = mkRss text;
mkImg = mkRss image;
mkPod = mkRss podcast;
mkText = mkRss "text";
mkImg = mkRss "image";
mkPod = mkRss "podcast";
# host-specific helpers
mkSubstack = subdomain: mkText "https://${subdomain}.substack.com/feed";
mkSubstack = subdomain: { substack = subdomain; };
# merge the attrs `new` into each value of the attrs `addTo`
addAttrs = new: addTo: builtins.mapAttrs (k: v: v // new) addTo;
# for each value in `attrs`, add a value to the child attrs which holds its key within the parent attrs.
withInverseMapping = key: attrs: builtins.mapAttrs (k: v: v // { "${key}" = k; }) attrs;
in rec {
podcasts = [
(mkPod "https://lexfridman.com/feed/podcast/" // rat // weekly)
## Astral Codex Ten
@@ -149,46 +139,13 @@ in rec {
images = [
(mkImg "https://www.smbc-comics.com/comic/rss" // humor // daily)
(mkImg "https://xkcd.com/atom.xml" // humor // daily)
(mkImg "http://dilbert.com/feed" // humor // daily)
(mkImg "https://pbfcomics.com/feed" // humor // infrequent)
# (mkImg "http://dilbert.com/feed" // humor // daily)
# ART
(mkImg "https://miniature-calendar.com/feed" // art // daily)
];
all = texts ++ images ++ podcasts;
# return only the feed items which match this category (e.g. "tech")
filterCat = cat: feeds: builtins.filter (item: item.cat == cat) feeds;
# return only the feed items which match this format (e.g. "podcast")
filterFormat = format: feeds: builtins.filter (item: item.format == format) feeds;
# transform a list of feeds into an attrs mapping cat => [ feed0 feed1 ... ]
partitionByCat = feeds: builtins.groupBy (f: f.cat) feeds;
# represents a single RSS feed.
opmlTerminal = feed: ''<outline xmlUrl="${feed.url}" type="rss"/>'';
# a list of RSS feeds.
opmlTerminals = feeds: lib.strings.concatStringsSep "\n" (builtins.map opmlTerminal feeds);
# one node which packages some flat grouping of terminals.
opmlGroup = title: feeds: ''
<outline text="${title}" title="${title}">
${opmlTerminals feeds}
</outline>
'';
# a list of groups (`groupMap` is an attrs mapping groupName => [ feed0 feed1 ... ]).
opmlGroups = groupMap: lib.strings.concatStringsSep "\n" (
builtins.attrValues (builtins.mapAttrs opmlGroup groupMap)
);
# top-level OPML file which could be consumed by something else.
opmlTopLevel = body: ''
<?xml version="1.0" encoding="utf-8"?>
<opml version="2.0">
<body>
${body}
</body>
</opml>
'';
# **primary API**: generate a OPML file from the provided feeds
feedsToOpml = feeds: opmlTopLevel (opmlGroups (partitionByCat feeds));
in
{
sane.feeds = texts ++ images ++ podcasts;
}

60
hosts/common/ids.nix Normal file
View File

@@ -0,0 +1,60 @@
{ ... }:
{
# legacy servo users, some are inconvenient to migrate
sane.ids.dhcpcd.gid = 991;
sane.ids.dhcpcd.uid = 992;
sane.ids.gitea.gid = 993;
sane.ids.git.uid = 994;
sane.ids.jellyfin.gid = 994;
sane.ids.pleroma.gid = 995;
sane.ids.jellyfin.uid = 996;
sane.ids.acme.gid = 996;
sane.ids.pleroma.uid = 997;
sane.ids.acme.uid = 998;
# greetd (used by sway)
sane.ids.greeter.uid = 999;
sane.ids.greeter.gid = 999;
# new servo users
sane.ids.freshrss.uid = 2401;
sane.ids.freshrss.gid = 2401;
sane.ids.mediawiki.uid = 2402;
sane.ids.colin.uid = 1000;
sane.ids.guest.uid = 1100;
# found on all hosts
sane.ids.sshd.uid = 2001; # 997
sane.ids.sshd.gid = 2001; # 997
sane.ids.polkituser.gid = 2002; # 998
sane.ids.systemd-coredump.gid = 2003; # 996
sane.ids.nscd.uid = 2004;
sane.ids.nscd.gid = 2004;
sane.ids.systemd-oom.uid = 2005;
sane.ids.systemd-oom.gid = 2005;
# found on graphical hosts
sane.ids.nm-iodine.uid = 2101; # desko/moby/lappy
# found on desko host
# from services.usbmuxd
sane.ids.usbmux.uid = 2204;
sane.ids.usbmux.gid = 2204;
# originally found on moby host
# gnome core-shell
sane.ids.avahi.uid = 2304;
sane.ids.avahi.gid = 2304;
sane.ids.colord.uid = 2305;
sane.ids.colord.gid = 2305;
sane.ids.geoclue.uid = 2306;
sane.ids.geoclue.gid = 2306;
# gnome core-os-services
sane.ids.rtkit.uid = 2307;
sane.ids.rtkit.gid = 2307;
# phosh
sane.ids.feedbackd.gid = 2308;
}

View File

@@ -31,19 +31,13 @@
General.RoamThreshold5G = "-52"; # default -76
};
# TODO: don't need to depend on binsh if we were to use a nix-style shebang
system.activationScripts.linkIwdKeys = let
unwrapped = ../../scripts/install-iwd;
install-iwd = pkgs.writeShellApplication {
name = "install-iwd";
runtimeInputs = with pkgs; [ coreutils gnused ];
text = ''${unwrapped} "$@"'';
};
in (lib.stringAfter
[ "setupSecrets" "binsh" ]
''
mkdir -p /var/lib/iwd
${install-iwd}/bin/install-iwd /run/secrets/iwd /var/lib/iwd
''
);
sane.fs."/var/lib/iwd/.secrets.psk.stamp" = {
wantedBeforeBy = [ "iwd.service" ];
generated.acl.mode = "0600";
# XXX: install-iwd uses sed, but that's part of the default systemd unit path, it seems
generated.script.script = builtins.readFile ../../scripts/install-iwd + ''
touch "/var/lib/iwd/.secrets.psk.stamp"
'';
generated.script.scriptArgs = [ "/run/secrets/iwd" "/var/lib/iwd" ];
};
}

View File

@@ -1,10 +1,24 @@
{ config, lib, ... }:
{ config, lib, sane-data, sane-lib, ... }:
{
environment.etc."ssh/host_keys".source = "/nix/persist/etc/ssh/host_keys";
services.openssh.hostKeys = [
{ type = "rsa"; bits = 4096; path = "/etc/ssh/host_keys/ssh_host_rsa_key"; }
{ type = "ed25519"; path = "/etc/ssh/host_keys/ssh_host_ed25519_key"; }
];
sane.ssh.pubkeys =
let
# path is a DNS-style path like [ "org" "uninsane" "root" ]
keyNameForPath = path:
let
rev = lib.reverseList path;
name = builtins.head rev;
host = lib.concatStringsSep "." (builtins.tail rev);
in
"${name}@${host}";
# [{ path :: [String], value :: String }] for the keys we want to install
globalKeys = sane-lib.flattenAttrs sane-data.keys;
localKeys = sane-lib.flattenAttrs sane-data.keys.org.uninsane.local;
in lib.mkMerge (builtins.map
({ path, value }: {
"${keyNameForPath path}" = value;
})
(globalKeys ++ localKeys)
);
}

View File

@@ -1,16 +1,10 @@
{ config, pkgs, lib, ... }:
{ config, pkgs, lib, sane-lib, ... }:
# installer docs: https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/installation-device.nix
with lib;
let
cfg = config.sane.users;
# 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" ];
};
fs = sane-lib.fs;
in
{
options = {
@@ -32,7 +26,6 @@ in
home = "/home/colin";
createHome = true;
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.
subUidRanges = [
@@ -55,7 +48,6 @@ in
passwordFile = lib.mkIf (config.sops.secrets ? "colin-passwd") config.sops.secrets.colin-passwd.path;
shell = pkgs.zsh;
openssh.authorizedKeys.keys = builtins.attrValues (import ../../modules/pubkeys.nix).users;
# mount encrypted stuff at login
# some other nix pam users:
@@ -82,7 +74,7 @@ in
mode = config.users.users.colin.homeMode;
};
sane.impermanence.dirs.home.plaintext = [
sane.persist.home.plaintext = [
"archive"
"dev"
# TODO: records should be private
@@ -96,34 +88,25 @@ in
".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
];
# 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";
sane.fs."/home/colin/knowledge" = fs.wantedSymlinkTo "/home/colin/private/knowledge";
sane.fs."/home/colin/nixos" = fs.wantedSymlinkTo "/home/colin/dev/nixos";
sane.fs."/home/colin/Videos/servo" = fs.wantedSymlinkTo "/mnt/servo-media/Videos";
sane.fs."/home/colin/Videos/servo-incomplete" = fs.wantedSymlinkTo "/mnt/servo-media/incomplete";
sane.fs."/home/colin/Music/servo" = fs.wantedSymlinkTo "/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.fs."/home/colin/.password-store" = fs.wantedSymlinkTo "/home/colin/knowledge/secrets/accounts";
sane.impermanence.dirs.sys.plaintext = mkIf cfg.guest.enable [
sane.persist.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;
home = "/home/guest";
uid = config.sane.allocations.guest-uid;
subUidRanges = [
{ startUid=200000; count=1; }
];
@@ -135,13 +118,6 @@ in
];
};
users.users.dhcpcd = mkIf hasDHCP {
uid = config.sane.allocations.dhcpcd-uid;
};
users.groups.dhcpcd = mkIf hasDHCP {
gid = config.sane.allocations.dhcpcd-gid;
};
security.sudo = {
enable = true;
wheelNeedsPassword = false;
@@ -152,31 +128,5 @@ in
permitRootLogin = "no";
passwordAuthentication = false;
};
# affix some UIDs which were historically auto-generated
users.users.sshd.uid = config.sane.allocations.sshd-uid;
users.groups.polkituser.gid = config.sane.allocations.polkituser-gid;
users.groups.sshd.gid = config.sane.allocations.sshd-gid;
users.groups.systemd-coredump.gid = config.sane.allocations.systemd-coredump-gid;
users.users.nscd.uid = config.sane.allocations.nscd-uid;
users.groups.nscd.gid = config.sane.allocations.nscd-gid;
users.users.systemd-oom.uid = config.sane.allocations.systemd-oom-uid;
users.groups.systemd-oom.gid = config.sane.allocations.systemd-oom-gid;
# guarantee determinism in uid/gid generation for users:
assertions = let
uidAssertions = builtins.attrValues (builtins.mapAttrs (name: user: {
assertion = user.uid != null;
message = "non-deterministic uid detected for: ${name}";
}) config.users.users);
gidAssertions = builtins.attrValues (builtins.mapAttrs (name: group: {
assertion = group.gid != null;
message = "non-deterministic gid detected for: ${name}";
}) config.users.groups);
autoSubAssertions = builtins.attrValues (builtins.mapAttrs (name: user: {
assertion = !user.autoSubUidGidRange;
message = "non-deterministic subUids/Guids detected for: ${name}";
}) config.users.users);
in uidAssertions ++ gidAssertions ++ autoSubAssertions;
};
}

View File

@@ -10,15 +10,13 @@
sane.services.duplicity.enable = true;
sane.services.nixserve.enable = true;
sane.services.nixserve.sopsFile = ../../secrets/desko.yaml;
sane.impermanence.enable = true;
sane.persist.enable = true;
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
# needed to use libimobiledevice/ifuse, for iphone sync
services.usbmuxd.enable = true;
users.users.usbmux.uid = config.sane.allocations.usbmux-uid;
users.groups.usbmux.gid = config.sane.allocations.usbmux-gid;
sops.secrets.colin-passwd = {
sopsFile = ../../secrets/desko.yaml;
@@ -52,7 +50,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.dirs.home.plaintext = [
sane.persist.home.plaintext = [
".steam"
".local/share/Steam"
];

View File

@@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.root-on-tmpfs = true;
sane.persist.root-on-tmpfs = true;
# we need a /tmp for building large nix things.
# a cross-compiled kernel, particularly, will easily use 30+GB of tmp
fileSystems."/tmp" = {

View File

@@ -8,7 +8,7 @@
# sane.users.guest.enable = true;
sane.gui.sway.enable = true;
sane.impermanence.enable = true;
sane.persist.enable = true;
sane.nixcache.enable = true;
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];

View File

@@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.root-on-tmpfs = true;
sane.persist.root-on-tmpfs = true;
# we need a /tmp of default size (half RAM) for building large nix things
fileSystems."/tmp" = {
device = "none";

View File

@@ -24,11 +24,9 @@
};
# usability compromises
sane.impermanence.dirs.home.private = [
config.sane.web-browser.dotDir
config.sane.web-browser.cacheDir
];
sane.impermanence.dirs.home.plaintext = [
sane.web-browser.persistCache = "private";
sane.web-browser.persistData = "private";
sane.persist.home.plaintext = [
".config/pulse" # persist pulseaudio volume
];
@@ -38,7 +36,7 @@
];
sane.nixcache.enable = true;
sane.impermanence.enable = true;
sane.persist.enable = true;
sane.gui.phosh.enable = true;
boot.loader.efi.canTouchEfiVariables = false;

View File

@@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.root-on-tmpfs = true;
sane.persist.root-on-tmpfs = true;
fileSystems."/nix" = {
device = "/dev/disk/by-uuid/1f1271f8-53ce-4081-8a29-60a4a6b5d6f9";
fsType = "btrfs";

View File

@@ -8,9 +8,6 @@
boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
users.users.dhcpcd.uid = config.sane.allocations.dhcpcd-uid;
users.groups.dhcpcd.gid = config.sane.allocations.dhcpcd-gid;
# docs: https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion
system.stateVersion = "21.05";
}

View File

@@ -13,7 +13,7 @@
pkgs.matrix-synapse
pkgs.freshrss
];
sane.impermanence.enable = true;
sane.persist.enable = true;
sane.services.dyn-dns.enable = true;
# sane.services.duplicity.enable = true; # TODO: re-enable after HW upgrade

View File

@@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.root-on-tmpfs = true;
sane.persist.root-on-tmpfs = true;
# we need a /tmp for building large nix things
fileSystems."/tmp" = {
device = "none";
@@ -27,7 +27,7 @@
};
# slow, external storage (for archiving, etc)
fileSystems."/mnt/impermanence/ext" = {
fileSystems."/mnt/persist/ext" = {
device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
fsType = "btrfs";
options = [
@@ -36,18 +36,18 @@
];
};
sane.impermanence.stores."ext" = {
origin = "/mnt/impermanence/ext/persist";
sane.persist.stores."ext" = {
origin = "/mnt/persist/ext/persist";
storeDescription = "external HDD storage";
};
sane.fs."/mnt/impermanence/ext".mount = {};
sane.fs."/mnt/persist/ext".mount = {};
sane.impermanence.dirs.sys.plaintext = [
sane.persist.sys.plaintext = [
# TODO: this is overly broad; only need media and share directories to be persisted
{ user = "colin"; group = "users"; directory = "/var/lib/uninsane"; }
];
# make sure large media is stored to the HDD
sane.impermanence.dirs.sys.ext = [
sane.persist.sys.ext = [
{
user = "colin";
group = "users";

View File

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

View File

@@ -9,19 +9,17 @@
# $ sudo -u freshrss -g freshrss FRESHRSS_DATA_PATH=/var/lib/freshrss ./result/cli/export-opml-for-user.php --user admin
# ```
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, sane-lib, ... }:
{
sops.secrets.freshrss_passwd = {
sopsFile = ../../../secrets/servo.yaml;
owner = config.users.users.freshrss.name;
mode = "0400";
};
sane.impermanence.dirs.sys.plaintext = [
sane.persist.sys.plaintext = [
{ user = "freshrss"; group = "freshrss"; directory = "/var/lib/freshrss"; }
];
users.users.freshrss.uid = config.sane.allocations.freshrss-uid;
users.groups.freshrss.gid = config.sane.allocations.freshrss-gid;
services.freshrss.enable = true;
services.freshrss.baseUrl = "https://rss.uninsane.org";
services.freshrss.virtualHost = "rss.uninsane.org";
@@ -29,9 +27,11 @@
systemd.services.freshrss-import-feeds =
let
feeds = sane-lib.feeds;
fresh = config.systemd.services.freshrss-config;
feeds = import ../../../modules/home-manager/feeds.nix { inherit lib; };
opml = pkgs.writeText "sane-freshrss.opml" (feeds.feedsToOpml feeds.all);
all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat ["text" "image"] all-feeds;
opml = pkgs.writeText "sane-freshrss.opml" (feeds.feedsToOpml wanted-feeds);
in {
inherit (fresh) wantedBy environment;
serviceConfig = {

View File

@@ -1,11 +1,10 @@
{ config, pkgs, lib, ... }:
{
sane.impermanence.dirs.sys.plaintext = [
sane.persist.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "git"; group = "gitea"; directory = "/var/lib/gitea"; }
];
users.groups.gitea.gid = config.sane.allocations.gitea-gid;
services.gitea.enable = true;
services.gitea.user = "git"; # default is 'gitea'
services.gitea.database.type = "postgres";

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{ lib, ... }:
{
sane.impermanence.dirs.sys.plaintext = [
sane.persist.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.sys.plaintext = [
sane.persist.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.sys.plaintext = [
sane.persist.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"

View File

@@ -1,9 +1,9 @@
# docs: https://nixos.wiki/wiki/Nginx
{ config, pkgs, ... }:
{ config, lib, pkgs, ... }:
let
# make the logs for this host "public" so that they show up in e.g. metrics
publog = vhost: vhost // {
publog = vhost: lib.attrsets.unionOfDisjoint vhost {
extraConfig = (vhost.extraConfig or "") + ''
access_log /var/log/nginx/public.log vcombined;
'';
@@ -120,9 +120,7 @@ in
security.acme.acceptTerms = true;
security.acme.defaults.email = "admin.acme@uninsane.org";
users.users.acme.uid = config.sane.allocations.acme-uid;
users.groups.acme.gid = config.sane.allocations.acme-gid;
sane.impermanence.dirs.sys.plaintext = [
sane.persist.sys.plaintext = [
# TODO: mode?
{ user = "acme"; group = "acme"; directory = "/var/lib/acme"; }
{ user = "colin"; group = "users"; directory = "/var/www/sites"; }

View File

@@ -6,12 +6,10 @@
{ config, pkgs, ... }:
{
sane.impermanence.dirs.sys.plaintext = [
sane.persist.sys.plaintext = [
# TODO: mode? could be more granular
{ user = "pleroma"; group = "pleroma"; directory = "/var/lib/pleroma"; }
];
users.users.pleroma.uid = config.sane.allocations.pleroma-uid;
users.groups.pleroma.gid = config.sane.allocations.pleroma-gid;
services.pleroma.enable = true;
services.pleroma.secretConfigFile = config.sops.secrets.pleroma_secrets.path;
services.pleroma.configs = [

View File

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

View File

@@ -1,7 +1,7 @@
{ ... }:
{
sane.impermanence.dirs.sys.plaintext = [
sane.persist.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.sys.plaintext = [
sane.persist.sys.plaintext = [
{ user = "prosody"; group = "prosody"; directory = "/var/lib/prosody"; }
];
networking.firewall.allowedTCPPorts = [

View File

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

View File

@@ -11,8 +11,6 @@ lib.mkIf false
sopsFile = ../../../secrets/servo.yaml;
};
users.users.mediawiki.uid = config.sane.allocations.mediawiki-uid;
services.mediawiki.enable = true;
services.mediawiki.name = "Uninsane Wiki";
services.mediawiki.passwordFile = config.sops.secrets.mediawiki_pw.path;

View File

@@ -12,7 +12,6 @@
home = "/var/lib/gitea";
useDefaultShell = true;
group = "gitea";
uid = config.sane.allocations.git-uid;
isSystemUser = true;
# sendmail access (not 100% sure if this is necessary)
extraGroups = [ "postdrop" ];

View File

@@ -1,63 +0,0 @@
{ lib, ... }:
with lib;
let
mkId = id: mkOption {
default = id;
type = types.int;
};
in
{
options = {
# legacy servo users, some are inconvenient to migrate
sane.allocations.dhcpcd-gid = mkId 991;
sane.allocations.dhcpcd-uid = mkId 992;
sane.allocations.gitea-gid = mkId 993;
sane.allocations.git-uid = mkId 994;
sane.allocations.jellyfin-gid = mkId 994;
sane.allocations.pleroma-gid = mkId 995;
sane.allocations.jellyfin-uid = mkId 996;
sane.allocations.acme-gid = mkId 996;
sane.allocations.pleroma-uid = mkId 997;
sane.allocations.acme-uid = mkId 998;
sane.allocations.greeter-uid = mkId 999;
sane.allocations.greeter-gid = mkId 999;
# new servo users
sane.allocations.freshrss-uid = mkId 2401;
sane.allocations.freshrss-gid = mkId 2401;
sane.allocations.mediawiki-uid = mkId 2402;
sane.allocations.colin-uid = mkId 1000;
sane.allocations.guest-uid = mkId 1100;
# found on all hosts
sane.allocations.sshd-uid = mkId 2001; # 997
sane.allocations.sshd-gid = mkId 2001; # 997
sane.allocations.polkituser-gid = mkId 2002; # 998
sane.allocations.systemd-coredump-gid = mkId 2003; # 996
sane.allocations.nscd-uid = mkId 2004;
sane.allocations.nscd-gid = mkId 2004;
sane.allocations.systemd-oom-uid = mkId 2005;
sane.allocations.systemd-oom-gid = mkId 2005;
# found on graphical hosts
sane.allocations.nm-iodine-uid = mkId 2101; # desko/moby/lappy
# found on desko host
sane.allocations.usbmux-uid = mkId 2204;
sane.allocations.usbmux-gid = mkId 2204;
# originally found on moby host
sane.allocations.avahi-uid = mkId 2304;
sane.allocations.avahi-gid = mkId 2304;
sane.allocations.colord-uid = mkId 2305;
sane.allocations.colord-gid = mkId 2305;
sane.allocations.geoclue-uid = mkId 2306;
sane.allocations.geoclue-gid = mkId 2306;
sane.allocations.rtkit-uid = mkId 2307;
sane.allocations.rtkit-gid = mkId 2307;
sane.allocations.feedbackd-gid = mkId 2308;
};
}

12
modules/data/default.nix Normal file
View File

@@ -0,0 +1,12 @@
# this directory contains data of a factual nature.
# for example, public ssh keys, GPG keys, DNS-type name mappings.
#
# don't put things like fully-specific ~/.config files in here,
# even if they're "relatively unopinionated".
moduleArgs:
{
feeds = import ./feeds moduleArgs;
keys = import ./keys.nix;
}

View File

@@ -0,0 +1,58 @@
{ lib, ... }:
let
inherit (builtins) concatLists concatStringsSep foldl' fromJSON map readDir readFile;
inherit (lib) init mapAttrsToList removePrefix removeSuffix splitString;
inherit (lib.attrsets) recursiveUpdate setAttrByPath;
inherit (lib.filesystem) listFilesRecursive;
# given a path to a .json file relative to sources, construct the best feed object we can.
# the .json file could be empty, in which case we make assumptions about the feed based
# on its fs path.
# Type: feedFromSourcePath :: String -> { path = [String]; value = feed; }
feedFromSourcePath = json-path:
let
canonical-name = removeSuffix "/default" (lib.removeSuffix ".json" json-path);
default-url = "https://${canonical-name}";
attr-path = splitString "/" canonical-name;
feed-details = { url = default-url; } // (tryImportJson (./sources/${json-path}));
in { path = attr-path; value = mkFeed feed-details; };
# TODO: for now, feeds are just ordinary Attrs.
# in the future, we'd like to set them up with an update script.
mkFeed = { url, ... }@details: details;
# return an AttrSet representing the json at the provided path,
# or {} if the path is empty.
tryImportJson = path:
let
as-str = readFile path;
in
if as-str == "" then
{}
else
fromJSON as-str;
sources = enumerateFilePaths ./sources;
# like `lib.listFilesRecursive` but does not mangle paths.
# Type: enumerateFilePaths :: path -> [String]
enumerateFilePaths = base:
concatLists (
mapAttrsToList
(name: type:
if type == "directory" then
# enumerate this directory and then prefix each result with the directory's name
map (e: "${name}/${e}") (enumerateFilePaths (base + "/${name}"))
else
[ name ]
)
(readDir base)
);
# like listToAttrs, except takes { path, value } pairs instead of { name, value } pairs.
# Type: listToAttrsByPath :: [{ path = [String]; value = Any; }] -> Attrs
listToAttrsByPath = items:
foldl' (acc: { path, value }: recursiveUpdate acc (setAttrByPath path value)) {} items;
in
listToAttrsByPath (map feedFromSourcePath sources)

24
modules/data/keys.nix Normal file
View File

@@ -0,0 +1,24 @@
# hierarchical, DNS-like mapping from <name> => ssh host/user for that name.
# host keys are represented as user keys, just with the user specified as "root".
{
org.uninsane = rec {
root = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOfdSmFkrVT6DhpgvFeQKm3Fh9VKZ9DbLYOPOJWYQ0E8";
git.root = root;
local = {
# machine aliases i specify on my lan; not actually asserted as DNS
desko.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPU5GlsSfbaarMvDA20bxpSZGWviEzXGD8gtrIowc1pX";
desko.root = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFw9NoRaYrM6LbDd3aFBc4yyBlxGQn8HjeHd/dZ3CfHk";
lappy.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDpmFdNSVPRol5hkbbCivRhyeENzb9HVyf9KutGLP2Zu";
lappy.root = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILSJnqmVl9/SYQ0btvGb0REwwWY8wkdkGXQZfn/1geEc";
moby.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrR+gePnl0nV/vy7I5BzrGeyVL+9eOuXHU1yNE3uCwU";
moby.root = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1N/IT3nQYUD+dBlU1sTEEVMxfOyMkrrDeyHcYgnJvw";
servo.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPS1qFzKurAdB9blkWomq8gI1g0T3sTs9LsmFOj5VtqX";
servo.root = root;
};
};
}

View File

@@ -2,19 +2,22 @@
{
imports = [
./allocations.nix
./feeds.nix
./fs
./gui
./home-manager
./ids.nix
./packages.nix
./image.nix
./impermanence
./nixcache.nix
./persist
./services
./sops.nix
./ssh.nix
];
_module.args = {
sane-lib = import ./lib { inherit lib utils; };
sane-data = import ./data { inherit lib; };
};
}

51
modules/feeds.nix Normal file
View File

@@ -0,0 +1,51 @@
{ lib, ... }:
with lib;
let
feed = types.submodule ({ config, ... }: {
options = {
freq = mkOption {
type = types.enum [ "hourly" "daily" "weekly" "infrequent" ];
default = "infrequent";
};
cat = mkOption {
type = types.enum [ "art" "humor" "pol" "rat" "tech" "uncat" ];
default = "uncat";
};
format = mkOption {
type = types.enum [ "text" "image" "podcast" ];
default = "text";
};
url = mkOption {
type = types.str;
description = ''
url to a RSS feed
'';
};
substack = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
if the feed is a substack domain, just enter the subdomain here and the url/format field can be populated automatically
'';
};
};
config = lib.mkIf (config.substack != null) {
url = "https://${config.substack}.substack.com/feed";
format = "text";
};
});
in
{
# we don't explicitly generate anything from the feeds here.
# instead, config.sane.feeds is used by a variety of services at their definition site.
options = {
sane.feeds = mkOption {
type = types.listOf feed;
default = [];
description = ''
RSS feeds indexed by a human-readable name.
'';
};
};
}

View File

@@ -21,7 +21,7 @@ let
default = null;
};
symlink = mkOption {
type = types.nullOr symlinkEntry;
type = types.nullOr (symlinkEntryFor name);
default = null;
};
generated = mkOption {
@@ -81,6 +81,9 @@ let
# make the unit file which generates the underlying thing available so that `mount` can use it.
generated.unit = (serviceNameFor name) + ".service";
# if we were asked to mount, make sure we create the dir that we mount over
dir = lib.mkIf (config.mount != null) {};
# 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.
@@ -108,15 +111,25 @@ let
# takes no special options
dirEntry = types.submodule propagatedGenerateMod;
symlinkEntry = types.submodule {
symlinkEntryFor = path: types.submodule ({ config, ...}: {
options = {
inherit (propagatedGenerateMod.options) acl;
target = mkOption {
type = types.str;
type = types.coercedTo types.package toString types.str;
description = "fs path to link to";
};
text = mkOption {
type = types.nullOr types.str;
default = null;
description = "create a file in the /nix/store with the provided text and use that as the target";
};
};
};
config = {
target = lib.mkIf (config.text != null) (
pkgs.writeText (path-lib.leaf path) config.text
);
};
});
generatedEntry = types.submodule {
options = {
@@ -222,25 +235,11 @@ let
});
mkFsConfig = path: opt: mergeTopLevel [
mkFsConfig = path: opt: lib.mkMerge [
(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"
@@ -295,19 +294,17 @@ let
lnfrom="$1"
lnto="$2"
ln -sf --no-dereference "$lnto" "$lnfrom"
# ln is clever when there's something else at the place we want to create the link
# only create the link if nothing's there or what is there is another link,
# otherwise you'll get links at unexpected fs locations
! test -e "$lnfrom" || test -L "$lnfrom" && 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
[ ]
;
ancestorsOf = path: lib.init (path-lib.walk "/" path);
# 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"`
@@ -349,5 +346,12 @@ in {
};
};
config = mergeTopLevel (lib.mapAttrsToList mkFsConfig cfg);
config =
let
configs = lib.mapAttrsToList mkFsConfig cfg;
take = f: {
systemd.services = f.systemd.services;
fileSystems = f.fileSystems;
};
in take (sane-lib.mkTypedMerge take configs);
}

View File

@@ -23,7 +23,9 @@ in
config = lib.mkIf cfg.enable {
sane.packages.enableGuiPkgs = lib.mkDefault true;
# all GUIs use network manager?
users.users.nm-iodine.uid = config.sane.allocations.nm-iodine-uid;
# preserve backlight brightness across power cycles
# see `man systemd-backlight`
sane.persist.sys.plaintext = [ "/var/lib/systemd/backlight" ];
};
}

View File

@@ -15,15 +15,6 @@ in
config = mkIf cfg.enable {
sane.gui.enable = true;
users.users.avahi.uid = config.sane.allocations.avahi-uid;
users.groups.avahi.gid = config.sane.allocations.avahi-gid;
users.users.colord.uid = config.sane.allocations.colord-uid;
users.groups.colord.gid = config.sane.allocations.colord-gid;
users.users.geoclue.uid = config.sane.allocations.geoclue-uid;
users.groups.geoclue.gid = config.sane.allocations.geoclue-gid;
users.users.rtkit.uid = config.sane.allocations.rtkit-uid;
users.groups.rtkit.gid = config.sane.allocations.rtkit-gid;
# start gnome/gdm on boot
services.xserver.enable = true;
services.xserver.desktopManager.gnome.enable = true;

View File

@@ -24,16 +24,6 @@ in
{
sane.gui.enable = true;
users.users.avahi.uid = config.sane.allocations.avahi-uid;
users.users.colord.uid = config.sane.allocations.colord-uid;
users.users.geoclue.uid = config.sane.allocations.geoclue-uid;
users.users.rtkit.uid = config.sane.allocations.rtkit-uid;
users.groups.avahi.gid = config.sane.allocations.avahi-gid;
users.groups.colord.gid = config.sane.allocations.colord-gid;
users.groups.feedbackd.gid = config.sane.allocations.feedbackd-gid;
users.groups.geoclue.gid = config.sane.allocations.geoclue-gid;
users.groups.rtkit.gid = config.sane.allocations.rtkit-gid;
# docs: https://github.com/NixOS/nixpkgs/blob/nixos-22.05/nixos/modules/services/x11/desktop-managers/phosh.nix
services.xserver.desktopManager.phosh = {
enable = true;

View File

@@ -2,6 +2,7 @@ https://search.nixos.org/options?channel=unstable&query=
https://search.nixos.org/packages?channel=unstable&query=
https://nixos.wiki/index.php?go=Go&search=
https://github.com/nixos/nixpkgs/pulls?q=
https://nur.nix-community.org/
https://nix-community.github.io/home-manager/options.html
https://w.uninsane.org/viewer#search?books.name=wikipedia_en_all_maxi_2022-05&pattern=
https://jackett.uninsane.org/UI/Dashboard#search=

View File

@@ -22,14 +22,15 @@ in
};
config = mkIf cfg.enable {
sane.gui.enable = true;
users.users.greeter.uid = config.sane.allocations.greeter-uid;
users.groups.greeter.gid = config.sane.allocations.greeter-gid;
programs.sway = {
# we configure sway with home-manager, but this enable gets us e.g. opengl and fonts
enable = true;
};
# alternatively, could use SDDM
# instead of using `services.greetd`, can instead use SDDM by swapping in these lines.
# services.xserver.displayManager.sddm.enable = true;
# services.xserver.enable = true;
services.greetd = let
swayConfig-greeter = pkgs.writeText "greetd-sway-config" ''
# `-l` activates layer-shell mode.
@@ -71,13 +72,24 @@ in
pulse.enable = true;
};
hardware.bluetooth.enable = true;
services.blueman.enable = true;
networking.useDHCP = false;
networking.networkmanager.enable = true;
networking.wireless.enable = lib.mkForce false;
hardware.bluetooth.enable = true;
services.blueman.enable = true;
# gsd provides Rfkill, which is required for the bluetooth pane in gnome-control-center to work
services.gnome.gnome-settings-daemon.enable = true;
# start the components of gsd we need at login
systemd.user.targets."org.gnome.SettingsDaemon.Rfkill".wantedBy = [ "graphical-session.target" ];
# go ahead and `systemctl --user cat gnome-session-initialized.target`. i dare you.
# the only way i can figure out how to get Rfkill to actually load is to just disable all the shit it depends on.
# it doesn't actually seem to need ANY of them in the first place T_T
systemd.user.targets."gnome-session-initialized".enable = false;
# bluez can't connect to audio devices unless pipewire is running.
# a system service can't depend on a user service, so just launch it at graphical-session
systemd.user.services."pipewire".wantedBy = [ "graphical-session.target" ];
sane.home-manager.windowManager.sway = {
enable = true;
wrapperFeatures.gtk = true;

View File

@@ -1,5 +1,5 @@
# Terminal UI mail client
{ config, lib, ... }:
{ config, lib, sane-lib, ... }:
lib.mkIf config.sane.home-manager.enable
{
@@ -8,9 +8,5 @@ lib.mkIf config.sane.home-manager.enable
sopsFile = ../../secrets/universal/aerc_accounts.conf;
format = "binary";
};
home-manager.users.colin = let sysconfig = config; in { config, ... }: {
# aerc TUI mail client
xdg.configFile."aerc/accounts.conf".source =
config.lib.file.mkOutOfStoreSymlink sysconfig.sops.secrets.aerc_accounts.path;
};
sane.fs."/home/colin/.config/aerc/accounts.conf" = sane-lib.fs.wantedSymlinkTo config.sops.secrets.aerc_accounts.path;
}

View File

@@ -11,17 +11,19 @@ let
cfg = config.sane.home-manager;
# extract `pkg` from `sane.packages.enabledUserPkgs`
pkg-list = pkgspec: builtins.map (e: e.pkg) pkgspec;
feeds = import ./feeds.nix { inherit lib; };
in
{
imports = [
./aerc.nix
./firefox.nix
./gfeeds.nix
./git.nix
./gpodder.nix
./keyring.nix
./kitty.nix
./mpv.nix
./nb.nix
./neovim.nix
./newsflash.nix
./splatmoji.nix
./ssh.nix
./sublime-music.nix
@@ -67,14 +69,6 @@ in
home.username = "colin";
home.homeDirectory = "/home/colin";
home.activation = {
initKeyring = {
after = ["writeBoundary"];
before = [];
data = "${../../scripts/init-keyring}";
};
};
# XDG defines things like ~/Desktop, ~/Downloads, etc.
# these clutter the home, so i mostly don't use them.
xdg.userDirs = {
@@ -94,7 +88,7 @@ in
# - `xdg-mime query filetype path/to/thing.ext`
xdg.mimeApps.enable = true;
xdg.mimeApps.defaultApplications = let
www = sysconfig.sane.web-browser.desktop;
www = sysconfig.sane.web-browser.browser.desktop;
pdf = "org.gnome.Evince.desktop";
md = "obsidian.desktop";
thumb = "org.gnome.gThumb.desktop";
@@ -137,54 +131,14 @@ in
# <item oor:path="/org.openoffice.Setup/Product"><prop oor:name="LastTimeGetInvolvedShown" oor:op="fuse"><value>1667693880</value></prop></item>
xdg.configFile."gpodderFeeds.opml".text = with feeds;
feedsToOpml feeds.podcasts;
# news-flash RSS viewer
xdg.configFile."newsflashFeeds.opml".text = with feeds;
feedsToOpml (feeds.texts ++ feeds.images);
# gnome feeds RSS viewer
xdg.configFile."org.gabmus.gfeeds.json".text =
let
myFeeds = feeds.texts ++ feeds.images;
in builtins.toJSON {
# feed format is a map from URL to a dict,
# with dict["tags"] a list of string tags.
feeds = builtins.foldl' (acc: feed: acc // {
"${feed.url}".tags = [ feed.cat feed.freq ];
}) {} myFeeds;
dark_reader = false;
new_first = true;
# windowsize = {
# width = 350;
# height = 650;
# };
max_article_age_days = 90;
enable_js = false;
max_refresh_threads = 3;
# saved_items = {};
# read_items = [];
show_read_items = true;
full_article_title = true;
# views: "webview", "reader", "rsscont"
default_view = "rsscont";
open_links_externally = true;
full_feed_name = false;
refresh_on_startup = true;
tags = lib.lists.unique (
(builtins.catAttrs "cat" myFeeds) ++ (builtins.catAttrs "freq" myFeeds)
);
open_youtube_externally = false;
media_player = "vlc"; # default: mpv
};
programs = {
home-manager.enable = true; # this lets home-manager manage dot-files in user dirs, i think
# "command not found" will cause the command to be searched in nixpkgs
nix-index.enable = true;
} // cfg.programs;
programs = lib.mkMerge [
{
home-manager.enable = true; # this lets home-manager manage dot-files in user dirs, i think
# "command not found" will cause the command to be searched in nixpkgs
nix-index.enable = true;
}
cfg.programs
];
};
};
}

View File

@@ -6,7 +6,7 @@
# many of the settings below won't have effect without those patches.
# see: https://gitlab.com/librewolf-community/settings/-/blob/master/distribution/policies.json
{ config, lib, pkgs, ...}:
{ config, lib, pkgs, sane-lib, ...}:
with lib;
let
cfg = config.sane.web-browser;
@@ -32,11 +32,11 @@ let
defaultSettings = firefoxSettings;
# defaultSettings = librewolfSettings;
package = pkgs.wrapFirefox cfg.browser {
package = pkgs.wrapFirefox cfg.browser.browser {
# inherit the default librewolf.cfg
# it can be further customized via ~/.librewolf/librewolf.overrides.cfg
inherit (pkgs.librewolf-unwrapped) extraPrefsFiles;
inherit (cfg) libName;
inherit (cfg.browser) libName;
extraNativeMessagingHosts = [ pkgs.browserpass ];
# extraNativeMessagingHosts = [ pkgs.gopass-native-messaging-host ];
@@ -105,43 +105,57 @@ let
in
{
options = {
sane.web-browser = mkOption {
sane.web-browser.browser = mkOption {
default = defaultSettings;
type = types.attrs;
};
sane.web-browser.persistData = mkOption {
description = "optional store name to which persist browsing data (like history)";
type = types.nullOr types.str;
default = null;
};
sane.web-browser.persistCache = mkOption {
description = "optional store name to which persist browser cache";
type = types.nullOr types.str;
default = "cryptClearOnBoot";
};
};
config = lib.mkIf config.sane.home-manager.enable {
# XXX: although home-manager calls this option `firefox`, we can use other browsers and it still mostly works.
home-manager.users.colin = lib.mkIf (config.sane.gui.enable) {
programs.firefox = {
enable = true;
inherit package;
};
# uBlock filter list configuration.
# specifically, enable the GDPR cookie prompt blocker.
# data.toOverwrite.filterLists is additive (i.e. it supplements the default filters)
# this configuration method is documented here:
# - <https://github.com/gorhill/uBlock/issues/2986#issuecomment-364035002>
# the specific attribute path is found via scraping ublock code here:
# - <https://github.com/gorhill/uBlock/blob/master/src/js/storage.js>
# - <https://github.com/gorhill/uBlock/blob/master/assets/assets.json>
home.file."${cfg.dotDir}/managed-storage/uBlock0@raymondhill.net.json".text = ''
{
"name": "uBlock0@raymondhill.net",
"description": "ignored",
"type": "storage",
"data": {
"toOverwrite": "{\"filterLists\": [\"fanboy-cookiemonster\"]}"
}
}
'';
home.file."${cfg.dotDir}/${cfg.libName}.overrides.cfg".text = ''
// if we can't query the revocation status of a SSL cert because the issuer is offline,
// treat it as unrevoked.
// see: <https://librewolf.net/docs/faq/#im-getting-sec_error_ocsp_server_error-what-can-i-do>
defaultPref("security.OCSP.require", false);
'';
config = lib.mkIf config.sane.home-manager.enable {
# uBlock filter list configuration.
# specifically, enable the GDPR cookie prompt blocker.
# data.toOverwrite.filterLists is additive (i.e. it supplements the default filters)
# this configuration method is documented here:
# - <https://github.com/gorhill/uBlock/issues/2986#issuecomment-364035002>
# the specific attribute path is found via scraping ublock code here:
# - <https://github.com/gorhill/uBlock/blob/master/src/js/storage.js>
# - <https://github.com/gorhill/uBlock/blob/master/assets/assets.json>
sane.fs."/home/colin/${cfg.browser.dotDir}/managed-storage/uBlock0@raymondhill.net.json" = sane-lib.fs.wantedText ''
{
"name": "uBlock0@raymondhill.net",
"description": "ignored",
"type": "storage",
"data": {
"toOverwrite": "{\"filterLists\": [\"fanboy-cookiemonster\"]}"
}
}
'';
sane.fs."/home/colin/${cfg.browser.dotDir}/${cfg.browser.libName}.overrides.cfg" = sane-lib.fs.wantedText ''
// if we can't query the revocation status of a SSL cert because the issuer is offline,
// treat it as unrevoked.
// see: <https://librewolf.net/docs/faq/#im-getting-sec_error_ocsp_server_error-what-can-i-do>
defaultPref("security.OCSP.require", false);
'';
sane.packages.extraGuiPkgs = [ package ];
# flood the cache to disk to avoid it taking up too much tmp
sane.persist.home.byPath."${cfg.browser.cacheDir}" = lib.mkIf (cfg.persistCache != null) {
store = cfg.persistCache;
};
sane.persist.home.byPath."${cfg.browser.dotDir}" = lib.mkIf (cfg.persistData != null) {
store = cfg.persistData;
};
};
}

View File

@@ -0,0 +1,42 @@
# gnome feeds RSS viewer
{ config, lib, sane-lib, ... }:
let
feeds = sane-lib.feeds;
all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat ["text" "image"] all-feeds;
in {
sane.fs."/home/colin/.config/org.gabmus.gfeeds.json" = sane-lib.fs.wantedText (
builtins.toJSON {
# feed format is a map from URL to a dict,
# with dict["tags"] a list of string tags.
feeds = sane-lib.mapToAttrs (feed: {
name = feed.url;
value.tags = [ feed.cat feed.freq ];
}) wanted-feeds;
dark_reader = false;
new_first = true;
# windowsize = {
# width = 350;
# height = 650;
# };
max_article_age_days = 90;
enable_js = false;
max_refresh_threads = 3;
# saved_items = {};
# read_items = [];
show_read_items = true;
full_article_title = true;
# views: "webview", "reader", "rsscont"
default_view = "rsscont";
open_links_externally = true;
full_feed_name = false;
refresh_on_startup = true;
tags = lib.unique (
(builtins.catAttrs "cat" wanted-feeds) ++ (builtins.catAttrs "freq" wanted-feeds)
);
open_youtube_externally = false;
media_player = "vlc"; # default: mpv
}
);
}

View File

@@ -0,0 +1,12 @@
# gnome feeds RSS viewer
{ config, sane-lib, ... }:
let
feeds = sane-lib.feeds;
all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat ["podcast"] all-feeds;
in {
sane.fs."/home/colin/.config/gpodderFeeds.opml" = sane-lib.fs.wantedText (
feeds.feedsToOpml wanted-feeds
);
}

View File

@@ -0,0 +1,11 @@
{ config, lib, sane-lib, ... }:
lib.mkIf config.sane.home-manager.enable
{
sane.persist.home.private = [ ".local/share/keyrings" ];
sane.fs."/home/colin/private/.local/share/keyrings/default" = {
generated.script.script = builtins.readFile ../../scripts/init-keyring;
wantedBy = [ config.sane.fs."/home/colin/private".unit ];
};
}

View File

@@ -1,27 +0,0 @@
# nb is a CLI-drive Personal Knowledge Manager
# - <https://xwmx.github.io/nb/>
#
# it's pretty opinionated:
# - autocommits (to git) excessively (disable-able)
# - inserts its own index files to give deterministic names to files
#
# it offers a primitive web-server
# and it offers some CLI query tools
{ config, lib, pkgs, ... }:
# lib.mkIf config.sane.home-manager.enable
lib.mkIf false # XXX disabled!
{
sane.packages.extraUserPkgs = [ pkgs.nb ];
home-manager.users.colin = { config, ... }: {
# nb markdown/personal knowledge manager
home.file.".nb/knowledge".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/knowledge";
home.file.".nb/.current".text = "knowledge";
home.file.".nbrc".text = ''
# manage with `nb settings`
export NB_AUTO_SYNC=0
'';
};
}

View File

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

View File

@@ -0,0 +1,12 @@
# news-flash RSS viewer
{ config, sane-lib, ... }:
let
feeds = sane-lib.feeds;
all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat ["text" "image"] all-feeds;
in {
sane.fs."/home/colin/.config/newsflashFeeds.opml" = sane-lib.fs.wantedText (
feeds.feedsToOpml wanted-feeds
);
}

View File

@@ -1,20 +1,19 @@
# borrows from:
# - default config: <https://github.com/cspeterson/splatmoji/blob/master/splatmoji.config>
# - wayland: <https://github.com/cspeterson/splatmoji/issues/32#issuecomment-830862566>
{ pkgs, ... }:
{ pkgs, sane-lib, ... }:
{
home-manager.users.colin = {
xdg.configFile."splatmoji/splatmoji.config".text = ''
history_file=/home/colin/.local/state/splatmoji/history
history_length=5
# TODO: wayland equiv
paste_command=xdotool key ctrl+v
# rofi_command=${pkgs.wofi}/bin/wofi --dmenu --insensitive --cache-file /dev/null
rofi_command=${pkgs.fuzzel}/bin/fuzzel -d -i -w 60
xdotool_command=${pkgs.wtype}/bin/wtype
# TODO: wayland equiv
xsel_command=xsel -b -i
'';
};
sane.persist.home.plaintext = [ ".local/state/splatmoji" ];
sane.fs."/home/colin/.config/splatmoji/splatmoji.config" = sane-lib.fs.wantedText ''
history_file=/home/colin/.local/state/splatmoji/history
history_length=5
# TODO: wayland equiv
paste_command=xdotool key ctrl+v
# rofi_command=${pkgs.wofi}/bin/wofi --dmenu --insensitive --cache-file /dev/null
rofi_command=${pkgs.fuzzel}/bin/fuzzel -d -i -w 60
xdotool_command=${pkgs.wtype}/bin/wtype
# TODO: wayland equiv
xsel_command=xsel -b -i
'';
}

View File

@@ -1,20 +1,23 @@
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, sane-lib, ... }:
lib.mkIf config.sane.home-manager.enable
{
home-manager.users.colin = let
host = config.networking.hostName;
user_pubkey = (import ../pubkeys.nix).users."${host}";
known_hosts_text = builtins.concatStringsSep
"\n"
(builtins.attrValues (import ../pubkeys.nix).hosts);
in { config, ...}: {
# ssh key is stored in private storage
home.file.".ssh/id_ed25519".source = config.lib.file.mkOutOfStoreSymlink "/home/colin/private/.ssh/id_ed25519";
home.file.".ssh/id_ed25519.pub".text = user_pubkey;
with lib;
let
host = config.networking.hostName;
user-pubkey = config.sane.ssh.pubkeys."colin@${host}".asUserKey;
host-keys = filter (k: k.user == "root") (attrValues config.sane.ssh.pubkeys);
known-hosts-text = concatStringsSep
"\n"
(map (k: k.asHostKey) host-keys)
;
in lib.mkIf config.sane.home-manager.enable {
# ssh key is stored in private storage
sane.persist.home.private = [ ".ssh/id_ed25519" ];
sane.fs."/home/colin/.ssh/id_ed25519.pub" = sane-lib.fs.wantedText user-pubkey;
sane.fs."/home/colin/.ssh/known_hosts" = sane-lib.fs.wantedText known-hosts-text;
programs.ssh.enable = true;
# this optionally accepts multiple known_hosts paths, separated by space.
programs.ssh.userKnownHostsFile = builtins.toString (pkgs.writeText "known_hosts" known_hosts_text);
};
users.users.colin.openssh.authorizedKeys.keys =
let
user-keys = filter (k: k.user == "colin") (attrValues config.sane.ssh.pubkeys);
in
map (k: k.asUserKey) user-keys;
}

View File

@@ -1,4 +1,4 @@
{ config, lib, ... }:
{ config, lib, sane-lib, ... }:
lib.mkIf config.sane.home-manager.enable
{
@@ -8,9 +8,5 @@ lib.mkIf config.sane.home-manager.enable
sopsFile = ../../secrets/universal/sublime_music_config.json.bin;
format = "binary";
};
home-manager.users.colin = let sysconfig = config; in { config, ... }: {
# sublime music player
xdg.configFile."sublime-music/config.json".source =
config.lib.file.mkOutOfStoreSymlink sysconfig.sops.secrets.sublime_music_config.path;
};
sane.fs."/home/colin/.config/sublime-music/config.json" = sane-lib.fs.wantedSymlinkTo config.sops.secrets.sublime_music_config.path;
}

View File

@@ -1,16 +1,18 @@
{ config, lib, ... }:
{ config, lib, sane-lib, ... }:
let
feeds = sane-lib.feeds;
all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat ["podcast"] all-feeds;
podcast-urls = lib.concatStringsSep "|" (
builtins.map (feed: feed.url) wanted-feeds
);
in
lib.mkIf config.sane.home-manager.enable
{
home-manager.users.colin.xdg.configFile."vlc/vlcrc".text =
let
feeds = import ./feeds.nix { inherit lib; };
podcastUrls = lib.strings.concatStringsSep "|" (
builtins.map (feed: feed.url) feeds.podcasts
);
in ''
sane.fs."/home/colin/.config/vlc/vlcrc" = sane-lib.fs.wantedText ''
[podcast]
podcast-urls=${podcastUrls}
podcast-urls=${podcast-urls}
[core]
metadata-network-access=0
[qt]

View File

@@ -2,7 +2,7 @@
lib.mkIf config.sane.home-manager.enable
{
sane.impermanence.dirs.home.plaintext = [
sane.persist.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?

89
modules/ids.nix Normal file
View File

@@ -0,0 +1,89 @@
{ lib, config, ... }:
with lib;
let
cfg = config.sane.ids;
id = types.submodule {
options = {
uid = mkOption {
type = types.nullOr types.int;
default = null;
};
gid = mkOption {
type = types.nullOr types.int;
default = null;
};
};
};
userOpts = { name, ... }: {
config =
let
ent-ids = cfg."${name}" or {};
uid = ent-ids.uid or null;
in
{
uid = lib.mkIf (uid != null) uid;
};
};
groupOpts = { name, ... }: {
config =
let
ent-ids = cfg."${name}" or {};
gid = ent-ids.gid or null;
in
{
gid = lib.mkIf (gid != null) gid;
};
};
in
{
options = {
sane.ids = mkOption {
type = types.attrsOf id;
default = {};
description = ''
mapping from user/group name to gids/uids you expect that entity to have.
for users/groups created elsewhere *without* an id, this is used to provide them a fixed/stable id.
'';
};
# these get merged with the nixpkgs options.
users.users = mkOption {
type = types.attrsOf (types.submodule userOpts);
};
users.groups = mkOption {
type = types.attrsOf (types.submodule groupOpts);
};
};
config = {
# guarantee determinism in uid/gid generation for users:
assertions = lib.mkMerge [
(
lib.mapAttrsToList
(name: user: {
assertion = user.uid != null;
message = "non-deterministic uid detected for: ${name}";
})
config.users.users
)
(
lib.mapAttrsToList
(name: group: {
assertion = group.gid != null;
message = "non-deterministic gid detected for: ${name}";
})
config.users.groups
)
(
lib.mapAttrsToList
(name: user: {
assertion = !user.autoSubUidGidRange;
message = "non-deterministic subUids/Guids detected for: ${name}";
})
config.users.users
)
];
};
}

View File

@@ -79,7 +79,9 @@ in
"ext4" = pkgs.imageBuilder.fileSystem.makeExt4;
"btrfs" = pkgs.imageBuilder.fileSystem.makeBtrfs;
};
in {
in
lib.mkIf cfg.enable
{
system.build.img-without-firmware = with pkgs; imageBuilder.diskImage.makeGPT {
name = "nixos";
diskID = vfatUuidFromFs bootFs;

View File

@@ -1,191 +0,0 @@
# borrows from:
# 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, sane-lib, ... }:
with lib;
let
path = sane-lib.path;
sane-types = sane-lib.types;
cfg = config.sane.impermanence;
storeType = types.submodule {
options = {
storeDescription = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
an optional description of the store, which is rendered like
{store.name}: {store.storeDescription}
for example, a store named "private" could have description "ecnrypted to the user's password and decrypted on login".
'';
};
origin = mkOption {
type = types.str;
};
prefix = mkOption {
type = types.str;
default = "/";
description = ''
optional prefix to strip from children when stored here.
for example, prefix="/var/private" and mountpoint="/mnt/crypt/private"
would cause /var/private/www/root to be stored at /mnt/crypt/private/www/root instead of
/mnt/crypt/private/var/private/www/root.
'';
};
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.
'';
};
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.
'';
};
};
};
# 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;
};
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 = {
sane.impermanence.enable = mkOption {
default = false;
type = types.bool;
};
sane.impermanence.root-on-tmpfs = mkOption {
default = false;
type = types.bool;
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.
'';
};
};
imports = [
./root-on-tmpfs.nix
./stores
];
config = let
cfgFor = opt:
let
store = opt.store;
store-rel-path = path.from store.prefix opt.directory;
backing-path = path.concat [ store.origin store-rel-path ];
# pass through the perm/mode overrides
dir-acl = sane-lib.filterNonNull {
inherit (opt) user group mode;
};
in {
# create destination and backing directory, with correct perms
sane.fs."${opt.directory}" = {
# inherit perms & make sure we don't mount until after the mount point is setup correctly.
dir.acl = dir-acl;
mount.bind = backing-path;
inherit (store.defaultOrdering) wantedBy wantedBeforeBy;
};
sane.fs."${backing-path}" = {
# ensure the backing path has same perms as the mount point.
# TODO: maybe we want to do this, crawling all the way up to the store base?
# that would simplify (remove) the code in stores/default.nix
dir.acl = config.sane.fs."${opt.directory}".generated.acl;
};
};
in mkIf cfg.enable {
sane.fs = lib.mkMerge (map (d: (cfgFor d).sane.fs) cfg.dirs.all);
};
}

View File

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

@@ -1,8 +1,72 @@
{ lib, ... }@moduleArgs:
{
let
sane-lib = rec {
feeds = import ./feeds.nix moduleArgs;
fs = import ./fs.nix moduleArgs;
merge = import ./merge.nix ({ inherit sane-lib; } // moduleArgs);
path = import ./path.nix moduleArgs;
types = import ./types.nix moduleArgs;
# re-exports
inherit (merge) mkTypedMerge;
# like `builtins.listToAttrs` but any duplicated `name` throws error on access.
# Type: listToDisjointAttrs :: [{ name :: String, value :: Any }] -> AttrSet
listToDisjointAttrs = l: joinAttrsets (builtins.map nameValueToAttrs l);
# true if p is a prefix of l (even if p == l)
# Type: isPrefixOfList :: [Any] -> [Any] -> bool
isPrefixOfList = p: l: (lib.sublist 0 (lib.length p) l) == p;
# merges N attrsets
# Type: flattenAttrsList :: [AttrSet] -> AttrSet
joinAttrsets = l: lib.foldl' lib.attrsets.unionOfDisjoint {} l;
# evaluate a `{ name, value }` pair in the same way that `listToAttrs` does.
# Type: nameValueToAttrs :: { name :: String, value :: Any } -> Any
nameValueToAttrs = { name, value }: {
"${name}" = value;
};
# if `maybe-null` is non-null, yield that. else, return the `default`.
withDefault = default: maybe-null: if maybe-null != null then
maybe-null
else
default;
# removes null entries from the provided AttrSet. acts recursively.
# Type: filterNonNull :: AttrSet -> AttrSet
filterNonNull = attrs: lib.filterAttrsRecursive (n: v: v != null) attrs;
}
# return only the subset of `attrs` whose name is in the provided set.
# Type: filterByName :: [String] -> AttrSet
filterByName = names: attrs: lib.filterAttrs
(name: value: builtins.elem name names)
attrs;
# transform a list into an AttrSet via a function which maps an element to a { name, value } pair.
# it's an error for the same name to be specified more than once
# Type: mapToAttrs :: (a -> { name :: String, value :: Any }) -> [a] -> AttrSet
mapToAttrs = f: list: listToDisjointAttrs (builtins.map f list);
# flatten a nested AttrSet into a list of { path = [String]; value } items.
# the output contains only non-attr leafs.
# so e.g. { a.b = 1; } -> [ { path = ["a" "b"]; value = 1; } ]
# but e.g. { a.b = {}; } -> []
#
# Type: flattenAttrs :: AttrSet[AttrSet|Any] -> [{ path :: String, value :: Any }]
flattenAttrs = flattenAttrs' [];
flattenAttrs' = path: value: if builtins.isAttrs value then (
builtins.concatLists (
lib.mapAttrsToList
(name: flattenAttrs' (path ++ [ name ]))
value
)
) else [
{
inherit path value;
}
];
};
in sane-lib

38
modules/lib/feeds.nix Normal file
View File

@@ -0,0 +1,38 @@
{ lib, ... }:
rec {
# PRIMARY API: generate a OPML file from a list of feeds
feedsToOpml = feeds: opmlTopLevel (opmlGroups (partitionByCat feeds));
# only keep feeds whose category is one of the provided
filterByFormat = fmts: builtins.filter (feed: builtins.elem feed.format fmts);
## INTERNAL APIS
# transform a list of feeds into an attrs mapping cat => [ feed0 feed1 ... ]
partitionByCat = feeds: builtins.groupBy (f: f.cat) feeds;
# represents a single RSS feed.
opmlTerminal = feed: ''<outline xmlUrl="${feed.url}" type="rss"/>'';
# a list of RSS feeds.
opmlTerminals = feeds: lib.concatStringsSep "\n" (builtins.map opmlTerminal feeds);
# one node which packages some flat grouping of terminals.
opmlGroup = title: feeds: ''
<outline text="${title}" title="${title}">
${opmlTerminals feeds}
</outline>
'';
# a list of groups (`groupMap` is an attrs mapping groupName => [ feed0 feed1 ... ]).
opmlGroups = groupMap: lib.concatStringsSep "\n" (
builtins.attrValues (builtins.mapAttrs opmlGroup groupMap)
);
# top-level OPML file which could be consumed by something else.
opmlTopLevel = body: ''
<?xml version="1.0" encoding="utf-8"?>
<opml version="2.0">
<body>
${body}
</body>
</opml>
'';
}

9
modules/lib/fs.nix Normal file
View File

@@ -0,0 +1,9 @@
{ lib, ... }:
rec {
wanted = lib.attrsets.unionOfDisjoint { wantedBeforeBy = [ "multi-user.target" ]; };
wantedSymlink = symlink: wanted { inherit symlink; };
wantedSymlinkTo = target: wantedSymlink { inherit target; };
wantedText = text: wantedSymlink { inherit text; };
}

100
modules/lib/merge.nix Normal file
View File

@@ -0,0 +1,100 @@
{ lib, sane-lib, ... }:
rec {
# type-checked `lib.mkMerge`, intended to be usable at the top of a file.
# `take` is a function which defines a spec enforced against every item to be merged.
# for example:
# take = f: { x = f.x; y.z = f.y.z; };
# - the output is guaranteed to have an `x` attribute and a `y.z` attribute and nothing else.
# - each output is a `lib.mkMerge` of the corresponding paths across the input lists.
# - if an item in the input list defines an attr not captured by `f`, this function will throw.
#
# Type: mkTypedMerge :: (Attrs -> Attrs) -> [Attrs] -> Attrs
mkTypedMerge = take: l:
let
pathsToMerge = findTerminalPaths take [];
discharged = dischargeAll l pathsToMerge;
merged = builtins.map (p: lib.setAttrByPath p (mergeAtPath p discharged)) pathsToMerge;
in
assert builtins.all (assertNoExtraPaths pathsToMerge) discharged;
sane-lib.joinAttrsets merged;
# `take` is as in mkTypedMerge. this function queries which items `take` is interested in.
# for example:
# take = f: { x = f.x; y.z = f.y.z; };
# - for `path == []` we return the toplevel attr names: [ "x" "y"]
# - for `path == [ "y" ]` we return [ "z" ]
# - for `path == [ "x" ]` or `path == [ "y" "z" ]` we return []
#
# Type: findSubNames :: (Attrs -> Attrs) -> [String] -> [String]
findSubNames = take: path:
let
# define the current path, but nothing more.
curLevel = lib.setAttrByPath path {};
# `take curLevel` will act one of two ways here:
# - { $path = f.$path; } => { $path = {}; };
# - { $path.subAttr = f.$path.subAttr; } => { $path = { subAttr = ?; }; }
# so, index $path into the output of `take`,
# and if it has any attrs (like `subAttr`) that means we're interested in those too.
nextLevel = lib.getAttrFromPath path (take curLevel);
in
builtins.attrNames nextLevel;
# computes a list of all terminal paths that `take` is interested in,
# where each path is a list of attr names to descend to reach that terminal.
# Type: findTerminalPaths :: (Attrs -> Attrs) -> [String] -> [[String]]
findTerminalPaths = take: path:
let
subNames = findSubNames take path;
in if subNames == [] then
[ path ]
else
lib.concatMap (name: findTerminalPaths take (path ++ [name])) subNames;
# ensures that all nodes in the attrset from the root to and including the given path
# are ordinary attrs -- if they exist.
# this has to return a list of Attrs, in case any portion of the path was previously merged.
# by extension, each returned item is a subset of the original item, and might not have *all* the paths that the original has.
# Type: dischargeToPath :: [String] -> Attrs -> [Attrs]
dischargeToPath = path: i:
let
items = lib.pushDownProperties i;
# now items is a list where every element is undecorated at the toplevel.
# e.g. each item is an ordinary attrset or primitive.
# we still need to discharge the *rest* of the path though, for every item.
name = lib.head path;
downstream = lib.tail path;
dischargeDownstream = it: if path != [] && it ? name then
builtins.map (v: it // { "${name}" = v; }) (dischargeToPath downstream it."${name}")
else
[ it ];
in
lib.concatMap dischargeDownstream items;
# discharge many items but only over one path.
# Type: dischargeItemsToPaths :: [Attrs] -> String -> [Attrs]
dischargeItemsToPath = l: path: builtins.concatMap (dischargeToPath path) l;
# Type: dischargeAll :: [Attrs] -> [String] -> [Attrs]
dischargeAll = l: paths:
builtins.foldl' dischargeItemsToPath l paths;
# merges all present values for the provided path
# Type: mergeAtPath :: [String] -> [Attrs] -> (lib.mkMerge)
mergeAtPath = path: l:
let
itemsToMerge = builtins.filter (lib.hasAttrByPath path) l;
in lib.mkMerge (builtins.map (lib.getAttrFromPath path) itemsToMerge);
# check that attrset `i` contains no terminals other than those specified in (or direct ancestors of) paths
assertNoExtraPaths = paths: i:
let
# since the act of discharging should have forced all the relevant data out to the leaves,
# we just set each expected terminal to null (initializing the parents when necessary)
# and that gives a standard value for any fully-consumed items that we can do equality comparisons with.
wipePath = acc: path: lib.recursiveUpdate acc (lib.setAttrByPath path null);
remainder = builtins.foldl' wipePath i paths;
expected-remainder = builtins.foldl' wipePath {} paths;
in
assert remainder == expected-remainder; true;
}

View File

@@ -1,6 +1,7 @@
{ 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:
@@ -19,6 +20,10 @@ let path = rec {
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"
# return the last path component; error on the empty path
leaf = str: lib.last (split str);
from = start: end: let
s = path.norm start;
e = path.norm end;
@@ -26,5 +31,14 @@ let path = rec {
assert lib.hasPrefix s e;
"/" + (lib.removePrefix s e)
);
# yield every node between start and end, including each the endpoints
# e.g. walk "/foo" "/foo/bar/baz" => [ "/foo" "/foo/bar" "/foo/bar/baz" ]
# XXX: assumes input paths are normalized
walk = start: end: if start == end then
[ start ]
else
(walk start (parent end)) ++ [ end ]
;
};
in path

View File

@@ -264,13 +264,20 @@ let
};
};
};
toPkgSpec = types.coercedTo types.package (p: { pkg = p; }) pkgSpec;
in
{
options = {
# packages to deploy to the user's home
sane.packages.extraUserPkgs = mkOption {
default = [ ];
type = types.listOf (types.either types.package pkgSpec);
type = types.listOf toPkgSpec;
};
sane.packages.extraGuiPkgs = mkOption {
default = [ ];
type = types.listOf toPkgSpec;
description = "packages to only ship if gui's enabled";
};
sane.packages.enableConsolePkgs = mkOption {
default = false;
@@ -297,18 +304,18 @@ in
sane.packages.enabledUserPkgs = mkOption {
default = cfg.extraUserPkgs
++ (if cfg.enableConsolePkgs then consolePkgs else [])
++ (if cfg.enableGuiPkgs then guiPkgs else [])
++ (if cfg.enableGuiPkgs then guiPkgs ++ cfg.extraGuiPkgs else [])
++ (if cfg.enableDevPkgs then devPkgs else [])
;
type = types.listOf (types.coercedTo types.package (p: { pkg = p; }) pkgSpec);
type = types.listOf toPkgSpec;
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);
sane.persist.home.plaintext = concatLists (map (p: p.dir) cfg.enabledUserPkgs);
sane.persist.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

@@ -0,0 +1,18 @@
{ config, lib, sane-lib, ... }:
let
path = sane-lib.path;
cfg = config.sane.persist;
withPrefix = relativeTo: entries: lib.mapAttrs' (fspath: value: {
name = path.concat [ relativeTo fspath ];
inherit value;
}) entries;
in
{
# merge the `byPath` mappings from both `home` and `sys` into one namespace
sane.persist.byPath = lib.mkMerge [
(withPrefix "/home/colin" cfg.home.byPath)
(withPrefix "/" cfg.sys.byPath)
];
}

254
modules/persist/default.nix Normal file
View File

@@ -0,0 +1,254 @@
# borrows from:
# 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, sane-lib, ... }:
with lib;
let
path = sane-lib.path;
sane-types = sane-lib.types;
cfg = config.sane.persist;
storeType = types.submodule {
options = {
storeDescription = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
an optional description of the store, which is rendered like
{store.name}: {store.storeDescription}
for example, a store named "private" could have description "ecnrypted to the user's password and decrypted on login".
'';
};
origin = mkOption {
type = types.str;
};
prefix = mkOption {
type = types.str;
default = "/";
description = ''
optional prefix to strip from children when stored here.
for example, prefix="/var/private" and mountpoint="/mnt/crypt/private"
would cause /var/private/www/root to be stored at /mnt/crypt/private/www/root instead of
/mnt/crypt/private/var/private/www/root.
'';
};
defaultMethod = mkOption {
type = types.enum [ "bind" "symlink" ];
default = "bind";
description = ''
preferred way to link items from the store into the fs
'';
};
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.
'';
};
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.
'';
};
};
};
# allows a user to specify the store either by name or as an attrset
coercedToStore = types.coercedTo types.str (s: cfg.stores."${s}") storeType;
# options common to all entries, whether they're keyed by path or store
entryOpts = {
options = {
acl = mkOption {
type = sane-types.aclOverride;
default = {};
};
method = mkOption {
type = types.nullOr (types.enum [ "bind" "symlink" ]);
default = null;
description = ''
how to link the store entry into the fs
'';
};
};
};
# options for a single mountpoint / persistence where the store is specified externally
entryInStore = types.submodule [
entryOpts
{
options = {
directory = mkOption {
type = types.str;
};
};
}
];
# allow "bar/baz" as shorthand for { directory = "bar/baz"; }
entryInStoreOrShorthand = types.coercedTo
types.str
(d: { directory = d; })
entryInStore;
# allow the user to provide the `acl` field inline: we pop acl sub-attributes placed at the
# toplevel and move them into an `acl` attribute.
convertInlineAcl = to: types.coercedTo
types.attrs
(orig: lib.recursiveUpdate
(builtins.removeAttrs orig ["user" "group" "mode" ])
{
acl = sane-lib.filterByName ["user" "group" "mode"] (orig.acl or {});
}
)
to;
# entry where the path is specified externally
entryAtPath = types.submodule [
entryOpts
{
options = {
store = mkOption {
type = coercedToStore;
};
};
}
];
# this submodule creates one attr per store, so that the user can specify something like:
# <option>.private.".cache/vim" = { mode = "0700"; };
# to place ".cache/vim" into the private store and create with the appropriate mode
dirsSubModule = types.submodule ({ config, ... }: {
options = lib.attrsets.unionOfDisjoint
(mapAttrs (store: store-cfg: mkOption {
default = [];
type = types.listOf (convertInlineAcl entryInStoreOrShorthand);
description = let
suffix = if store-cfg.storeDescription != null then
": ${store-cfg.storeDescription}"
else "";
in "directories to persist in ${store}${suffix}";
}) cfg.stores)
{
byPath = mkOption {
type = types.attrsOf (convertInlineAcl entryAtPath);
default = {};
description = ''
map of <path> => <path config> for all paths to be persisted.
this is computed from the other options, but users can also set it explicitly (useful for overriding)
'';
};
};
config = let
# set the `store` attribute on one dir attrset
annotateWithStore = store: dir: {
"${dir.directory}".store = store;
};
# convert an `entryInStore` to an `entryAtPath` (less the `store` item)
dirToAttrs = dir: {
"${dir.directory}" = builtins.removeAttrs dir ["directory"];
};
store-names = attrNames cfg.stores;
# :: (store -> entry -> AttrSet) -> [AttrSet]
applyToAllStores = f: lib.concatMap
(store: map (f store) config."${store}")
store-names;
in {
byPath = lib.mkMerge (concatLists [
(applyToAllStores (store: dirToAttrs))
(applyToAllStores annotateWithStore)
]);
};
});
in
{
options = {
sane.persist.enable = mkOption {
default = false;
type = types.bool;
};
sane.persist.root-on-tmpfs = mkOption {
default = false;
type = types.bool;
description = "define / fs root to be a tmpfs. make sure to mount some other device to /nix";
};
sane.persist.home = mkOption {
description = "directories to persist to disk, relative to a user's home ~";
default = {};
type = dirsSubModule;
};
sane.persist.sys = mkOption {
description = "directories to persist to disk, relative to the fs root /";
default = {};
type = dirsSubModule;
};
sane.persist.byPath = mkOption {
type = types.attrsOf (convertInlineAcl entryAtPath);
description = ''
map of <path> => <path config> for all paths to be persisted.
this is computed from the other options, but users can also set it explicitly (useful for overriding)
'';
};
sane.persist.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.
'';
};
};
imports = [
./computed.nix
./root-on-tmpfs.nix
./stores
];
config = let
cfgFor = fspath: opt:
let
store = opt.store;
method = (sane-lib.withDefault store.defaultMethod) opt.method;
fsPathToStoreRelPath = fspath: path.from store.prefix fspath;
fsPathToBackingPath = fspath: path.concat [ store.origin (fsPathToStoreRelPath fspath) ];
in lib.mkMerge [
{
# create destination dir, with correct perms
sane.fs."${fspath}" = {
inherit (store.defaultOrdering) wantedBy wantedBeforeBy;
} // (lib.optionalAttrs (method == "bind") {
# inherit perms & make sure we don't mount until after the mount point is setup correctly.
dir.acl = opt.acl;
mount.bind = fsPathToBackingPath fspath;
}) // (lib.optionalAttrs (method == "symlink") {
symlink.acl = opt.acl;
symlink.target = fsPathToBackingPath fspath;
});
# create the backing path as a dir
sane.fs."${fsPathToBackingPath fspath}".dir = {};
}
{
# default each item along the backing path to have the same acl as the location it would be mounted.
sane.fs = lib.mkMerge (builtins.map
(fsSubpath: {
"${fsPathToBackingPath fsSubpath}" = {
generated.acl = config.sane.fs."${fsSubpath}".generated.acl;
};
})
(path.walk store.prefix fspath)
);
}
];
configs = lib.mapAttrsToList cfgFor cfg.byPath;
take = f: { sane.fs = f.sane.fs; };
in mkIf cfg.enable (
take (sane-lib.mkTypedMerge take configs)
);
}

View File

@@ -1,7 +1,7 @@
{ config, lib, ... }:
let
cfg = config.sane.impermanence;
cfg = config.sane.persist;
in
{
fileSystems."/" = lib.mkIf (cfg.enable && cfg.root-on-tmpfs) {

View File

@@ -2,17 +2,17 @@
let
store = rec {
device = "/mnt/impermanence/crypt/clearedonboot";
device = "/mnt/persist/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";
key = "/mnt/persist/crypt/clearedonboot.key";
};
};
in
lib.mkIf config.sane.impermanence.enable
lib.mkIf config.sane.persist.enable
{
sane.impermanence.stores."cryptClearOnBoot" = {
sane.persist.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

View File

@@ -0,0 +1,9 @@
{ ... }:
{
imports = [
./crypt.nix
./plaintext.nix
./private.nix
];
}

View File

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

View File

@@ -1,8 +1,8 @@
{ config, lib, pkgs, utils, ... }:
lib.mkIf config.sane.impermanence.enable
lib.mkIf config.sane.persist.enable
{
sane.impermanence.stores."private" = {
sane.persist.stores."private" = {
storeDescription = ''
encrypted to the user's password and auto-unlocked at login
'';
@@ -20,6 +20,7 @@ lib.mkIf config.sane.impermanence.enable
# we can't create things in private before local-fs.target
wantedBeforeBy = [ ];
};
defaultMethod = "symlink";
};
fileSystems."/home/colin/private" = {

View File

@@ -1,34 +0,0 @@
# create ssh key by running:
# - `ssh-keygen -t ed25519`
let
withHost = host: key: "${host} ${key}";
withUser = user: key: "${key} ${user}";
keys = rec {
lappy = {
host = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILSJnqmVl9/SYQ0btvGb0REwwWY8wkdkGXQZfn/1geEc";
users.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDpmFdNSVPRol5hkbbCivRhyeENzb9HVyf9KutGLP2Zu";
};
desko = {
host = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFw9NoRaYrM6LbDd3aFBc4yyBlxGQn8HjeHd/dZ3CfHk";
users.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPU5GlsSfbaarMvDA20bxpSZGWviEzXGD8gtrIowc1pX";
};
servo = {
host = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOfdSmFkrVT6DhpgvFeQKm3Fh9VKZ9DbLYOPOJWYQ0E8";
users.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPS1qFzKurAdB9blkWomq8gI1g0T3sTs9LsmFOj5VtqX";
};
moby = {
host = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1N/IT3nQYUD+dBlU1sTEEVMxfOyMkrrDeyHcYgnJvw";
users.colin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrR+gePnl0nV/vy7I5BzrGeyVL+9eOuXHU1yNE3uCwU";
};
"uninsane.org" = servo;
"git.uninsane.org" = servo;
};
in {
# map hostname -> something suitable for known_keys
hosts = builtins.mapAttrs (host: keys: withHost host keys.host) keys;
# map hostname -> something suitable for authorized_keys to allow access to colin@<hostname>
users = builtins.mapAttrs (host: keys: withUser "colin@${host}" keys.users.colin) keys;
}

View File

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

87
modules/ssh.nix Normal file
View File

@@ -0,0 +1,87 @@
{ config, lib, ... }:
with lib;
let
key = types.submodule ({ name, config, ...}: {
options = {
typedPubkey = mkOption {
type = types.str;
description = ''
the pubkey with type attached.
e.g. "ssh-ed25519 <base64>"
'';
};
# type = mkOption {
# type = types.str;
# description = ''
# the type of the key, e.g. "id_ed25519"
# '';
# };
host = mkOption {
type = types.str;
description = ''
the hostname of a key
'';
};
user = mkOption {
type = types.str;
description = ''
the username of a key
'';
};
asUserKey = mkOption {
type = types.str;
description = ''
append the "user@host" value to the pubkey to make it usable for ~/.ssh/id_<x>.pub or authorized_keys
'';
};
asHostKey = mkOption {
type = types.str;
description = ''
prepend the "host" value to the pubkey to make it usable for ~/.ssh/known_hosts
'';
};
};
config = rec {
user = head (lib.splitString "@" name);
host = last (lib.splitString "@" name);
asUserKey = "${config.typedPubkey} ${name}";
asHostKey = "${host} ${config.typedPubkey}";
};
});
coercedToKey = types.coercedTo types.str (typedPubkey: {
inherit typedPubkey;
}) key;
in
{
options = {
sane.ssh.pubkeys = mkOption {
type = types.attrsOf coercedToKey;
default = [];
description = ''
mapping from "user@host" to pubkey.
'';
};
};
config = {
# persist the host key
# prefer specifying it via environment.etc since although it is generated per-host,
# it's made to be immutable after generation. hence, a `persist`-style mount wouldn't be as great.
environment.etc."ssh/host_keys".source = "/nix/persist/etc/ssh/host_keys";
# sane.persist.sys.plaintext = [ "/etc/ssh/host_keys" ];
# let openssh find our host keys
services.openssh.hostKeys = [
{ type = "rsa"; bits = 4096; path = "/etc/ssh/host_keys/ssh_host_rsa_key"; }
{ type = "ed25519"; path = "/etc/ssh/host_keys/ssh_host_ed25519_key"; }
];
services.openssh.knownHosts =
let
host-keys = filter (k: k.user == "root") (attrValues config.sane.ssh.pubkeys);
in lib.mkMerge (builtins.map (key: {
"${key.host}".publicKey = key.typedPubkey;
}) host-keys);
};
}

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
target="$1"
host="$(hostname)"
if [ "$host" = "$target" ]
then
sudo shutdown now
else
echo "WRONG MACHINE. you're on $host."
exit 1
fi

View File

@@ -2,18 +2,19 @@
# initializes the default libsecret keyring (used by gnome-keyring) if not already initialized.
# this initializes it to be plaintext/unencrypted.
if [ -f ~/.local/share/keyrings/default ]
ringdir=/home/colin/private/.local/share/keyrings
if test -f "$ringdir/default"
then
echo 'keyring already initialized: not doing anything'
exit 0
echo 'keyring already initialized: not doing anything'
else
keyring="$ringdir/Default_keyring.keyring"
echo 'initializing default user keyring:' "$keyring.new"
echo '[keyring]' > "$keyring.new"
echo 'display-name=Default keyring' >> "$keyring.new"
echo 'lock-on-idle=false' >> "$keyring.new"
echo 'lock-after=false' >> "$keyring.new"
chown colin:users "$keyring.new"
# closest to an atomic update we can achieve
mv "$keyring.new" "$keyring" && echo -n "Default_keyring" > "$ringdir/default"
fi
keyring=~/.local/share/keyrings/Default_keyring.keyring
echo 'initializing default user keyring:' "$keyring.new"
echo '[keyring]' > "$keyring.new"
echo 'display-name=Default keyring' >> "$keyring.new"
echo 'lock-on-idle=false' >> "$keyring.new"
echo 'lock-after=false' >> "$keyring.new"
# closest to an atomic update we can achieve
mv "$keyring.new" "$keyring" && echo -n "Default_keyring" > ~/.local/share/keyrings/default

View File

@@ -1,25 +1,37 @@
#!/bin/sh
# usage: install-bluetooth <source_dir> <dest_dir>
# usage: install-bluetooth <source_dir> <destdir>
# source_dir contains plain-text files of any filename.
# for each file, this extracts the MAC and creates a symlink in dest_dir which
# for each file, this extracts the MAC and creates a symlink in destdir which
# points to the original file, using the MAC name as file path
#
# bluetooth connection structure is /var/lib/bluetooth/<HOST_MAC>/<DEVICE_MAX>/{attributes,info}
#
set -ex
# bluetoothd/main.conf options can be found here:
# - <https://pythonhosted.org/BT-Manager/config.html>
# can be set via nixos' `hardware.bluetooth.settings`
src_dir="$1"
dest_dir="$2"
srcdir="$1"
destdir="$2"
if [ "x$dest_dir" = "x" ]
if [ "x$destdir" = "x" ]
then
devmac=$(cat /sys/kernel/debug/bluetooth/hci0/identity | cut -f 1 -d' ' | tr "a-z" "A-Z")
# default to the first MAC address on the host
dest_dir="/var/lib/bluetooth/$(ls /var/lib/bluetooth)"
destdir="/var/lib/bluetooth/$devmac"
test -d "$destdir" || mkdir "$destdir" || test -d "$destdir"
fi
for f in $(ls "$src_dir")
for f in $(ls "$srcdir")
do
mac=$(sed -rn 's/# MAC=(.*)/\1/p' "$src_dir/$f")
mkdir -p "$dest_dir/$mac"
ln -sf "$src_dir/$f" "$dest_dir/$mac/info"
mac=$(sed -rn 's/# MAC=(.*)/\1/p' "$srcdir/$f")
condir="$destdir/$mac"
if ! test -f "$condir/info"
then
# don't *overwrite* pairings. instead, only copy the device data if the host doesn't yet know about it.
# unfortunately, it seems that for most BT devices i can't share link keys across hosts.
# perhaps i could using `bdaddr` to force a shared host MAC across all hosts, but that doesn't work for all manufacturers.
# instead, my bluetooth "secrets" are mostly just a list of MACs i want a host to trust.
mkdir "$condir"
cp "$srcdir/$f" "$condir/info"
touch "$condir/attributes"
fi
done

View File

@@ -1,5 +1,5 @@
{
"data": "ENC[AES256_GCM,data:639/SycbUOC4kwBAJvk2sQCBfnXZrYDzucXFXysOW8DOPU7nZwsdWKmB+41zvLg3vS5GyPTzMco/EaytPbGj+HvRqyKEYCJvBY/ZOoPpAZKltqMkkIS449OFy3vuOS46TPLQ1c5AgsBnHuSFrGGcVXRs+yP1A4rRcn5Au3Z7hPujZA/W3lUR+/Cp4UYc2tk1w2kpWD/SXLqkSX2XTRNPXb1rOva6xxllUiFuG7Yt1a7KfjrkaF4GJK5TNWgGVrY1N7IKzC75Sh1x5wvumF7A1mwLE1I+sa55Sn9qtGsmWwfsDBN+9SRg7uCUipTCrAEAgJK07QdN5P0ZFrQlxqNGn0i5BvKSxn+GK+f7xJIHjb+sexQgNMFQ56V83Zej8U5HQ9vyrx7EHpE44UneUQQ8FkckQVWyhSGs3MVSkfbznb3L2WnOCEogb0xft56pXPOG/K4q7uLkOwUtgcq0Jlf+gZZmuXRoVnNQc//o0u296CF8AFnxA/MJJc67bkONITjlekB7f23NFoywhzyX4RYjjbflRkMF8BRu0RDuynGu0xLZLLlK8QM4uJfpPyH8L2ThiKRbYO7OGaKCYJy6fYx+ViUVsRf3hpHMiDn8yGHxrqABOTv6HSIZvIKo3G4Kn5iF/GPxWQISTZxE7YdYhgUf61UpZEfkgw==,iv:jqWb8k8f8jKscWPwcZy9o9QmOJKG38m9ukbeBDX3IN8=,tag:vZh6J2mtUhaoiwpn17l80g==,type:str]",
"data": "ENC[AES256_GCM,data:WbIbrAEaBV1ye3Vzxt5DybxNzsQgqsGmRRG8GWibq3DTv8zeJ0hBAWKJHA6BZvK3gxoAOCr+jEsKq/hIcaZkth0G+otQI8UWd8aUC0O+MQMT7bXXu7g+uI/VwofZTu/+AwceBfnyPdfWcmORobbzWABaNxT0OC4tZ+369u37QRmp9Nny9sQALaXW5cUS2kK2ajouWhXaXUt0hVhDTYULDWm7tNaqEFG1sTpg75AGlXfDcvRUNP+FHs1ISfNPgHf2QrPDD48xOCT/weNAy1xIMqS8Ecns5XGRt2sitTz0O5Lz2B5b436GdlsxcGfFv+RATpLGY7DtEirmfPyNybRbxdIrV3oceJO8op2TRmaDR9D0zhF2vtPb470z8lCrU7uyooc+b9mUx2tX3b3FLQy9ukSS2QG6TleVGxznrqO2XYFvse1OZMVDFdXjhn0CaiCHc0f51SNEunS5gLMFpcBvI20FHntT9uj7nwV4yCOGVlWAziyUtK7w/mbcACvOWhwjrN3D4LXQ9QPGSuWBqni9Dsk7de+uArqYP//zqSotJbv1qS1TpB7QYrlJf9VaJ/eLxwAAM7AxEt2So5sNbreh+zZi5cAhaudyEb1fNSf7suACqnHk1BhXoQtRh93ngetMmneQlsXMZGM=,iv:6kYaqB/TMZdvns3Gv0nO0yp+LQBGSS2xn979FfXffvc=,tag:QzfQi4YXhGWLjPIWng1HAg==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
@@ -39,8 +39,8 @@
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzYnMxbDNUR2xyV3B5VzFQ\nM1IxQzV5OXM5L1VYdFRYWUt0cWl4ZUdsQVNJCmRjUjdPMmhoaEFmUUxrVmJCRlFl\nNzZqY3p0YUF3T2lYdysvakx4WVg0bFUKLS0tIFFlazJzb3hmVXNyUU5leUFKL3p0\nNlN0TGxVbGtoUHFtK3hBS2RiYUViVFEKii4w04zeDD6HWURzmAhJdxNdNmQgsPw/\nawI6HSVbbmEGXyL23Pe0oultY8k/ZVE4oHRKBkHh00XoCZM/Ye6neA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2022-12-03T10:23:14Z",
"mac": "ENC[AES256_GCM,data:KBm0rXwAGPa0ZkqGI9K3rW5B4vJ1FLmITa8xV5WR1SG2MlSqvCqSj4Qe5kxcIc3AqqHF2W+LDaJ0f1fXOCVqWRe1mi/LJyYgPERL5Hn3iOHty9g984Q/QSGvH13O7eY/Fuk2h0mpIX4pOhdpW74qlp1zYDXqUswsKW7ERTTRf6E=,iv:maE+9/OgdgYNX4F/MrzIpJr+/XXyFSayC1YX382oc2Y=,tag:NmrKXA9AjnoTXrQThnvxvg==,type:str]",
"lastmodified": "2023-01-07T11:04:42Z",
"mac": "ENC[AES256_GCM,data:QiNqZSB5WIVroTQKWxt73NLGvv13waePyMcQ3OJaecaOZQiXGhuq9Ojwnk+I2DSs7X8Nv10VilHk97kYNgTjsNdWmXHqtSY0LKbbMoJpzPoF42MCPSv8g5tLOnIR095Ihu8Ntw+FdOsl0rqa9ipqJFFswOpGI/xamcsLtpRnQnQ=,iv:i4YCULu9YJR5zLomeAYpzvFG7SB9x+4wWPhaiFGlTQQ=,tag:xLbmIhg7hPZnHvQVhbgDpQ==,type:str]",
"pgp": null,
"unencrypted_suffix": "_unencrypted",
"version": "3.7.3"

View File

@@ -1,5 +1,5 @@
{
"data": "ENC[AES256_GCM,data:ou55VGY+beKMouNj4qQaBOAZK/5UKu6A521lNW2i0KlSmgJ8qQ501lesy0bEmDkZqqhluP8XE5FZLwEXvqqMh/TBuN1OkCsQis53/M1s0g==,iv:Ir5uD1P8OlHlcjGCHVkUHr0AjoXzd7kOcAeajo66hUE=,tag:m+rReK9o/8TG4LBkNN1ZZQ==,type:str]",
"data": "ENC[AES256_GCM,data:OaFr+OOaBxi0PaApOYLUjJ0NgD5ABBQOaf6KpR9rheE2d1pQNa0jqnD4/ttqJrq8JjZT2Y6GDSwM5gPM,iv:TuyQPPDXM8cJU/GhJpdvxwB8+v6JavHcA+vmLHA3/74=,tag:V6RTKw6Cot4B4sK1JcRGmA==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
@@ -39,8 +39,8 @@
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAraXJQOHR6ZzE5TjNQYmpB\nSStEQS9mcUpMSXlFQ05DcllFSjNOT1pWdVJZCmtSL3FkZ2Q1cU1Fc1dZbG13eXJC\nTXJkN0NzWTlDOEFMRGNQUG5HQUNUVDgKLS0tIGRwcmVxS0lNQ09GdmxKY2pkQ2Yz\nSkpZam1ZQUN1L1FZZ010ZlhUV1N4VlkKqsFAE+xZ24IMzIFjbsgANdjiGwVZk5rq\n66y00bjw+uj6WOwQuE1I9WcYDhCXEUQB9u4Q+hzejaFzCJ90N/WF4w==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2022-10-08T03:39:12Z",
"mac": "ENC[AES256_GCM,data:4Rr2iqmzLtE9i45Hn10wuf8unKt+YNAYTF3RWwEW1AjN+pF7ZvwMbrUutRCb6uMxCQUyNl+adfFRu8Xae0/SqFBfdAPxzeQZGrBjb384seLrNS0XyUacfdoSCczrRUF8+F3mIHetaJCd2jOpoh5HotoSN3fx+nZNhD+56XmJBr0=,iv:YlDMimhG+a9Wzq0ZN0tnZ1gH69e7olyHGWhIV2/4K64=,tag:GjVzbNa/NdzVmdPyE5etXw==,type:str]",
"lastmodified": "2023-01-07T03:06:02Z",
"mac": "ENC[AES256_GCM,data:L3wY2ZdR1ASbLbKXiipWfBiQ5cumItuiL1+TwTJhU5ZtxLe6SMUyhckvuX8hczlFPUlJQJDCwpgVBs9C6GRAU45jzHYmpcfF30auiRT2dF/2doH9yiYZoF7JtbTas0Kvt1yxlPfuTi5mFuJGAKDOw6+a5ayQHYlK3/RxAUn0yPc=,iv:U/vlmvI1l4u92eUDXRphS0tscLOlWorOdmT7wDwGbAM=,tag:bQayboRgsMKT6akDq+rzQw==,type:str]",
"pgp": null,
"unencrypted_suffix": "_unencrypted",
"version": "3.7.3"