summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/borgbackup/btrfs-snapshots.nix52
-rw-r--r--modules/borgbackup/default.nix206
-rw-r--r--modules/borgbackup/lvm-snapshots.nix133
-rw-r--r--modules/borgbackup/repokeys/borg_munin__borg.yaml33
-rw-r--r--modules/kill-user.nix13
-rw-r--r--modules/knot.nix126
-rw-r--r--modules/luksroot.nix1075
-rw-r--r--modules/networkd.nix297
-rw-r--r--modules/tinc-networkmanager.nix36
-rw-r--r--modules/uucp.nix391
-rw-r--r--modules/yggdrasil/default.nix50
-rw-r--r--modules/yggdrasil/hosts/sif/default.nix13
-rw-r--r--modules/yggdrasil/hosts/sif/private-keys.yaml34
-rw-r--r--modules/yggdrasil/hosts/ymir.nix19
14 files changed, 2478 insertions, 0 deletions
diff --git a/modules/borgbackup/btrfs-snapshots.nix b/modules/borgbackup/btrfs-snapshots.nix
new file mode 100644
index 00000000..96d2b2ba
--- /dev/null
+++ b/modules/borgbackup/btrfs-snapshots.nix
@@ -0,0 +1,52 @@
1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.btrfs-snapshots;
7
8 snapshotMount = str: "${str}${cfg.mountSuffix}";
9in {
10 options = {
11
12 services.btrfs-snapshots = {
13 enable = mkEnableOption "a systemd unit for btrfs snapshots";
14
15 mountSuffix = mkOption {
16 type = types.str;
17 default = ".snapshot";
18 };
19
20 readOnly = mkOption {
21 type = types.bool;
22 default = true;
23 };
24
25 persist = mkOption {
26 type = types.bool;
27 default = false;
28 };
29 };
30
31 };
32
33
34 config = mkIf cfg.enable {
35 systemd.services."btrfs-snapshot@" = {
36 enable = true;
37
38 unitConfig = {
39 StopWhenUnneeded = !cfg.persist;
40 };
41
42 serviceConfig = with pkgs; {
43 Type = "oneshot";
44 ExecStartPre = "-${btrfs-progs}/bin/btrfs subvolume delete -c ${snapshotMount "%f"}";
45 ExecStart = "${btrfs-progs}/bin/btrfs subvolume snapshot ${optionalString cfg.readOnly "-r"} %f ${snapshotMount "%f"}";
46 RemainAfterExit = true;
47 ExecStop = "${btrfs-progs}/bin/btrfs subvolume delete -c ${snapshotMount "%f"}";
48 };
49 };
50
51 };
52}
diff --git a/modules/borgbackup/default.nix b/modules/borgbackup/default.nix
new file mode 100644
index 00000000..a0419d0e
--- /dev/null
+++ b/modules/borgbackup/default.nix
@@ -0,0 +1,206 @@
1{ config, lib, utils, pkgs, ... }:
2
3with utils;
4with lib;
5
6let
7 cfg = config.services.borgbackup;
8
9 lvmPath = {
10 options = {
11 LV = mkOption {
12 type = types.str;
13 };
14 VG = mkOption {
15 type = types.str;
16 };
17 };
18 };
19
20 pathType = if cfg.snapshots == "lvm" then types.submodule lvmPath else types.path;
21
22 systemdPath = path: escapeSystemdPath (if cfg.snapshots == "lvm" then "${path.VG}-${path.LV}" else path);
23
24 withSuffix = path: path + (if cfg.snapshots == "btrfs" then config.services.btrfs-snapshots.mountSuffix else config.services.lvm-snapshots.mountSuffix);
25
26 mountPoint = if cfg.snapshots == "lvm" then config.services.lvm-snapshots.mountPoint else "";
27
28 targetOptions = {
29 options = {
30 repo = mkOption {
31 type = types.str;
32 };
33
34 paths = mkOption {
35 type = types.listOf pathType;
36 default = [];
37 };
38
39 prune = mkOption {
40 type = types.attrsOf (types.listOf types.str);
41 default = {};
42 };
43
44 interval = mkOption {
45 type = types.str;
46 default = "6h";
47 };
48
49 jitter = mkOption {
50 type = with types; nullOr str;
51 default = "6h";
52 };
53
54 lock = mkOption {
55 type = types.nullOr types.str;
56 default = "backup";
57 };
58
59 network = mkOption {
60 type = types.bool;
61 default = true;
62 };
63
64 lockWait = mkOption {
65 type = types.int;
66 default = 600;
67 };
68
69 keyFile = mkOption {
70 type = types.nullOr types.path;
71 default = null;
72 };
73 };
74 };
75in {
76 disabledModules = [ "services/backup/borgbackup.nix" ];
77
78 options = {
79 services.borgbackup = {
80 snapshots = mkOption {
81 type = types.nullOr (types.enum ["btrfs" "lvm"]);
82 default = null;
83 };
84
85 targets = mkOption {
86 type = types.attrsOf (types.submodule targetOptions);
87 default = {};
88 };
89
90 prefix = mkOption {
91 type = types.str;
92 };
93 };
94 };
95
96 imports =
97 [ ./lvm-snapshots.nix
98 ./btrfs-snapshots.nix
99 ];
100
101 config = mkIf (any (t: t.paths != []) (attrValues cfg.targets)) {
102 services.btrfs-snapshots.enable = mkIf (cfg.snapshots == "btrfs") true;
103
104 services.lvm-snapshots.snapshots = mkIf (cfg.snapshots == "lvm") (listToAttrs (map (path: nameValuePair (path.VG + "-" + path.LV) {
105 inherit (path) LV VG;
106 mountName = withSuffix (path.VG + "-" + path.LV);
107 }) (unique (flatten (mapAttrsToList (target: tCfg: tCfg.paths) cfg.targets)))));
108
109 systemd.targets."timers-borg" = {
110 wantedBy = [ "timers.target" ];
111 };
112
113 systemd.slices."system-borgbackup" = {};
114
115 systemd.timers = (listToAttrs (map ({ target, path, tCfg }: nameValuePair "borgbackup-${target}@${systemdPath path}" {
116 requiredBy = [ "timers-borg.target" ];
117
118 timerConfig = {
119 Persistent = false;
120 OnBootSec = tCfg.interval;
121 OnUnitActiveSec = tCfg.interval;
122 RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter;
123 };
124 }) (flatten (mapAttrsToList (target: tCfg: map (path: { inherit target path tCfg; }) tCfg.paths) cfg.targets)))) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" {
125 enable = tCfg.prune != {};
126
127 requiredBy = [ "timers-borg.target" ];
128
129 timerConfig = {
130 Persistent = false;
131 OnBootSec = tCfg.interval;
132 OnUnitActiveSec = tCfg.interval;
133 RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter;
134 };
135 }) cfg.targets);
136
137 systemd.services = (mapAttrs' (target: tCfg: nameValuePair "borgbackup-${target}@" (let
138 deps = flatten [
139 (optional (cfg.snapshots == "btrfs") "btrfs-snapshot@%i.service")
140 (optional tCfg.network "network-online.target")
141 ];
142 in {
143 bindsTo = deps;
144 after = deps;
145
146 path = with pkgs; [borgbackup] ++ optional (tCfg.lock != null) utillinux;
147
148 script = let
149 borgCmd = ''
150 borg create \
151 --lock-wait ${toString tCfg.lockWait} \
152 --stats \
153 --list \
154 --filter 'AME' \
155 --exclude-caches \
156 --keep-exclude-tags \
157 --patterns-from .backup-${target} \
158 --one-file-system \
159 --compression auto,lzma \
160 ${tCfg.repo}::${cfg.prefix}$1-{utcnow}
161 '';
162 in if tCfg.lock == null then borgCmd else "flock -xo /var/lock/${tCfg.lock} ${borgCmd}";
163 scriptArgs = if cfg.snapshots == "lvm" then "%I" else "%i";
164
165 unitConfig = {
166 AssertPathIsDirectory = mkIf (tCfg.lock != null) "/var/lock";
167 DefaultDependencies = false;
168 RequiresMountsFor = mkIf (cfg.snapshots == "lvm") [ "${mountPoint}/${withSuffix "%I"}" ];
169 };
170
171 serviceConfig = {
172 Type = "oneshot";
173 WorkingDirectory = if (cfg.snapshots == null) then "%I" else (if (cfg.snapshots == "lvm") then "${mountPoint}/${withSuffix "%I"}" else "${withSuffix "%f"}");
174 Nice = 15;
175 IOSchedulingClass = 2;
176 IOSchedulingPriority = 7;
177 SuccessExitStatus = [1 2];
178 Slice = "system-borgbackup.slice";
179 Environment = lib.mkIf (tCfg.keyFile != null) "BORG_KEY_FILE=${tCfg.keyFile}";
180 };
181 })) cfg.targets) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" {
182 enable = tCfg.prune != {};
183
184 bindsTo = ["network-online.target"];
185 after = ["network-online.target"];
186
187 path = with pkgs; [borgbackup];
188
189 script = concatStringsSep "\n" (mapAttrsToList (path: args: ''
190 borg prune \
191 --lock-wait ${toString tCfg.lockWait} \
192 --list \
193 --stats \
194 --prefix ${escapeShellArg "${cfg.prefix}${path}"} \
195 ${escapeShellArgs args} \
196 ${tCfg.repo}
197 '') tCfg.prune);
198
199 serviceConfig = {
200 Type = "oneshot";
201 Slice = "system-borgbackup.slice";
202 Environment = lib.mkIf (tCfg.keyFile != null) "BORG_KEY_FILE=${tCfg.keyFile}";
203 };
204 }) cfg.targets);
205 };
206}
diff --git a/modules/borgbackup/lvm-snapshots.nix b/modules/borgbackup/lvm-snapshots.nix
new file mode 100644
index 00000000..9b2a6562
--- /dev/null
+++ b/modules/borgbackup/lvm-snapshots.nix
@@ -0,0 +1,133 @@
1{ config, lib, utils, pkgs, ... }:
2
3with utils;
4with lib;
5
6let
7 cfg = config.services.lvm-snapshots;
8
9 snapshotMount = name: "${cfg.mountPoint}/${if isNull cfg.snapshots."${name}".mountName then name else cfg.snapshots."${name}".mountName}";
10 snapshotName = name: "${name}-${cfg.mountSuffix}";
11
12 snapshotConfig = {
13 options = {
14 LV = mkOption {
15 type = types.str;
16 };
17
18 VG = mkOption {
19 type = types.str;
20 };
21
22 mountName = mkOption {
23 type = types.nullOr types.str;
24 default = null;
25 };
26
27 cowSize = mkOption {
28 type = types.str;
29 default = "-l20%ORIGIN";
30 };
31
32 readOnly = mkOption {
33 type = types.bool;
34 default = true;
35 };
36
37 persist = mkOption {
38 type = types.bool;
39 default = false;
40 };
41 };
42 };
43in {
44 options = {
45
46 services.lvm-snapshots = {
47 snapshots = mkOption {
48 type = types.attrsOf (types.submodule snapshotConfig);
49 default = {};
50 };
51
52 mountPoint = mkOption {
53 type = types.path;
54 default = "/mnt";
55 };
56
57 mountSuffix = mkOption {
58 type = types.str;
59 default = "-snapshot";
60 };
61 };
62 };
63
64
65 config = mkIf (cfg != {}) {
66
67 boot.kernelModules = [ "dm_snapshot" ];
68
69 # system.activationScripts = mapAttrs' (name: scfg: nameValuePair ("lvm-mountpoint" + name) ''
70 # mkdir -p ${snapshotMount name}
71 # '') cfg.snapshots;
72
73 systemd.services = mapAttrs' (name: scfg: nameValuePair ("lvm-snapshot@" + escapeSystemdPath name) {
74 enable = true;
75
76 description = "LVM-snapshot of ${scfg.VG}/${scfg.LV}";
77
78 bindsTo = ["${escapeSystemdPath "/dev/${scfg.VG}/${scfg.LV}"}.device"];
79 after = ["${escapeSystemdPath "/dev/${scfg.VG}/${scfg.LV}"}.device"];
80
81 unitConfig = {
82 StopWhenUnneeded = !scfg.persist;
83 AssertPathIsDirectory = "/var/lock";
84 };
85
86 path = with pkgs; [ devicemapper utillinux ];
87
88 script = ''
89 (
90 flock -xn -E 4 9
91 if [[ "$?" -ne 0 ]]; then
92 exit $?
93 fi
94
95 lvcreate -s ${scfg.cowSize} --name ${snapshotName name} ${scfg.VG}/${scfg.LV}
96
97 sleep infinity &
98 ) 9>/var/lock/lvm-snapshot.${scfg.VG}
99 '';
100
101 preStart = ''
102 lvremove -f ${scfg.VG}/${snapshotName name}
103 '';
104
105 preStop = ''
106 lvremove -f ${scfg.VG}/${snapshotName name}
107 '';
108
109 serviceConfig = with pkgs; {
110 Type = "forking";
111 RestartForceExitStatus = [ "4" ];
112 RestartSec = "5min";
113 };
114 }) cfg.snapshots;
115
116 systemd.mounts = mapAttrsToList (name: scfg: {
117 enable = true;
118
119 unitConfig = {
120 # AssertPathIsDirectory = snapshotMount name;
121 StopWhenUnneeded = !scfg.persist;
122 };
123
124 bindsTo = [ ("lvm-snapshot@" + escapeSystemdPath name + ".service") ];
125 after = [ ("lvm-snapshot@" + escapeSystemdPath name + ".service") ];
126
127 options = concatStringsSep "," ([ "noauto" ] ++ optional scfg.readOnly "ro");
128
129 where = snapshotMount name;
130 what = "/dev/" + scfg.VG + "/" + snapshotName name;
131 }) cfg.snapshots;
132 };
133}
diff --git a/modules/borgbackup/repokeys/borg_munin__borg.yaml b/modules/borgbackup/repokeys/borg_munin__borg.yaml
new file mode 100644
index 00000000..f302fe06
--- /dev/null
+++ b/modules/borgbackup/repokeys/borg_munin__borg.yaml
@@ -0,0 +1,33 @@
1key: ENC[AES256_GCM,data:mxh+Jtxx+HyD246yPwo0vy7vSTz3IG8VmfbxPMwqJRreh9ZwkGnH5aCTDOvWOHIrkmzaRMF3oCi1P8D29+abMUZdt0MuJ3UE6iL8+SXlflR+WACgALM2Df+x9B3BwQM3yeoCiWG+ebr0iQPHM3jqqpkjoRv1CcythxG2deZueur9lzgC2CwG1g3O8Prnl9z0JQGOa+gjic8Zwfn38B1BECeNPrbjzICGBOrSbN/6EnfBDygI2QzseamzK2I6R6jT+QxHvkl+Zi1m2TRB+4o82VgTjPhIReJyT7PrlDnUyrKObhCOlb3v+LiSdp16IPIDVs968kyDzgyi7QPOpGr+5tutWCZrau5xhPDrONKByl/0nVVwEZfRIYATvEXtn5okJru/mglcpeD0I7AtLt+Vfv9CB9pQczvkHo0cDtgudQDf9ADt/nkmqHugm5VfMg9m9aGbKqzXt6pPOMsXSbS43K7wgDaduLZ/PW4Ookx9gTNLtJHnZ64GBorOv4QSrZIZF8pE1FsQdUhmp/YzVhaNBnjCr+Jh77sYjoOwzF77Xy+VP2C/yVIf492P+FcgkSj6XhYYqHffpFW9l/xmUvyQF5gjj2k5T21UvgChhI1HeLPzQ7W9+xuGSMtg58aD/VPe1loCy8zLITNl71bneararRS5vItoZyzMdmIRMLAZD1klPmDNe1yufTpubOXzNYbWUqFUZtwH/mDL5GRZBD9dqs2b3F26c1CUyw==,iv:NJBHesKSZ1zuKk8qHnYKqIwMnFkH+rkQD1bam5XpLXU=,tag:EiYbIFY/r/eTSTJIhYV+GA==,type:str]
2sops:
3 kms: []
4 gcp_kms: []
5 azure_kv: []
6 hc_vault: []
7 lastmodified: '2021-01-02T20:38:48Z'
8 mac: ENC[AES256_GCM,data:3rkFTOk3r2dx3hOqu1u7XIIibTDfqNlRcWY9X2N/LFa/BKojgDt5tcpbphV4HqWvl8nS+fPcVrIElJfQ/QGFEOx68G95BhByntT9+JhSbHJt73dGnCSroZCw5QefdydREGvA5n00Vo9yT9IMvQsQbmpRzo6hcrSSUvagZqmZckA=,iv:F/HllDzyxgulIWZbfz9bFKR+SFg4PoaUYZ5N5hfIzw0=,tag:h2NXmvj/thhBg1rIkwdXXA==,type:str]
9 pgp:
10 - created_at: '2021-01-02T20:38:09Z'
11 enc: |
12 -----BEGIN PGP MESSAGE-----
13
14 hF4Dgwm4NZSaLAcSAQdAwmvyXlr9MyfPfLgkfQkoktKBV2WA2xhZrGL7NeeGfhAw
15 REk+clJ9WgiJ0iceRAONPnEjeiK0J6Fsj+5Ulq8flFGkoj5Pta0pm/9fudKmcPdC
16 0l4BF0G5LSpG1EmY+LmVdSdas16rWgthnojoXPvbbHG6jZs3aDETshdiN8Bdlqsf
17 aVhq2LYzscnYezNcdernR4uojtiFny8qcmdF3tFacr+mkgfgIQr0W9yWFhDH15gm
18 =4TwU
19 -----END PGP MESSAGE-----
20 fp: F1AF20B9511B63F681A14E8D51AEFBCD1DEF68F8
21 - created_at: '2021-01-02T20:38:09Z'
22 enc: |
23 -----BEGIN PGP MESSAGE-----
24
25 hF4DXxoViZlp6dISAQdAruPXj9IsllEN7R5jk4gF7bW0ZirhvX7qsu22/6HbSw8w
26 66RwN3WGjYO1CcVbHKuLqVVaUBCnrR/4XHN0JYUaqjubrSZBTWFKTBFsKSTT0LZq
27 0l4BKcsXrbGpYC5+yQvg0RHJ7LplxpKOmqMY8KGckvGnVf2xg7k6wuWQREFzqwt+
28 lOa3x+xFy9c0JwE8AafyKjb/cgqJiMb96lhsH57BpXJa2E39ImQbXqzDzdx2jEUt
29 =3rxi
30 -----END PGP MESSAGE-----
31 fp: 30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51
32 unencrypted_suffix: _unencrypted
33 version: 3.6.1
diff --git a/modules/kill-user.nix b/modules/kill-user.nix
new file mode 100644
index 00000000..dd897b36
--- /dev/null
+++ b/modules/kill-user.nix
@@ -0,0 +1,13 @@
1{ lib, pkgs, config, ... }:
2{
3 options = {
4 systemd.kill-user.enable = lib.mkEnableOption "Systemd kill-user@ services";
5 };
6
7 config.systemd.services."kill-user@" = lib.mkIf config.systemd.kill-user.enable {
8 serviceConfig = {
9 Type = "oneshot";
10 ExecStart = "${pkgs.systemd}/bin/loginctl kill-user %I";
11 };
12 };
13}
diff --git a/modules/knot.nix b/modules/knot.nix
new file mode 100644
index 00000000..a4691324
--- /dev/null
+++ b/modules/knot.nix
@@ -0,0 +1,126 @@
1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.knot;
7
8 configFile = pkgs.writeTextFile {
9 name = "knot.conf";
10 text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" +
11 cfg.extraConfig;
12 checkPhase = lib.optionalString (cfg.keyFiles == []) ''
13 ${cfg.package}/bin/knotc --config=$out conf-check
14 '';
15 };
16
17 socketFile = "/run/knot/knot.sock";
18
19 knot-cli-wrappers = pkgs.stdenv.mkDerivation {
20 name = "knot-cli-wrappers";
21 buildInputs = [ pkgs.makeWrapper ];
22 buildCommand = ''
23 mkdir -p $out/bin
24 makeWrapper ${cfg.package}/bin/knotc "$out/bin/knotc" \
25 --add-flags "--config=${configFile}" \
26 --add-flags "--socket=${socketFile}"
27 makeWrapper ${cfg.package}/bin/keymgr "$out/bin/keymgr" \
28 --add-flags "--config=${configFile}"
29 for executable in kdig khost kjournalprint knsec3hash knsupdate kzonecheck
30 do
31 ln -s "${cfg.package}/bin/$executable" "$out/bin/$executable"
32 done
33 mkdir -p "$out/share"
34 ln -s '${cfg.package}/share/man' "$out/share/"
35 '';
36 };
37in {
38 disabledModules = [ "services/networking/knot.nix" ];
39
40 options = {
41 services.knot = {
42 enable = mkEnableOption "Knot authoritative-only DNS server";
43
44 extraArgs = mkOption {
45 type = types.listOf types.str;
46 default = [];
47 description = ''
48 List of additional command line paramters for knotd
49 '';
50 };
51
52 keyFiles = mkOption {
53 type = types.listOf types.path;
54 default = [];
55 description = ''
56 A list of files containing additional configuration
57 to be included using the include directive. This option
58 allows to include configuration like TSIG keys without
59 exposing them to the nix store readable to any process.
60 Note that using this option will also disable configuration
61 checks at build time.
62 '';
63 };
64
65 extraConfig = mkOption {
66 type = types.lines;
67 default = "";
68 description = ''
69 Extra lines to be added verbatim to knot.conf
70 '';
71 };
72
73 package = mkOption {
74 type = types.package;
75 default = pkgs.knot-dns;
76 defaultText = "pkgs.knot-dns";
77 description = ''
78 Which Knot DNS package to use
79 '';
80 };
81
82 cliWrappers = mkOption {
83 readOnly = true;
84 type = types.package;
85 default = knot-cli-wrappers;
86 defaultText = "knot-cli-wrappers";
87 };
88 };
89 };
90
91 config = mkIf cfg.enable {
92 users.users.knot = {
93 isSystemUser = true;
94 group = "knot";
95 description = "Knot daemon user";
96 };
97
98 users.groups.knot.gid = null;
99 systemd.services.knot = {
100 unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/";
101 description = cfg.package.meta.description;
102 wantedBy = [ "multi-user.target" ];
103 wants = [ "network.target" ];
104 after = ["network.target" ];
105
106 serviceConfig = {
107 Type = "notify";
108 ExecStart = "${cfg.package}/bin/knotd --config=${configFile} --socket=${socketFile} ${concatStringsSep " " cfg.extraArgs}";
109 ExecReload = "${cfg.cliWrappers}/bin/knotc reload";
110 CapabilityBoundingSet = "CAP_NET_BIND_SERVICE CAP_SETPCAP";
111 AmbientCapabilities = "CAP_NET_BIND_SERVICE CAP_SETPCAP";
112 NoNewPrivileges = true;
113 User = "knot";
114 RuntimeDirectory = "knot";
115 StateDirectory = "knot";
116 StateDirectoryMode = "0700";
117 PrivateDevices = true;
118 RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
119 SystemCallArchitectures = "native";
120 Restart = "on-abort";
121 };
122 };
123
124 environment.systemPackages = [ cfg.cliWrappers ];
125 };
126}
diff --git a/modules/luksroot.nix b/modules/luksroot.nix
new file mode 100644
index 00000000..abaee692
--- /dev/null
+++ b/modules/luksroot.nix
@@ -0,0 +1,1075 @@
1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 luks = config.boot.initrd.luks;
7 kernelPackages = config.boot.kernelPackages;
8
9 commonFunctions = ''
10 die() {
11 echo "$@" >&2
12 exit 1
13 }
14
15 dev_exist() {
16 local target="$1"
17 if [ -e $target ]; then
18 return 0
19 else
20 local uuid=$(echo -n $target | sed -e 's,UUID=\(.*\),\1,g')
21 blkid --uuid $uuid >/dev/null
22 return $?
23 fi
24 }
25
26 wait_target() {
27 local name="$1"
28 local target="$2"
29 local secs="''${3:-10}"
30 local desc="''${4:-$name $target to appear}"
31
32 if ! dev_exist $target; then
33 echo -n "Waiting $secs seconds for $desc..."
34 local success=false;
35 for try in $(seq $secs); do
36 echo -n "."
37 sleep 1
38 if dev_exist $target; then
39 success=true
40 break
41 fi
42 done
43 if [ $success == true ]; then
44 echo " - success";
45 return 0
46 else
47 echo " - failure";
48 return 1
49 fi
50 fi
51 return 0
52 }
53
54 wait_yubikey() {
55 local secs="''${1:-10}"
56
57 ykinfo -v 1>/dev/null 2>&1
58 if [ $? != 0 ]; then
59 echo -n "Waiting $secs seconds for YubiKey to appear..."
60 local success=false
61 for try in $(seq $secs); do
62 echo -n .
63 sleep 1
64 ykinfo -v 1>/dev/null 2>&1
65 if [ $? == 0 ]; then
66 success=true
67 break
68 fi
69 done
70 if [ $success == true ]; then
71 echo " - success";
72 return 0
73 else
74 echo " - failure";
75 return 1
76 fi
77 fi
78 return 0
79 }
80
81 wait_gpgcard() {
82 local secs="''${1:-10}"
83
84 gpg --card-status > /dev/null 2> /dev/null
85 if [ $? != 0 ]; then
86 echo -n "Waiting $secs seconds for GPG Card to appear"
87 local success=false
88 for try in $(seq $secs); do
89 echo -n .
90 sleep 1
91 gpg --card-status > /dev/null 2> /dev/null
92 if [ $? == 0 ]; then
93 success=true
94 break
95 fi
96 done
97 if [ $success == true ]; then
98 echo " - success";
99 return 0
100 else
101 echo " - failure";
102 return 1
103 fi
104 fi
105 return 0
106 }
107 '';
108
109 preCommands = ''
110 # A place to store crypto things
111
112 # A ramfs is used here to ensure that the file used to update
113 # the key slot with cryptsetup will never get swapped out.
114 # Warning: Do NOT replace with tmpfs!
115 mkdir -p /crypt-ramfs
116 mount -t ramfs none /crypt-ramfs
117
118 # Cryptsetup locking directory
119 mkdir -p /run/cryptsetup
120
121 # For YubiKey salt storage
122 mkdir -p /crypt-storage
123
124 ${optionalString luks.gpgSupport ''
125 export GPG_TTY=$(tty)
126 export GNUPGHOME=/crypt-ramfs/.gnupg
127
128 gpg-agent --daemon --scdaemon-program $out/bin/scdaemon > /dev/null 2> /dev/null
129 ''}
130
131 # Disable all input echo for the whole stage. We could use read -s
132 # instead but that would ocasionally leak characters between read
133 # invocations.
134 stty -echo
135 '';
136
137 postCommands = ''
138 stty echo
139 umount /crypt-storage 2>/dev/null
140 umount /crypt-ramfs 2>/dev/null
141 '';
142
143 openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, gpgCard, fido2, clevis, dmi, fallbackToPassword, preOpenCommands, postOpenCommands, ... }: assert name' == name;
144 let
145 csopen = "cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} ${optionalString (header != null) "--header=${header}"}";
146 cschange = "cryptsetup luksChangeKey ${device} ${optionalString (header != null) "--header=${header}"}";
147 in ''
148 # Wait for luksRoot (and optionally keyFile and/or header) to appear, e.g.
149 # if on a USB drive.
150 wait_target "device" ${device} || die "${device} is unavailable"
151
152 ${optionalString (header != null) ''
153 wait_target "header" ${header} || die "${header} is unavailable"
154 ''}
155
156 do_open_passphrase() {
157 local passphrase
158
159 while true; do
160 echo -n "Passphrase for ${device}: "
161 passphrase=
162 while true; do
163 if [ -e /crypt-ramfs/passphrase ]; then
164 echo "reused"
165 passphrase=$(cat /crypt-ramfs/passphrase)
166 break
167 else
168 # ask cryptsetup-askpass
169 echo -n "${device}" > /crypt-ramfs/device
170
171 # and try reading it from /dev/console with a timeout
172 IFS= read -t 1 -r passphrase
173 if [ -n "$passphrase" ]; then
174 ${if luks.reusePassphrases then ''
175 # remember it for the next device
176 echo -n "$passphrase" > /crypt-ramfs/passphrase
177 '' else ''
178 # Don't save it to ramfs. We are very paranoid
179 ''}
180 echo
181 break
182 fi
183 fi
184 done
185 echo -n "Verifying passphrase for ${device}..."
186 echo -n "$passphrase" | ${csopen} --key-file=-
187 if [ $? == 0 ]; then
188 echo " - success"
189 ${if luks.reusePassphrases then ''
190 # we don't rm here because we might reuse it for the next device
191 '' else ''
192 rm -f /crypt-ramfs/passphrase
193 ''}
194 break
195 else
196 echo " - failure"
197 # ask for a different one
198 rm -f /crypt-ramfs/passphrase
199 fi
200 done
201 }
202
203 # LUKS
204 open_normally() {
205 ${if (keyFile != null) then ''
206 if wait_target "key file" ${keyFile}; then
207 ${csopen} --key-file=${keyFile} \
208 ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"} \
209 ${optionalString (keyFileOffset != null) "--keyfile-offset=${toString keyFileOffset}"}
210 else
211 ${if fallbackToPassword then "echo" else "die"} "${keyFile} is unavailable"
212 echo " - failing back to interactive password prompt"
213 do_open_passphrase
214 fi
215 '' else ''
216 do_open_passphrase
217 ''}
218 }
219
220 ${optionalString (luks.yubikeySupport && (yubikey != null)) ''
221 # YubiKey
222 rbtohex() {
223 ( od -An -vtx1 | tr -d ' \n' )
224 }
225
226 hextorb() {
227 ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf )
228 }
229
230 do_open_yubikey() {
231 # Make all of these local to this function
232 # to prevent their values being leaked
233 local salt
234 local iterations
235 local k_user
236 local challenge
237 local response
238 local k_luks
239 local opened
240 local new_salt
241 local new_iterations
242 local new_challenge
243 local new_response
244 local new_k_luks
245
246 mount -t ${yubikey.storage.fsType} ${yubikey.storage.device} /crypt-storage || \
247 die "Failed to mount YubiKey salt storage device"
248
249 salt="$(cat /crypt-storage${yubikey.storage.path} | sed -n 1p | tr -d '\n')"
250 iterations="$(cat /crypt-storage${yubikey.storage.path} | sed -n 2p | tr -d '\n')"
251 challenge="$(echo -n $salt | openssl-wrap dgst -binary -sha512 | rbtohex)"
252 response="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)"
253
254 for try in $(seq 3); do
255 ${optionalString yubikey.twoFactor ''
256 echo -n "Enter two-factor passphrase: "
257 k_user=
258 while true; do
259 if [ -e /crypt-ramfs/passphrase ]; then
260 echo "reused"
261 k_user=$(cat /crypt-ramfs/passphrase)
262 break
263 else
264 # Try reading it from /dev/console with a timeout
265 IFS= read -t 1 -r k_user
266 if [ -n "$k_user" ]; then
267 ${if luks.reusePassphrases then ''
268 # Remember it for the next device
269 echo -n "$k_user" > /crypt-ramfs/passphrase
270 '' else ''
271 # Don't save it to ramfs. We are very paranoid
272 ''}
273 echo
274 break
275 fi
276 fi
277 done
278 ''}
279
280 if [ ! -z "$k_user" ]; then
281 k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)"
282 else
283 k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)"
284 fi
285
286 echo -n "$k_luks" | hextorb | ${csopen} --key-file=-
287
288 if [ $? == 0 ]; then
289 opened=true
290 ${if luks.reusePassphrases then ''
291 # We don't rm here because we might reuse it for the next device
292 '' else ''
293 rm -f /crypt-ramfs/passphrase
294 ''}
295 break
296 else
297 opened=false
298 echo "Authentication failed!"
299 fi
300 done
301
302 [ "$opened" == false ] && die "Maximum authentication errors reached"
303
304 echo -n "Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..."
305 for i in $(seq ${toString yubikey.saltLength}); do
306 byte="$(dd if=/dev/random bs=1 count=1 2>/dev/null | rbtohex)";
307 new_salt="$new_salt$byte";
308 echo -n .
309 done;
310 echo "ok"
311
312 new_iterations="$iterations"
313 ${optionalString (yubikey.iterationStep > 0) ''
314 new_iterations="$(($new_iterations + ${toString yubikey.iterationStep}))"
315 ''}
316
317 new_challenge="$(echo -n $new_salt | openssl-wrap dgst -binary -sha512 | rbtohex)"
318
319 new_response="$(ykchalresp -${toString yubikey.slot} -x $new_challenge 2>/dev/null)"
320
321 if [ ! -z "$k_user" ]; then
322 new_k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)"
323 else
324 new_k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)"
325 fi
326
327 echo -n "$new_k_luks" | hextorb > /crypt-ramfs/new_key
328 echo -n "$k_luks" | hextorb | ${cschange} --key-file=- /crypt-ramfs/new_key
329
330 if [ $? == 0 ]; then
331 echo -ne "$new_salt\n$new_iterations" > /crypt-storage${yubikey.storage.path}
332 else
333 echo "Warning: Could not update LUKS key, current challenge persists!"
334 fi
335
336 rm -f /crypt-ramfs/new_key
337 umount /crypt-storage
338 }
339
340 open_with_hardware() {
341 if wait_yubikey ${toString yubikey.gracePeriod}; then
342 do_open_yubikey
343 else
344 echo "No YubiKey found, falling back to non-YubiKey open procedure"
345 open_normally
346 fi
347 }
348 ''}
349
350 ${optionalString (luks.gpgSupport && (gpgCard != null)) ''
351
352 do_open_gpg_card() {
353 # Make all of these local to this function
354 # to prevent their values being leaked
355 local pin
356 local opened
357
358 gpg --import /gpg-keys/${device}/pubkey.asc > /dev/null 2> /dev/null
359
360 gpg --card-status > /dev/null 2> /dev/null
361
362 for try in $(seq 3); do
363 echo -n "PIN for GPG Card associated with device ${device}: "
364 pin=
365 while true; do
366 if [ -e /crypt-ramfs/passphrase ]; then
367 echo "reused"
368 pin=$(cat /crypt-ramfs/passphrase)
369 break
370 else
371 # and try reading it from /dev/console with a timeout
372 IFS= read -t 1 -r pin
373 if [ -n "$pin" ]; then
374 ${if luks.reusePassphrases then ''
375 # remember it for the next device
376 echo -n "$pin" > /crypt-ramfs/passphrase
377 '' else ''
378 # Don't save it to ramfs. We are very paranoid
379 ''}
380 echo
381 break
382 fi
383 fi
384 done
385 echo -n "Verifying passphrase for ${device}..."
386 echo -n "$pin" | gpg -q --batch --passphrase-fd 0 --pinentry-mode loopback -d /gpg-keys/${device}/cryptkey.gpg 2> /dev/null | ${csopen} --key-file=- > /dev/null 2> /dev/null
387 if [ $? == 0 ]; then
388 echo " - success"
389 ${if luks.reusePassphrases then ''
390 # we don't rm here because we might reuse it for the next device
391 '' else ''
392 rm -f /crypt-ramfs/passphrase
393 ''}
394 break
395 else
396 echo " - failure"
397 # ask for a different one
398 rm -f /crypt-ramfs/passphrase
399 fi
400 done
401
402 [ "$opened" == false ] && die "Maximum authentication errors reached"
403 }
404
405 open_with_hardware() {
406 if wait_gpgcard ${toString gpgCard.gracePeriod}; then
407 do_open_gpg_card
408 else
409 echo "No GPG Card found, falling back to normal open procedure"
410 open_normally
411 fi
412 }
413 ''}
414
415 ${optionalString (luks.fido2Support && (fido2.credential != null)) ''
416
417 open_with_hardware() {
418 local passsphrase
419
420 ${if fido2.passwordLess then ''
421 export passphrase=""
422 '' else ''
423 read -rsp "FIDO2 salt for ${device}: " passphrase
424 echo
425 ''}
426 ${optionalString (lib.versionOlder kernelPackages.kernel.version "5.4") ''
427 echo "On systems with Linux Kernel < 5.4, it might take a while to initialize the CRNG, you might want to use linuxPackages_latest."
428 echo "Please move your mouse to create needed randomness."
429 ''}
430 echo "Waiting for your FIDO2 device..."
431 fido2luks open ${device} ${name} ${fido2.credential} --await-dev ${toString fido2.gracePeriod} --salt string:$passphrase
432 if [ $? -ne 0 ]; then
433 echo "No FIDO2 key found, falling back to normal open procedure"
434 open_normally
435 fi
436 }
437 ''}
438
439 ${optionalString (luks.clevisSupport && clevis) ''
440
441 open_with_hardware() {
442 mkdir -p /crypt-ramfs/clevis
443
444 TMPDIR=/crypt-ramfs/clevis clevis luks unlock -d ${device} -n ${name}
445
446 if [ $? -ne 0 ]; then
447 echo "Unlocking with clevis failed, falling back to normal open procedure"
448 open_normally
449 fi
450 }
451
452 ''}
453
454 ${optionalString (luks.dmiSupport && dmi) ''
455
456 open_with_hardware() {
457 dmidecode -s system-uuid > /crypt-ramfs/passphrase
458
459 ${csopen} --key-file=- < /crypt-ramfs/passphrase > /dev/null 2> /dev/null
460
461 if [ $? -ne 0 ]; then
462 echo "Unlocking with system-uuid failed, falling back to normal open procedure"
463 rm -f /crypt-ramfs/passphrase
464 open_normally
465 ${optionalString (!luks.reusePassphrases) ''
466 else
467 rm -f /crypt-ramfs/passphrase
468 ''}
469 fi
470 }
471
472 ''}
473
474 # commands to run right before we mount our device
475 ${preOpenCommands}
476
477 ${if (luks.yubikeySupport && (yubikey != null)) || (luks.gpgSupport && (gpgCard != null)) || (luks.fido2Support && (fido2.credential != null)) || (luks.clevisSupport && clevis) || (luks.dmiSupport && dmi) then ''
478 open_with_hardware
479 '' else ''
480 open_normally
481 ''}
482
483 # commands to run right after we mounted our device
484 ${postOpenCommands}
485 '';
486
487 askPass = pkgs.writeScriptBin "cryptsetup-askpass" ''
488 #!/bin/sh
489
490 ${commonFunctions}
491
492 while true; do
493 wait_target "luks" /crypt-ramfs/device 10 "LUKS to request a passphrase" || die "Passphrase is not requested now"
494 device=$(cat /crypt-ramfs/device)
495
496 echo -n "Passphrase for $device: "
497 IFS= read -rs passphrase
498 echo
499
500 rm /crypt-ramfs/device
501 echo -n "$passphrase" > /crypt-ramfs/passphrase
502 done
503 '';
504
505 preLVM = filterAttrs (n: v: v.preLVM) luks.devices;
506 postLVM = filterAttrs (n: v: !v.preLVM) luks.devices;
507
508in
509{
510 disabledModules = [ "system/boot/luksroot.nix" ];
511
512 imports = [
513 (mkRemovedOptionModule [ "boot" "initrd" "luks" "enable" ] "")
514 ];
515
516 options = {
517
518 boot.initrd.luks.mitigateDMAAttacks = mkOption {
519 type = types.bool;
520 default = true;
521 description = ''
522 Unless enabled, encryption keys can be easily recovered by an attacker with physical
523 access to any machine with PCMCIA, ExpressCard, ThunderBolt or FireWire port.
524 More information is available at <link xlink:href="http://en.wikipedia.org/wiki/DMA_attack"/>.
525
526 This option blacklists FireWire drivers, but doesn't remove them. You can manually
527 load the drivers if you need to use a FireWire device, but don't forget to unload them!
528 '';
529 };
530
531 boot.initrd.luks.cryptoModules = mkOption {
532 type = types.listOf types.str;
533 default =
534 [ "aes" "aes_generic" "blowfish" "twofish"
535 "serpent" "cbc" "xts" "lrw" "sha1" "sha256" "sha512"
536 "af_alg" "algif_skcipher"
537 ];
538 description = ''
539 A list of cryptographic kernel modules needed to decrypt the root device(s).
540 The default includes all common modules.
541 '';
542 };
543
544 boot.initrd.luks.forceLuksSupportInInitrd = mkOption {
545 type = types.bool;
546 default = false;
547 internal = true;
548 description = ''
549 Whether to configure luks support in the initrd, when no luks
550 devices are configured.
551 '';
552 };
553
554 boot.initrd.luks.reusePassphrases = mkOption {
555 type = types.bool;
556 default = true;
557 description = ''
558 When opening a new LUKS device try reusing last successful
559 passphrase.
560
561 Useful for mounting a number of devices that use the same
562 passphrase without retyping it several times.
563
564 Such setup can be useful if you use <command>cryptsetup
565 luksSuspend</command>. Different LUKS devices will still have
566 different master keys even when using the same passphrase.
567 '';
568 };
569
570 boot.initrd.luks.devices = mkOption {
571 default = { };
572 example = { luksroot.device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; };
573 description = ''
574 The encrypted disk that should be opened before the root
575 filesystem is mounted. Both LVM-over-LUKS and LUKS-over-LVM
576 setups are supported. The unencrypted devices can be accessed as
577 <filename>/dev/mapper/<replaceable>name</replaceable></filename>.
578 '';
579
580 type = with types; attrsOf (submodule (
581 { name, ... }: { options = {
582
583 name = mkOption {
584 visible = false;
585 default = name;
586 example = "luksroot";
587 type = types.str;
588 description = "Name of the unencrypted device in <filename>/dev/mapper</filename>.";
589 };
590
591 device = mkOption {
592 example = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08";
593 type = types.str;
594 description = "Path of the underlying encrypted block device.";
595 };
596
597 header = mkOption {
598 default = null;
599 example = "/root/header.img";
600 type = types.nullOr types.str;
601 description = ''
602 The name of the file or block device that
603 should be used as header for the encrypted device.
604 '';
605 };
606
607 keyFile = mkOption {
608 default = null;
609 example = "/dev/sdb1";
610 type = types.nullOr types.str;
611 description = ''
612 The name of the file (can be a raw device or a partition) that
613 should be used as the decryption key for the encrypted device. If
614 not specified, you will be prompted for a passphrase instead.
615 '';
616 };
617
618 keyFileSize = mkOption {
619 default = null;
620 example = 4096;
621 type = types.nullOr types.int;
622 description = ''
623 The size of the key file. Use this if only the beginning of the
624 key file should be used as a key (often the case if a raw device
625 or partition is used as key file). If not specified, the whole
626 <literal>keyFile</literal> will be used decryption, instead of just
627 the first <literal>keyFileSize</literal> bytes.
628 '';
629 };
630
631 keyFileOffset = mkOption {
632 default = null;
633 example = 4096;
634 type = types.nullOr types.int;
635 description = ''
636 The offset of the key file. Use this in combination with
637 <literal>keyFileSize</literal> to use part of a file as key file
638 (often the case if a raw device or partition is used as a key file).
639 If not specified, the key begins at the first byte of
640 <literal>keyFile</literal>.
641 '';
642 };
643
644 # FIXME: get rid of this option.
645 preLVM = mkOption {
646 default = true;
647 type = types.bool;
648 description = "Whether the luksOpen will be attempted before LVM scan or after it.";
649 };
650
651 allowDiscards = mkOption {
652 default = false;
653 type = types.bool;
654 description = ''
655 Whether to allow TRIM requests to the underlying device. This option
656 has security implications; please read the LUKS documentation before
657 activating it.
658 '';
659 };
660
661 fallbackToPassword = mkOption {
662 default = false;
663 type = types.bool;
664 description = ''
665 Whether to fallback to interactive passphrase prompt if the keyfile
666 cannot be found. This will prevent unattended boot should the keyfile
667 go missing.
668 '';
669 };
670
671 gpgCard = mkOption {
672 default = null;
673 description = ''
674 The option to use this LUKS device with a GPG encrypted luks password by the GPG Smartcard.
675 If null (the default), GPG-Smartcard will be disabled for this device.
676 '';
677
678 type = with types; nullOr (submodule {
679 options = {
680 gracePeriod = mkOption {
681 default = 10;
682 type = types.int;
683 description = "Time in seconds to wait for the GPG Smartcard.";
684 };
685
686 encryptedPass = mkOption {
687 default = "";
688 type = types.path;
689 description = "Path to the GPG encrypted passphrase.";
690 };
691
692 publicKey = mkOption {
693 default = "";
694 type = types.path;
695 description = "Path to the Public Key.";
696 };
697 };
698 });
699 };
700
701 fido2 = {
702 credential = mkOption {
703 default = null;
704 example = "f1d00200d8dc783f7fb1e10ace8da27f8312d72692abfca2f7e4960a73f48e82e1f7571f6ebfcee9fb434f9886ccc8fcc52a6614d8d2";
705 type = types.nullOr types.str;
706 description = "The FIDO2 credential ID.";
707 };
708
709 gracePeriod = mkOption {
710 default = 10;
711 type = types.int;
712 description = "Time in seconds to wait for the FIDO2 key.";
713 };
714
715 passwordLess = mkOption {
716 default = false;
717 type = types.bool;
718 description = ''
719 Defines whatever to use an empty string as a default salt.
720
721 Enable only when your device is PIN protected, such as <link xlink:href="https://trezor.io/">Trezor</link>.
722 '';
723 };
724 };
725
726 yubikey = mkOption {
727 default = null;
728 description = ''
729 The options to use for this LUKS device in YubiKey-PBA.
730 If null (the default), YubiKey-PBA will be disabled for this device.
731 '';
732
733 type = with types; nullOr (submodule {
734 options = {
735 twoFactor = mkOption {
736 default = true;
737 type = types.bool;
738 description = "Whether to use a passphrase and a YubiKey (true), or only a YubiKey (false).";
739 };
740
741 slot = mkOption {
742 default = 2;
743 type = types.int;
744 description = "Which slot on the YubiKey to challenge.";
745 };
746
747 saltLength = mkOption {
748 default = 16;
749 type = types.int;
750 description = "Length of the new salt in byte (64 is the effective maximum).";
751 };
752
753 keyLength = mkOption {
754 default = 64;
755 type = types.int;
756 description = "Length of the LUKS slot key derived with PBKDF2 in byte.";
757 };
758
759 iterationStep = mkOption {
760 default = 0;
761 type = types.int;
762 description = "How much the iteration count for PBKDF2 is increased at each successful authentication.";
763 };
764
765 gracePeriod = mkOption {
766 default = 10;
767 type = types.int;
768 description = "Time in seconds to wait for the YubiKey.";
769 };
770
771 /* TODO: Add to the documentation of the current module:
772
773 Options related to the storing the salt.
774 */
775 storage = {
776 device = mkOption {
777 default = "/dev/sda1";
778 type = types.path;
779 description = ''
780 An unencrypted device that will temporarily be mounted in stage-1.
781 Must contain the current salt to create the challenge for this LUKS device.
782 '';
783 };
784
785 fsType = mkOption {
786 default = "vfat";
787 type = types.str;
788 description = "The filesystem of the unencrypted device.";
789 };
790
791 path = mkOption {
792 default = "/crypt-storage/default";
793 type = types.str;
794 description = ''
795 Absolute path of the salt on the unencrypted device with
796 that device's root directory as "/".
797 '';
798 };
799 };
800 };
801 });
802 };
803
804 clevis = mkOption {
805 type = types.bool;
806 default = false;
807 description = ''
808 Unlock device via clevis (e.g. with a tpm)
809 '';
810 };
811
812 dmi = mkOption {
813 type = types.bool;
814 default = false;
815 description = ''
816 Unlock device via system-uuid (via dmidecode)
817 '';
818 };
819
820 preOpenCommands = mkOption {
821 type = types.lines;
822 default = "";
823 example = ''
824 mkdir -p /tmp/persistent
825 mount -t zfs rpool/safe/persistent /tmp/persistent
826 '';
827 description = ''
828 Commands that should be run right before we try to mount our LUKS device.
829 This can be useful, if the keys needed to open the drive is on another partion.
830 '';
831 };
832
833 postOpenCommands = mkOption {
834 type = types.lines;
835 default = "";
836 example = ''
837 umount /tmp/persistent
838 '';
839 description = ''
840 Commands that should be run right after we have mounted our LUKS device.
841 '';
842 };
843 };
844 }));
845 };
846
847 boot.initrd.luks.gpgSupport = mkOption {
848 default = false;
849 type = types.bool;
850 description = ''
851 Enables support for authenticating with a GPG encrypted password.
852 '';
853 };
854
855 boot.initrd.luks.yubikeySupport = mkOption {
856 default = false;
857 type = types.bool;
858 description = ''
859 Enables support for authenticating with a YubiKey on LUKS devices.
860 See the NixOS wiki for information on how to properly setup a LUKS device
861 and a YubiKey to work with this feature.
862 '';
863 };
864
865 boot.initrd.luks.fido2Support = mkOption {
866 default = false;
867 type = types.bool;
868 description = ''
869 Enables support for authenticating with FIDO2 devices.
870 '';
871 };
872
873 boot.initrd.luks.clevisSupport = mkOption {
874 default = false;
875 type = types.bool;
876 description = ''
877 Enables support for unlocking luks volumes via clevis (e.g. with a tpm)
878 '';
879 };
880
881 boot.initrd.luks.dmiSupport = mkOption {
882 default = false;
883 type = types.bool;
884 description = ''
885 Enables support for unlocking luks volumes via system-uuid (via dmidecode)
886 '';
887 };
888
889 };
890
891 config = mkIf (luks.devices != {} || luks.forceLuksSupportInInitrd) {
892
893 assertions =
894 [ { assertion = !(luks.gpgSupport && luks.yubikeySupport);
895 message = "YubiKey and GPG Card may not be used at the same time.";
896 }
897
898 { assertion = !(luks.gpgSupport && luks.fido2Support);
899 message = "FIDO2 and GPG Card may not be used at the same time.";
900 }
901
902 { assertion = !(luks.gpgSupport && luks.clevisSupport);
903 message = "Clevis and GPG Card may not be used at the same time.";
904 }
905
906 { assertion = !(luks.gpgSupport && luks.dmiSupport);
907 message = "DMI and GPG Card may not be used at the same time.";
908 }
909
910 { assertion = !(luks.fido2Support && luks.yubikeySupport);
911 message = "FIDO2 and YubiKey may not be used at the same time.";
912 }
913
914 { assertion = !(luks.fido2Support && luks.clevisSupport);
915 message = "FIDO2 and Clevis may not be used at the same time.";
916 }
917
918 { assertion = !(luks.fido2Support && luks.dmiSupport);
919 message = "FIDO2 and DMI may not be used at the same time.";
920 }
921
922 { assertion = !(luks.yubikeySupport && luks.clevisSupport);
923 message = "Clevis and YubiKey may not be used at the same time.";
924 }
925
926 { assertion = !(luks.yubikeySupport && luks.dmiSupport);
927 message = "DMI and YubiKey may not be used at the same time.";
928 }
929
930 ];
931
932 # actually, sbp2 driver is the one enabling the DMA attack, but this needs to be tested
933 boot.blacklistedKernelModules = optionals luks.mitigateDMAAttacks
934 ["firewire_ohci" "firewire_core" "firewire_sbp2"];
935
936 # Some modules that may be needed for mounting anything ciphered
937 boot.initrd.availableKernelModules = [ "dm_mod" "dm_crypt" "cryptd" "input_leds" ]
938 ++ luks.cryptoModules
939 # workaround until https://marc.info/?l=linux-crypto-vger&m=148783562211457&w=4 is merged
940 # remove once 'modprobe --show-depends xts' shows ecb as a dependency
941 ++ (if builtins.elem "xts" luks.cryptoModules then ["ecb"] else []);
942
943 # copy the cryptsetup binary and it's dependencies
944 boot.initrd.extraUtilsCommands =
945 let
946 extraUtils = config.system.build.extraUtils;
947
948 ipkgs = pkgs.appendOverlays [
949 (final: prev: {
950 tpm2-tss = prev.tpm2-tss.overrideAttrs (oldAttrs: {
951 doCheck = false;
952 patches = [];
953 postPatch = ''
954 patchShebangs script
955 '';
956 configureFlags = [];
957 });
958 })
959 ];
960 in ''
961 copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup
962 copy_bin_and_libs ${askPass}/bin/cryptsetup-askpass
963 sed -i s,/bin/sh,$out/bin/sh, $out/bin/cryptsetup-askpass
964
965 ${optionalString luks.yubikeySupport ''
966 copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykchalresp
967 copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykinfo
968 copy_bin_and_libs ${pkgs.openssl.bin}/bin/openssl
969
970 cc -O3 -I${pkgs.openssl.dev}/include -L${pkgs.openssl.out}/lib ${./pbkdf2-sha512.c} -o pbkdf2-sha512 -lcrypto
971 strip -s pbkdf2-sha512
972 copy_bin_and_libs pbkdf2-sha512
973
974 mkdir -p $out/etc/ssl
975 cp -pdv ${pkgs.openssl.out}/etc/ssl/openssl.cnf $out/etc/ssl
976
977 cat > $out/bin/openssl-wrap <<EOF
978 #!$out/bin/sh
979 export OPENSSL_CONF=$out/etc/ssl/openssl.cnf
980 $out/bin/openssl "\$@"
981 EOF
982 chmod +x $out/bin/openssl-wrap
983 ''}
984
985 ${optionalString luks.fido2Support ''
986 copy_bin_and_libs ${pkgs.fido2luks}/bin/fido2luks
987 ''}
988
989
990 ${optionalString luks.gpgSupport ''
991 copy_bin_and_libs ${pkgs.gnupg}/bin/gpg
992 copy_bin_and_libs ${pkgs.gnupg}/bin/gpg-agent
993 copy_bin_and_libs ${pkgs.gnupg}/libexec/scdaemon
994
995 ${concatMapStringsSep "\n" (x:
996 if x.gpgCard != null then
997 ''
998 mkdir -p $out/secrets/gpg-keys/${x.device}
999 cp -a ${x.gpgCard.encryptedPass} $out/secrets/gpg-keys/${x.device}/cryptkey.gpg
1000 cp -a ${x.gpgCard.publicKey} $out/secrets/gpg-keys/${x.device}/pubkey.asc
1001 ''
1002 else ""
1003 ) (attrValues luks.devices)
1004 }
1005 ''}
1006
1007 ${optionalString luks.clevisSupport ''
1008 for bin in ${ipkgs.tpm2-tools}/bin/* ${ipkgs.jose}/bin/* ${ipkgs.libpwquality}/bin/*; do
1009 if [ -L $bin ]; then
1010 cp -v $bin $out/bin
1011 else
1012 copy_bin_and_libs $bin
1013 fi
1014 done
1015
1016 copy_bin_and_libs ${ipkgs.bash}/bin/bash
1017 for bin in ${ipkgs.clevis}/bin/* ${ipkgs.clevis}/bin/.*; do
1018 [ -f $bin -o -L $bin ] || continue
1019
1020 substitute $bin $out/bin/$(basename $bin) \
1021 --replace ${ipkgs.bash}/bin $out/bin \
1022 --replace ${ipkgs.clevis}/bin $out/bin \
1023 --replace ${ipkgs.tpm2-tools}/bin $out/bin \
1024 --replace ${ipkgs.jose}/bin $out/bin \
1025 --replace ${ipkgs.libpwquality}/bin $out/bin \
1026 --replace ${ipkgs.coreutils}/bin $out/bin
1027
1028 [ -x $bin ] && chmod +x $out/bin/$(basename $bin)
1029 done
1030
1031 for lib in ${ipkgs.tpm2-tss}/lib/*.so*; do
1032 if [ -f $lib ]; then
1033 patchelf --output $out/lib/$(basename $lib) $lib \
1034 --set-rpath $out/lib
1035 else
1036 cp -pdv $lib $out/lib
1037 fi
1038 done
1039 ''}
1040
1041 ${optionalString luks.dmiSupport ''
1042 copy_bin_and_libs ${pkgs.dmidecode}/bin/dmidecode
1043 ''}
1044 '';
1045
1046 boot.initrd.extraUtilsCommandsTest = ''
1047 $out/bin/cryptsetup --version
1048 ${optionalString luks.yubikeySupport ''
1049 $out/bin/ykchalresp -V
1050 $out/bin/ykinfo -V
1051 $out/bin/openssl-wrap version
1052 ''}
1053 ${optionalString luks.gpgSupport ''
1054 $out/bin/gpg --version
1055 $out/bin/gpg-agent --version
1056 $out/bin/scdaemon --version
1057 ''}
1058 ${optionalString luks.fido2Support ''
1059 $out/bin/fido2luks --version
1060 ''}
1061 ${optionalString luks.clevisSupport ''
1062 $out/bin/jose alg
1063 ''}
1064 ${optionalString luks.dmiSupport ''
1065 $out/bin/dmidecode --version
1066 ''}
1067 '';
1068
1069 boot.initrd.preFailCommands = postCommands;
1070 boot.initrd.preLVMCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand preLVM) + postCommands;
1071 boot.initrd.postDeviceCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands;
1072
1073 environment.systemPackages = [ pkgs.cryptsetup ];
1074 };
1075}
diff --git a/modules/networkd.nix b/modules/networkd.nix
new file mode 100644
index 00000000..d0a48f98
--- /dev/null
+++ b/modules/networkd.nix
@@ -0,0 +1,297 @@
1{ config, lib, utils, pkgs, ... }:
2
3with utils;
4with lib;
5
6let
7
8 cfg = config.networking;
9 interfaces = attrValues cfg.interfaces;
10
11 interfaceIps = i:
12 i.ipv4.addresses
13 ++ optionals cfg.enableIPv6 i.ipv6.addresses;
14
15 dhcpStr = useDHCP: if useDHCP == true || useDHCP == null then "yes" else "no";
16
17 slaves =
18 concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds))
19 ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges))
20 ++ map (sit: sit.dev) (attrValues cfg.sits)
21 ++ map (vlan: vlan.interface) (attrValues cfg.vlans)
22 # add dependency to physical or independently created vswitch member interface
23 # TODO: warn the user that any address configured on those interfaces will be useless
24 ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches);
25
26in
27
28{
29 disabledModules = [ "tasks/network-interfaces-systemd.nix" ];
30
31 config = mkIf cfg.useNetworkd {
32
33 assertions = [ {
34 assertion = cfg.defaultGatewayWindowSize == null;
35 message = "networking.defaultGatewayWindowSize is not supported by networkd.";
36 } {
37 assertion = cfg.vswitches == {};
38 message = "networking.vswitches are not supported by networkd.";
39 } {
40 assertion = cfg.defaultGateway == null || cfg.defaultGateway.interface == null;
41 message = "networking.defaultGateway.interface is not supported by networkd.";
42 } {
43 assertion = cfg.defaultGateway6 == null || cfg.defaultGateway6.interface == null;
44 message = "networking.defaultGateway6.interface is not supported by networkd.";
45 } {
46 assertion = cfg.useDHCP == false;
47 message = ''
48 networking.useDHCP is not supported by networkd.
49 Please use per interface configuration and set the global option to false.
50 '';
51 } ] ++ flip mapAttrsToList cfg.bridges (n: { rstp, ... }: {
52 assertion = !rstp;
53 message = "networking.bridges.${n}.rstp is not supported by networkd.";
54 });
55
56 networking.dhcpcd.enable = mkDefault false;
57
58 systemd.network =
59 let
60 domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
61 genericNetwork = override:
62 let gateways = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address
63 ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address;
64 in optionalAttrs (gateways != [ ]) {
65 routes = override (map (gateway: {
66 routeConfig = {
67 Gateway = gateway;
68 GatewayOnLink = false;
69 };
70 }) gateways);
71 } // optionalAttrs (domains != [ ]) {
72 domains = override domains;
73 };
74 in mkMerge [ {
75 enable = true;
76 }
77 (mkMerge (forEach interfaces (i: {
78 netdevs = mkIf i.virtual ({
79 "40-${i.name}" = {
80 netdevConfig = {
81 Name = i.name;
82 Kind = i.virtualType;
83 };
84 "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
85 User = i.virtualOwner;
86 };
87 };
88 });
89 networks."40-${i.name}" = mkMerge [ (genericNetwork mkDefault) {
90 name = mkDefault i.name;
91 DHCP = mkForce (dhcpStr
92 (if i.useDHCP != null then i.useDHCP else false));
93 address = forEach (interfaceIps i)
94 (ip: "${ip.address}/${toString ip.prefixLength}");
95 networkConfig.IPv6PrivacyExtensions = "kernel";
96 linkConfig = optionalAttrs (i.macAddress != null) {
97 MACAddress = i.macAddress;
98 } // optionalAttrs (i.mtu != null) {
99 MTUBytes = toString i.mtu;
100 };
101 }];
102 })))
103 (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: {
104 netdevs."40-${name}" = {
105 netdevConfig = {
106 Name = name;
107 Kind = "bridge";
108 };
109 };
110 networks = listToAttrs (forEach bridge.interfaces (bi:
111 nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) {
112 DHCP = mkOverride 0 (dhcpStr false);
113 networkConfig.Bridge = name;
114 } ])));
115 })))
116 (mkMerge (flip mapAttrsToList cfg.bonds (name: bond: {
117 netdevs."40-${name}" = {
118 netdevConfig = {
119 Name = name;
120 Kind = "bond";
121 };
122 bondConfig = let
123 # manual mapping as of 2017-02-03
124 # man 5 systemd.netdev [BOND]
125 # to https://www.kernel.org/doc/Documentation/networking/bonding.txt
126 # driver options.
127 driverOptionMapping = let
128 trans = f: optName: { valTransform = f; optNames = [optName]; };
129 simp = trans id;
130 ms = trans (v: v + "ms");
131 in {
132 Mode = simp "mode";
133 TransmitHashPolicy = simp "xmit_hash_policy";
134 LACPTransmitRate = simp "lacp_rate";
135 MIIMonitorSec = ms "miimon";
136 UpDelaySec = ms "updelay";
137 DownDelaySec = ms "downdelay";
138 LearnPacketIntervalSec = simp "lp_interval";
139 AdSelect = simp "ad_select";
140 FailOverMACPolicy = simp "fail_over_mac";
141 ARPValidate = simp "arp_validate";
142 # apparently in ms for this value?! Upstream bug?
143 ARPIntervalSec = simp "arp_interval";
144 ARPIPTargets = simp "arp_ip_target";
145 ARPAllTargets = simp "arp_all_targets";
146 PrimaryReselectPolicy = simp "primary_reselect";
147 ResendIGMP = simp "resend_igmp";
148 PacketsPerSlave = simp "packets_per_slave";
149 GratuitousARP = { valTransform = id;
150 optNames = [ "num_grat_arp" "num_unsol_na" ]; };
151 AllSlavesActive = simp "all_slaves_active";
152 MinLinks = simp "min_links";
153 };
154
155 do = bond.driverOptions;
156 assertNoUnknownOption = let
157 knownOptions = flatten (mapAttrsToList (_: kOpts: kOpts.optNames)
158 driverOptionMapping);
159 # options that apparently don’t exist in the networkd config
160 unknownOptions = [ "primary" ];
161 assertTrace = bool: msg: if bool then true else builtins.trace msg false;
162 in assert all (driverOpt: assertTrace
163 (elem driverOpt (knownOptions ++ unknownOptions))
164 "The bond.driverOption `${driverOpt}` cannot be mapped to the list of known networkd bond options. Please add it to the mapping above the assert or to `unknownOptions` should it not exist in networkd.")
165 (mapAttrsToList (k: _: k) do); "";
166 # get those driverOptions that have been set
167 filterSystemdOptions = filterAttrs (sysDOpt: kOpts:
168 any (kOpt: do ? ${kOpt}) kOpts.optNames);
169 # build final set of systemd options to bond values
170 buildOptionSet = mapAttrs (_: kOpts: with kOpts;
171 # we simply take the first set kernel bond option
172 # (one option has multiple names, which is silly)
173 head (map (optN: valTransform (do.${optN}))
174 # only map those that exist
175 (filter (o: do ? ${o}) optNames)));
176 in seq assertNoUnknownOption
177 (buildOptionSet (filterSystemdOptions driverOptionMapping));
178
179 };
180
181 networks = listToAttrs (forEach bond.interfaces (bi:
182 nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) {
183 DHCP = mkOverride 0 (dhcpStr false);
184 networkConfig.Bond = name;
185 } ])));
186 })))
187 (mkMerge (flip mapAttrsToList cfg.macvlans (name: macvlan: {
188 netdevs."40-${name}" = {
189 netdevConfig = {
190 Name = name;
191 Kind = "macvlan";
192 };
193 macvlanConfig = optionalAttrs (macvlan.mode != null) { Mode = macvlan.mode; };
194 };
195 networks."40-${macvlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) {
196 macvlan = [ name ];
197 } ]);
198 })))
199 (mkMerge (flip mapAttrsToList cfg.sits (name: sit: {
200 netdevs."40-${name}" = {
201 netdevConfig = {
202 Name = name;
203 Kind = "sit";
204 };
205 tunnelConfig =
206 (optionalAttrs (sit.remote != null) {
207 Remote = sit.remote;
208 }) // (optionalAttrs (sit.local != null) {
209 Local = sit.local;
210 }) // (optionalAttrs (sit.ttl != null) {
211 TTL = sit.ttl;
212 });
213 };
214 networks = mkIf (sit.dev != null) {
215 "40-${sit.dev}" = (mkMerge [ (genericNetwork (mkOverride 999)) {
216 tunnel = [ name ];
217 } ]);
218 };
219 })))
220 (mkMerge (flip mapAttrsToList cfg.vlans (name: vlan: {
221 netdevs."40-${name}" = {
222 netdevConfig = {
223 Name = name;
224 Kind = "vlan";
225 };
226 vlanConfig.Id = vlan.id;
227 };
228 networks."40-${vlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) {
229 vlan = [ name ];
230 } ]);
231 })))
232 ];
233
234 # We need to prefill the slaved devices with networking options
235 # This forces the network interface creator to initialize slaves.
236 networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves);
237
238 systemd.services = let
239 # We must escape interfaces due to the systemd interpretation
240 subsystemDevice = interface:
241 "sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
242 # support for creating openvswitch switches
243 createVswitchDevice = n: v: nameValuePair "${n}-netdev"
244 (let
245 deps = map subsystemDevice (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces));
246 ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
247 in
248 { description = "Open vSwitch Interface ${n}";
249 wantedBy = [ "network.target" (subsystemDevice n) ];
250 # and create bridge before systemd-networkd starts because it might create internal interfaces
251 before = [ "systemd-networkd.service" ];
252 # shutdown the bridge when network is shutdown
253 partOf = [ "network.target" ];
254 # requires ovs-vswitchd to be alive at all times
255 bindsTo = [ "ovs-vswitchd.service" ];
256 # start switch after physical interfaces and vswitch daemon
257 after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps;
258 wants = deps; # if one or more interface fails, the switch should continue to run
259 serviceConfig.Type = "oneshot";
260 serviceConfig.RemainAfterExit = true;
261 path = [ pkgs.iproute2 config.virtualisation.vswitch.package ];
262 preStart = ''
263 echo "Resetting Open vSwitch ${n}..."
264 ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
265 -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions}
266 '';
267 script = ''
268 echo "Configuring Open vSwitch ${n}..."
269 ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \
270 ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \
271 ${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \
272 ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}
273
274
275 echo "Adding OpenFlow rules for Open vSwitch ${n}..."
276 ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules}
277 '';
278 postStop = ''
279 echo "Cleaning Open vSwitch ${n}"
280 echo "Shuting down internal ${n} interface"
281 ip link set ${n} down || true
282 echo "Deleting flows for ${n}"
283 ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
284 echo "Deleting Open vSwitch ${n}"
285 ovs-vsctl --if-exists del-br ${n} || true
286 '';
287 });
288 in mapAttrs' createVswitchDevice cfg.vswitches
289 // {
290 "network-local-commands" = {
291 after = [ "systemd-networkd.service" ];
292 bindsTo = [ "systemd-networkd.service" ];
293 };
294 };
295 };
296
297}
diff --git a/modules/tinc-networkmanager.nix b/modules/tinc-networkmanager.nix
new file mode 100644
index 00000000..ff03abd2
--- /dev/null
+++ b/modules/tinc-networkmanager.nix
@@ -0,0 +1,36 @@
1{ lib, config, pkgs, ... }:
2let
3 cfg = config.services.tinc;
4in {
5 options = {
6 services.tinc.networks = lib.mkOption {
7 type = lib.types.attrsOf (lib.types.submodule {
8 options.nmDispatch = lib.mkOption {
9 type = lib.types.bool;
10 default = config.networking.networkmanager.enable;
11 description = ''
12 Install a network-manager dispatcher script to automatically
13 connect to all remotes when networking is available
14 '';
15 };
16 });
17 };
18 };
19
20 config = {
21 networking.networkmanager.dispatcherScripts = lib.concatLists (lib.flip lib.mapAttrsToList cfg.networks (network: data: lib.optional data.nmDispatch {
22 type = "basic";
23 source = pkgs.writeScript "connect-${network}.sh" ''
24 #!${pkgs.stdenv.shell}
25
26 shopt -s extglob
27
28 case "''${2}" in
29 (?(vpn-)up)
30 ${data.package}/bin/tinc -n ${network} --pidfile /run/tinc.${network}.pid --batch retry
31 ;;
32 esac
33 '';
34 }));
35 };
36}
diff --git a/modules/uucp.nix b/modules/uucp.nix
new file mode 100644
index 00000000..0334a3db
--- /dev/null
+++ b/modules/uucp.nix
@@ -0,0 +1,391 @@
1{ flake, config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 portSpec = name: node: concatStringsSep "\n" (map (port: ''
7 port ${name}.${port}
8 type pipe
9 protocol ${node.protocols}
10 reliable true
11 command ${pkgs.openssh}/bin/ssh -x -o batchmode=yes ${name}.${port}
12 '') node.hostnames);
13 sysSpec = name: node: ''
14 system ${name}
15 time any
16 chat-seven-bit false
17 chat . ""
18 protocol ${node.protocols}
19 command-path ${concatStringsSep " " cfg.commandPath}
20 commands ${concatStringsSep " " node.commands}
21 ${concatStringsSep "\nalternate\n" (map (port: ''
22 port ${name}.${port}
23 '') node.hostnames)}
24 '';
25 sshConfig = name: node: concatStringsSep "\n" (map (port: ''
26 Host ${name}.${port}
27 Hostname ${port}
28 IdentitiesOnly Yes
29 IdentityFile ${cfg.sshKeyDir}/${name}
30 '') node.hostnames);
31 sshKeyGen = name: node: ''
32 if [[ ! -e ${cfg.sshKeyDir}/${name} ]]; then
33 ${pkgs.openssh}/bin/ssh-keygen ${escapeShellArgs node.generateKey} -f ${cfg.sshKeyDir}/${name}
34 fi
35 '';
36 restrictKey = key: ''
37 restrict,command="${chat}" ${key}
38 '';
39 chat = pkgs.writeScript "chat" ''
40 #!${pkgs.stdenv.shell}
41
42 echo .
43 exec ${config.security.wrapperDir}/uucico
44 '';
45
46 nodeCfg = {
47 options = {
48 commands = mkOption {
49 type = types.listOf types.str;
50 default = cfg.defaultCommands;
51 description = "Commands to allow for this remote";
52 };
53
54 protocols = mkOption {
55 type = types.separatedString "";
56 default = cfg.defaultProtocols;
57 description = "UUCP protocols to use for this remote";
58 };
59
60 publicKeys = mkOption {
61 type = types.listOf types.str;
62 default = [];
63 description = "SSH client public keys for this node";
64 };
65
66 generateKey = mkOption {
67 type = types.listOf types.str;
68 default = [ "-t" "ed25519" "-N" "" ];
69 description = "Arguments to pass to `ssh-keygen` to generate a keypair for communication with this host";
70 };
71
72 hostnames = mkOption {
73 type = types.listOf types.str;
74 default = [];
75 description = "Hostnames to try in order when connecting";
76 };
77 };
78 };
79
80 cfg = config.services.uucp;
81in {
82 options = {
83 services.uucp = {
84 enable = mkOption {
85 type = types.bool;
86 default = false;
87 description = ''
88 If enabled we set up an account accesible via uucp over ssh
89 '';
90 };
91
92 nodeName = mkOption {
93 type = types.str;
94 default = "nixos";
95 description = "uucp node name";
96 };
97
98 sshUser = mkOption {
99 type = types.attrs;
100 default = {};
101 description = "Overrides for the local uucp linux-user";
102 };
103
104 extraSSHConfig = mkOption {
105 type = types.str;
106 default = "";
107 description = "Extra SSH config";
108 };
109
110 remoteNodes = mkOption {
111 type = types.attrsOf (types.submodule nodeCfg);
112 default = {};
113 description = ''
114 Ports to set up
115 Names will probably need to be configured in sshConfig
116 '';
117 };
118
119 commandPath = mkOption {
120 type = types.listOf types.path;
121 default = [ "${pkgs.rmail}/bin" ];
122 description = ''
123 Command search path for all systems
124 '';
125 };
126
127 defaultCommands = mkOption {
128 type = types.listOf types.str;
129 default = ["rmail"];
130 description = "Commands allowed for remotes without explicit override";
131 };
132
133 defaultProtocols = mkOption {
134 type = types.separatedString "";
135 default = "te";
136 description = "UUCP protocol to use within ssh unless overriden";
137 };
138
139 incomingProtocols = mkOption {
140 type = types.separatedString "";
141 default = "te";
142 description = "UUCP protocols to use when called";
143 };
144
145 homeDir = mkOption {
146 type = types.path;
147 default = "/var/uucp";
148 description = "Home of the uucp user";
149 };
150
151 sshKeyDir = mkOption {
152 type = types.path;
153 default = "${cfg.homeDir}/.ssh/";
154 description = "Directory to store ssh keypairs";
155 };
156
157 spoolDir = mkOption {
158 type = types.path;
159 default = "/var/spool/uucp";
160 description = "Spool directory";
161 };
162
163 lockDir = mkOption {
164 type = types.path;
165 default = "/var/spool/uucp";
166 description = "Lock directory";
167 };
168
169 pubDir = mkOption {
170 type = types.path;
171 default = "/var/spool/uucppublic";
172 description = "Public directory";
173 };
174
175 logFile = mkOption {
176 type = types.path;
177 default = "/var/log/uucp";
178 description = "Log file";
179 };
180
181 statFile = mkOption {
182 type = types.path;
183 default = "/var/log/uucp.stat";
184 description = "Statistics file";
185 };
186
187 debugFile = mkOption {
188 type = types.path;
189 default = "/var/log/uucp.debug";
190 description = "Debug file";
191 };
192
193 interval = mkOption {
194 type = types.nullOr types.str;
195 default = "1h";
196 description = ''
197 Specification of when to run `uucico' in format used by systemd timers
198 The default is to do so every hour
199 '';
200 };
201
202 nmDispatch = mkOption {
203 type = types.bool;
204 default = config.networking.networkmanager.enable;
205 description = ''
206 Install a network-manager dispatcher script to automatically
207 call all remotes when networking is available
208 '';
209 };
210
211 extraConfig = mkOption {
212 type = types.lines;
213 default = ''
214 run-uuxqt 1
215 '';
216 description = "Extra configuration to append verbatim to `/etc/uucp/config'";
217 };
218
219 extraSys = mkOption {
220 type = types.lines;
221 default = ''
222 protocol-parameter g packet-size 4096
223 '';
224 description = "Extra configuration to prepend verbatim to `/etc/uucp/sys`";
225 };
226 };
227 };
228
229 config = mkIf cfg.enable {
230 environment.etc."uucp/config" = {
231 text = ''
232 hostname ${cfg.nodeName}
233
234 spool ${cfg.spoolDir}
235 lockdir ${cfg.lockDir}
236 pubdir ${cfg.pubDir}
237 logfile ${cfg.logFile}
238 statfile ${cfg.statFile}
239 debugfile ${cfg.debugFile}
240
241 ${cfg.extraConfig}
242 '';
243 };
244
245 users.users."uucp" = {
246 name = "uucp";
247 isSystemUser = true;
248 isNormalUser = false;
249 createHome = true;
250 home = cfg.homeDir;
251 description = "User for uucp over ssh";
252 useDefaultShell = true;
253 openssh.authorizedKeys.keys = map restrictKey (concatLists (mapAttrsToList (name: node: node.publicKeys) cfg.remoteNodes));
254 } // cfg.sshUser;
255
256 system.activationScripts."uucp-sshconfig" = ''
257 mkdir -p ${config.users.users."uucp".home}/.ssh
258 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${config.users.users."uucp".home}/.ssh
259 chmod 700 ${config.users.users."uucp".home}/.ssh
260 ln -fs ${builtins.toFile "ssh-config" ''
261 ${concatStringsSep "\n" (mapAttrsToList sshConfig cfg.remoteNodes)}
262
263 ${cfg.extraSSHConfig}
264 ''} ${config.users.users."uucp".home}/.ssh/config
265
266 mkdir -p ${cfg.sshKeyDir}
267 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.sshKeyDir}
268 chmod 700 ${cfg.sshKeyDir}
269
270 ${concatStringsSep "\n" (mapAttrsToList sshKeyGen cfg.remoteNodes)}
271 '';
272
273 system.activationScripts."uucp-logs" = ''
274 touch ${cfg.logFile}
275 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.logFile}
276 chmod 644 ${cfg.logFile}
277 touch ${cfg.statFile}
278 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.statFile}
279 chmod 644 ${cfg.statFile}
280 touch ${cfg.debugFile}
281 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.debugFile}
282 chmod 644 ${cfg.debugFile}
283 '';
284
285 environment.etc."uucp/port" = {
286 text = ''
287 port ssh
288 type stdin
289 protocol ${cfg.incomingProtocols}
290 '' + concatStringsSep "\n" (mapAttrsToList portSpec cfg.remoteNodes);
291 };
292 environment.etc."uucp/sys" = {
293 text = cfg.extraSys + "\n" + concatStringsSep "\n" (mapAttrsToList sysSpec cfg.remoteNodes);
294 };
295
296 security.wrappers = let
297 wrapper = p: {
298 name = p;
299 value = {
300 source = "${pkgs.uucp}/bin/${p}";
301 owner = "root";
302 group = "root";
303 setuid = true;
304 setgid = false;
305 };
306 };
307 in listToAttrs (map wrapper ["uucico" "cu" "uucp" "uuname" "uustat" "uux" "uuxqt"]);
308
309 nixpkgs.overlays = [(self: super: {
310 uucp = super.lib.overrideDerivation super.uucp (oldAttrs: {
311 configureFlags = "--with-newconfigdir=/etc/uucp";
312 patches = [
313 (super.writeText "mailprogram" ''
314 policy.h | 2 +-
315 1 file changed, 1 insertion(+), 1 deletion(-)
316
317 diff --git a/policy.h b/policy.h
318 index 5afe34b..8e92c8b 100644
319 --- a/policy.h
320 +++ b/policy.h
321 @@ -240,7 +240,7 @@
322 the sendmail choice below. Otherwise, select one of the other
323 choices as appropriate. */
324 #if 1
325 -#define MAIL_PROGRAM "/usr/lib/sendmail -t"
326 +#define MAIL_PROGRAM "${config.security.wrapperDir}/sendmail -t"
327 /* #define MAIL_PROGRAM "/usr/sbin/sendmail -t" */
328 #define MAIL_PROGRAM_TO_BODY 1
329 #define MAIL_PROGRAM_SUBJECT_BODY 1
330 '')
331 ];
332 });
333 rmail = super.writeScriptBin "rmail" ''
334 #!${super.stdenv.shell}
335
336 # Dummy UUCP rmail command for postfix/qmail systems
337
338 IFS=" " read junk from junk junk junk junk junk junk junk relay
339
340 case "$from" in
341 *[@!]*) ;;
342 *) from="$from@$relay";;
343 esac
344
345 exec ${config.security.wrapperDir}/sendmail -G -i -f "$from" -- "$@"
346 '';
347 })];
348
349 environment.systemPackages = with pkgs; [
350 uucp
351 ];
352
353 systemd.services."uucico@" = {
354 serviceConfig = {
355 User = "uucp";
356 Type = "oneshot";
357 ExecStart = "${config.security.wrapperDir}/uucico -D -S %i";
358 };
359 };
360
361 systemd.timers."uucico@" = {
362 timerConfig.OnActiveSec = cfg.interval;
363 timerConfig.OnUnitActiveSec = cfg.interval;
364 };
365
366 systemd.targets."multi-user" = {
367 wants = mapAttrsToList (name: node: "uucico@${name}.timer") cfg.remoteNodes;
368 };
369
370 systemd.kill-user.enable = true;
371 systemd.targets."sleep" = {
372 after = [ "kill-user@uucp.service" ];
373 wants = [ "kill-user@uucp.service" ];
374 };
375
376 networking.networkmanager.dispatcherScripts = optional cfg.nmDispatch {
377 type = "basic";
378 source = pkgs.writeScript "callRemotes.sh" ''
379 #!${pkgs.stdenv.shell}
380
381 shopt -s extglob
382
383 case "''${2}" in
384 (?(vpn-)up)
385 ${concatStringsSep "\n " (mapAttrsToList (name: node: "${pkgs.systemd}/bin/systemctl start uucico@${name}.service") cfg.remoteNodes)}
386 ;;
387 esac
388 '';
389 };
390 };
391}
diff --git a/modules/yggdrasil/default.nix b/modules/yggdrasil/default.nix
new file mode 100644
index 00000000..f4100e73
--- /dev/null
+++ b/modules/yggdrasil/default.nix
@@ -0,0 +1,50 @@
1{ config, lib, customUtils, ... }:
2let
3 cfg = config.services.tinc.yggdrasil;
4in {
5 options = {
6 services.tinc.yggdrasil = lib.mkOption {
7 default = {};
8 type = lib.types.submodule {
9 options = {
10 enable = lib.mkEnableOption "Yggdrasil tinc network";
11
12 connect = lib.mkOption {
13 default = true;
14 type = lib.types.bool;
15 description = ''
16 Connect to central server
17 '';
18 };
19 };
20 };
21 };
22 };
23
24 config = lib.mkIf cfg.enable {
25 services.tinc.networks.yggdrasil = {
26 name = config.networking.hostName;
27 hostSettings = customUtils.nixImport { dir = ./hosts; };
28 debugLevel = 2;
29 interfaceType = "tap";
30 settings = {
31 Mode = "switch";
32 PingTimeout = 30;
33 ConnectTo = lib.mkIf cfg.connect "ymir";
34 };
35 };
36
37 sops.secrets = {
38 tinc-yggdrasil-rsa = {
39 key = "rsa";
40 path = "/etc/tinc/yggdrasil/rsa_key.priv";
41 sopsFile = ./hosts + "/${config.services.tinc.networks.yggdrasil.name}/private-keys.yaml";
42 };
43 tinc-yggdrasil-ed25519 = {
44 key = "ed25519";
45 path = "/etc/tinc/yggdrasil/rsa_key.priv";
46 sopsFile = ./hosts + "/${config.services.tinc.networks.yggdrasil.name}/private-keys.yaml";
47 };
48 };
49 };
50}
diff --git a/modules/yggdrasil/hosts/sif/default.nix b/modules/yggdrasil/hosts/sif/default.nix
new file mode 100644
index 00000000..32b844de
--- /dev/null
+++ b/modules/yggdrasil/hosts/sif/default.nix
@@ -0,0 +1,13 @@
1{
2 settings.Ed25519PublicKey = "qJqty+wiTNcYaHQCvQNiMqXYz30C9M3+LI/qjmU/9hK";
3 rsaPublicKey = ''
4 -----BEGIN RSA PUBLIC KEY-----
5 MIIBCgKCAQEA0ACaacg9EN0hBQct8ZwQ/i6EsXKP4DIwKwabM2rp8azValTHU2uI
6 WW6JRY+Eii6zRx9B5kJ96C4rJJeAGV6lZPAogaC2LbM7lcsZ7oRDWZGaQKcZFNGi
7 laEcDg2dRuDx1W4at0rb03SDLNPt8sXSV6BcK9n/7m7+s9cwM/+PB8FHDMnWvwbC
8 usbP23020s+CVr/PU1z/7J0y3Eat+Acut6x5X8DNewpqV96wQpqdAggbhtYERMFH
9 +i0sa1WUDQtJ6HGChbENRTMlsPJ6lnzXY+J0pzatzzvetLsOljES9uJ8dtk6qBC7
10 KRZo5lvdUwR6j9XiHMQeRerUt23b9ATFXQIDAQAB
11 -----END RSA PUBLIC KEY-----
12 '';
13}
diff --git a/modules/yggdrasil/hosts/sif/private-keys.yaml b/modules/yggdrasil/hosts/sif/private-keys.yaml
new file mode 100644
index 00000000..9be82bc1
--- /dev/null
+++ b/modules/yggdrasil/hosts/sif/private-keys.yaml
@@ -0,0 +1,34 @@
1ed25519: ENC[AES256_GCM,data:1CqB4y6CIm5JUsznpXPqqLJqCKmmoAJOZQTWb7+Jbn0oZMX27qSMK4CchHF7Bmo24EK8rk5EyW5aQLnoxp/2NA62p8SXdaoI8Qgz3EgsQ5QrlJrt1jvERpNs4vttT9V6+aK3Yojr9IuQSvJ4jyKSLrzrTnLzF9pXlaOf1Ru5SxySRWtVzynzurRpdUVS6goE+lb+Irg6x2geV719iQ9bu1C2smeQDREdS+dlfoxp02/pU6kTFA7KAm5vA91HKEfMqfSEzuBgUB0=,iv:n6Yh0zZ9AbT+83P42QNO2rCCISJV5nbO9wYcwaRYD2E=,tag:dJpXV9ZzLSO1B+LsyV3vAg==,type:str]
2rsa: ENC[AES256_GCM,data:7faQJAhoYt3MJidg4TVwysmLGZ4V1fA9NYYKgEMgky4q0Q9tBGhEsA60uj7iKcMMRhGku7feIFkj2+1qjKy+e1Bajfs2rqxgyqYmM6yOTrmorbXBVyrPOTOwJp3yp7O1vIXwoUS9vWIYxFszpfaLL0/8aARYVrYmpxf3gsBfQ4LciM1VKEgjG3uRBf1tDLaNuMNyzdan0DFghwuDojPOXUFv/6yuPxU2U0TagVjwAk4FThGwEasvV454RSm/GmqYtX+P4Vc3pEWNYAK1rXJAuXm1392Uash+HGQ+3ln5N9yWneewgPPr0pePAugxxN0qnwhy5MRKGQE3ZHCZ0beslfOm6pkmYTfww3lKNIJGabMfMD3COoAI7zWebUvksZPsgH6f1olbzABkZdS1s//WNMnWQHGxsWePXkLFe8bfnNXouEXHtLvQ7On0KPyt8y5QBI9bDPpTn92/O9jCevXSttrez4buBdCHFmCE8xgW5JKKEXgMubPPjEF3MABiGu0TMeWM4a1ibY7HfvNrRkO1pE9RhdRT/dFV/MrPxk7P0k16x9H4+QnE7VglfNZO3Wd3bnYxcH7hmAbIzpFnUJvolyNfmynwL2WwaYuBskXASD1FuqpM0tbhantqGyHVPe62+KimU0zDAJ1HMyqhIN0MD1MSXsdoItAsw033GYLB83L8xPatARJR9qEdKwrhmgSDY36AbJ8VI/RUzicZoYdhK8+M7bNGIkD5MgrQO35q+3oa6Xcib+5MtW0RVJKLP4y5/XNkjd4EPl6nahcVi63/FG7LJmO+/I7bkLIAWmIq8BHcXEwbz0womYp404pSfEPr3cy1N5S3yqRdzVxavTJb0PLMpHq2rWuHK2DIY77hEOAt0XcReWYsRkmTl+v9iQLF+D4GBLr+O2oZNJrocNVZYkfdjsrUd2cUOCV7ZQphO5Yc+yKrqzmCqUUvdoJ3vlaPxMXx4LACeMImo1sAFxoOgIpyfklo/bdhi9osiL55I8pAIh5hGes/uCbwaRnW+wbaYcMliCuUO8XelfXwBot8W+0l0wk2zKRSKtYKcX1n/Ax5mIt6mIoQkvyL82lccS9ppJLjt7DYlvK8L6imeV11ATf1ZhSGB3c67/XYik5BXz827Rj29K6fg/CvU65f/bEAuE39gSJ4mHsRl3bvkNLiUMEBrDuZnText33fCbqVA5DUIfqSbLUzXtqNl8vHnlOBICYwjv8PtUMJ6VTCDu33SmtQzJAfnmuewOKAC51FPsyaDhouTKllUaqx34NfEP8k2C8/4oNPgDcLjInm3f43tIuJbScdp8ltNVCLoChS8jbBOvrVYTI0eP+BuAuEfWYldUYq96oH/x9d0yvPqZ1rnwmqg4y6GfkACw6+/QvrDdtcM+1uI86RxZ7KGurb8KG7NPdSWhzz+72+TO5Tq29K8QETLzzalnVzaVWj/xGsjgkslxmDMKxLJQw0o24lgg/R30aU9BL6YwDVi10nu+Tv5kayb/NVLdMNWxfKNg1KZcf8M2ApgonjingbpUlinZ25/IIcQB9lMT4HSyvtGtIqnsPL4SQNsgBLcMzdwbL0EvS3qMAEVWKfUm2v9AA2+RMsKEKtD4UNF2xF7oACJiyTcw/xUOmkaTIZZ2ev0JVb4IYs1qx5Skz+IMAvWQ2FjBMXna5e/LYgBl6kdLSTcDvlymHpbjjuRdRq+uq+ZMXIACyZ+qUnZ0qcfWGPxOCI0hXPc5ac/zSGkPKYiWT/rCSuo+MoijjK4YZ2fub9TCYjZRS+QvLlXOM8F06Or0jQQOveezqJFZdoBGj248BtcPAVbYqfaytIlYjARlhQL/lKaaOrbONk6kIlDpwkhlzO50OkhALItlbW4Aa8zZ/WeXkfkb/6A7NLce42XDoOnvZt9UdYVTRphf8yxjRE2YMwZsmeTIieg8KwwJdnoJIhiQFdVDFgXb2xPZA2CbdvZwGwuFkLWgJUg6H+aHdw39UnNM+S9PYaOQ9oaS7IyeWhXMgP7TKM98uILsBg/Xn9tafHaslQfjVRDEaYtrmDZMYhb+h/MZKngx7uwmUyqHszAYN/M+RMJVy3s4uBu/EufWYVMorunpPEXGYA4Rg1HUuAOvWSvpM3PJG9Wnrazw6xmkwIUSKju5irpWATYmqSX3pPkG5C0sTatszVDAvTs9+/9Xdbney7/6QskSHMph8Kn/Udpq7PPrZWADkIi1k4oibgABOXOWBk5ZbNbiDrZA==,iv:ZUAqvOpcVCXQD2PFzUh0e2m20t6gVT3mYb7S50iV/m8=,tag:AssxMqjVUEwQ4R6Y7eG9Tg==,type:str]
3sops:
4 kms: []
5 gcp_kms: []
6 azure_kv: []
7 hc_vault: []
8 lastmodified: '2021-01-02T14:46:16Z'
9 mac: ENC[AES256_GCM,data:Phng7z7UlE6nO3FFIQPOHgKCqDm2uOGL57ryJbokjipSSdoWPinpz0zIJv9Z67b9uOf3CQoGtV4YwcudNkzDBKOyD8uA6RYwCKpbYcZIdiy8DLL46+VT/wq9toTkeDXM6jKupzzOARZhHT8DCOLqW7u8Q3S645cbTJmw0+LMIGk=,iv:y4KEh0+bKhtnSobKVdfaPuRsueNC1lcrEbUGfEAn+Bg=,tag:3Oi4e/hSgPVsoFQpnVQj+g==,type:str]
10 pgp:
11 - created_at: '2021-01-02T14:45:04Z'
12 enc: |
13 -----BEGIN PGP MESSAGE-----
14
15 hF4Dgwm4NZSaLAcSAQdAwWM12Zara3T2xDIX3rhakGxXFyme4LE5QZgE2GjnnWEw
16 T/vhPfsKFCjA2kAmj41NupjvTPL/nzfd7+MrdHRfC462Jrq+UF1W8A4bUa3OMH5J
17 0l4BuFhl93w/VBftvnG8oSBAFCPNDapNADjTVJQStgsZa0/uD93NnCxyQmtuJYsQ
18 URlH0KMT6Kouaec4qk3SqkAHzaIIAukahBHAPf2C5cvXYw7AAOOBOdRaWycsmZDc
19 =S4Ig
20 -----END PGP MESSAGE-----
21 fp: F1AF20B9511B63F681A14E8D51AEFBCD1DEF68F8
22 - created_at: '2021-01-02T14:45:04Z'
23 enc: |
24 -----BEGIN PGP MESSAGE-----
25
26 hF4DXxoViZlp6dISAQdA7apd+ipJ0lUiuPI5Sq6uj6iOQYFfuNDuzse1JFJMfn4w
27 McsGPcbMorZV0OVFmg9vuZ0GP9sb7mkm+oRuY9OeMDEifjWGHJ2UN4TvdEcCO1zx
28 0l4BvYyzFbShlQjge7+nrzVi2lzEvqsozEW76K3arWb/iYLCRyl0/Vhw5WT4K/UE
29 fw4cbqz7JrogVLFNeWSRPk3Y+Dg4Pf9rQnw1EJhUEIczYjnfajPhYe5K4M01mOby
30 =B0n7
31 -----END PGP MESSAGE-----
32 fp: 30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51
33 unencrypted_suffix: _unencrypted
34 version: 3.6.1
diff --git a/modules/yggdrasil/hosts/ymir.nix b/modules/yggdrasil/hosts/ymir.nix
new file mode 100644
index 00000000..b77a9216
--- /dev/null
+++ b/modules/yggdrasil/hosts/ymir.nix
@@ -0,0 +1,19 @@
1{
2 addresses = [{ address = "ymir.yggdrasil.li"; }];
3 settings.Ed25519PublicKey = "b/SobnMqByzHOQeO+iU7OZ1liD8a++knbi5ebNawnaC";
4 rsaPublicKey = ''
5 -----BEGIN RSA PUBLIC KEY-----
6 MIICCgKCAgEAuInSfQf5euFXEVkLLzf9TumQJ+3WRsxX4uKdOXBqrIC7yjSBP8j9
7 ql5rNWPzgXxFF5ERmwW+E3cyzJLU9Htu7r3muqM6nhSZizhCskifPRFc3e5ssSke
8 XhHICHfe90+qvab/hWx/NjkW59bBYIzDuJfq+ijDFMVNgOxaiM2f3/2prUUhP7bN
9 r3wVI8KCkOaknc0SOOmOhLzfJaD5wosqLOjgaNhlro2eMgMjQlxbyW8dVVgjwseR
10 Cl/mpu7r1pSMhS66RFH68wDoC3X81f7Zs9ZGDLTD8KXWhx0qgUMUAH4n6YGY0RM6
11 BZ3qR/3KFRU64QPVAERpb0JdsU9ggCVydHkjrWW23ptHOPAOO5+yQj7tSDCKTRy9
12 dHMQnbtPrgAb6iMhO1XTxA8Hdta1sCHsewsQekarwsA1bmk3hTgi/k8vwoGDUWtk
13 jgiDEPuutfmH4C6qxq9s+6lRboNKH8wgkVGpHiaq7mmePFdhzFdrj4+fYAMZTbil
14 2iygsJ+yFOjA7U+iT6QDK33/MLsrQg0Ue6RPiG1qnDyax7gBAjz52iWkiuSkUXk0
15 E5ImdP4XMILgGcWk8iPq5iRS03edE0pCpxGX3ZZwFE5+CoXgO6wR1ToL1vZEEHMQ
16 SHJPufKjkavPKbejPps/mLaJQVw3W10PAJssB9nxW2aHX3n0ugGaIvMCAwEAAQ==
17 -----END RSA PUBLIC KEY-----
18 '';
19}