1{ pkgs, ... }:
2let
3 deviceName = "rp0";
4
5 server = {
6 ip = "fe80::1";
7 wg = {
8 public = "mQufmDFeQQuU/fIaB2hHgluhjjm1ypK4hJr1cW3WqAw=";
9 secret = "4N5Y1dldqrpsbaEiY8O0XBUGUFf8vkvtBtm8AoOX7Eo=";
10 listen = 10000;
11 };
12 };
13 client = {
14 ip = "fe80::2";
15 wg = {
16 public = "Mb3GOlT7oS+F3JntVKiaD7SpHxLxNdtEmWz/9FMnRFU=";
17 secret = "uC5dfGMv7Oxf5UDfdPkj6rZiRZT2dRWp5x8IQxrNcUE=";
18 };
19 };
20in
21{
22 name = "rosenpass";
23
24 nodes =
25 let
26 shared =
27 peer:
28 { config, modulesPath, ... }:
29 {
30 imports = [ "${modulesPath}/services/networking/rosenpass.nix" ];
31
32 boot.kernelModules = [ "wireguard" ];
33
34 services.rosenpass = {
35 enable = true;
36 defaultDevice = deviceName;
37 settings = {
38 verbosity = "Verbose";
39 public_key = "/etc/rosenpass/pqpk";
40 secret_key = "/etc/rosenpass/pqsk";
41 };
42 };
43
44 networking.firewall.allowedUDPPorts = [ 9999 ];
45
46 systemd.network = {
47 enable = true;
48 networks."rosenpass" = {
49 matchConfig.Name = deviceName;
50 networkConfig.IPv4Forwarding = true;
51 networkConfig.IPv6Forwarding = true;
52 address = [ "${peer.ip}/64" ];
53 };
54
55 netdevs."10-rp0" = {
56 netdevConfig = {
57 Kind = "wireguard";
58 Name = deviceName;
59 };
60 wireguardConfig.PrivateKeyFile = "/etc/wireguard/wgsk";
61 };
62 };
63
64 environment.etc."wireguard/wgsk" = {
65 text = peer.wg.secret;
66 user = "systemd-network";
67 group = "systemd-network";
68 };
69 };
70 in
71 {
72 server = {
73 imports = [ (shared server) ];
74
75 networking.firewall.allowedUDPPorts = [ server.wg.listen ];
76
77 systemd.network.netdevs."10-${deviceName}" = {
78 wireguardConfig.ListenPort = server.wg.listen;
79 wireguardPeers = [
80 {
81 AllowedIPs = [ "::/0" ];
82 PublicKey = client.wg.public;
83 }
84 ];
85 };
86
87 services.rosenpass.settings = {
88 listen = [ "0.0.0.0:9999" ];
89 peers = [
90 {
91 public_key = "/etc/rosenpass/peers/client/pqpk";
92 peer = client.wg.public;
93 }
94 ];
95 };
96 };
97 client = {
98 imports = [ (shared client) ];
99
100 systemd.network.netdevs."10-${deviceName}".wireguardPeers = [
101 {
102 AllowedIPs = [ "::/0" ];
103 PublicKey = server.wg.public;
104 Endpoint = "server:${builtins.toString server.wg.listen}";
105 }
106 ];
107
108 services.rosenpass.settings.peers = [
109 {
110 public_key = "/etc/rosenpass/peers/server/pqpk";
111 endpoint = "server:9999";
112 peer = server.wg.public;
113 }
114 ];
115 };
116 };
117
118 testScript =
119 { ... }:
120 ''
121 from os import system
122
123 # Full path to rosenpass in the store, to avoid fiddling with `$PATH`.
124 rosenpass = "${pkgs.rosenpass}/bin/rosenpass"
125
126 # Path in `/etc` where keys will be placed.
127 etc = "/etc/rosenpass"
128
129 start_all()
130
131 for machine in [server, client]:
132 machine.wait_for_unit("multi-user.target")
133
134 # Gently stop Rosenpass to avoid crashes during key generation/distribution.
135 for machine in [server, client]:
136 machine.execute("systemctl stop rosenpass.service")
137
138 for (name, machine, remote) in [("server", server, client), ("client", client, server)]:
139 pk, sk = f"{name}.pqpk", f"{name}.pqsk"
140 system(f"{rosenpass} gen-keys --force --secret-key {sk} --public-key {pk}")
141 machine.copy_from_host(sk, f"{etc}/pqsk")
142 machine.copy_from_host(pk, f"{etc}/pqpk")
143 remote.copy_from_host(pk, f"{etc}/peers/{name}/pqpk")
144
145 for machine in [server, client]:
146 machine.execute("systemctl start rosenpass.service")
147
148 for machine in [server, client]:
149 machine.wait_for_unit("rosenpass.service")
150
151 with subtest("ping"):
152 client.succeed("ping -c 2 -i 0.5 ${server.ip}%${deviceName}")
153
154 with subtest("preshared-keys"):
155 # Rosenpass works by setting the WireGuard preshared key at regular intervals.
156 # Thus, if it is not active, then no key will be set, and the output of `wg show` will contain "none".
157 # Otherwise, if it is active, then the key will be set and "none" will not be found in the output of `wg show`.
158 for machine in [server, client]:
159 machine.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5)
160 '';
161
162 # NOTE: Below configuration is for "interactive" (=developing/debugging) only.
163 interactive.nodes =
164 let
165 inherit (import ./ssh-keys.nix pkgs) snakeOilPublicKey snakeOilPrivateKey;
166
167 sshAndKeyGeneration = {
168 services.openssh.enable = true;
169 users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
170 environment.systemPackages = [
171 (pkgs.writeShellApplication {
172 name = "gen-keys";
173 runtimeInputs = [ pkgs.rosenpass ];
174 text = ''
175 HOST="$(hostname)"
176 if [ "$HOST" == "server" ]
177 then
178 PEER="client"
179 else
180 PEER="server"
181 fi
182
183 # Generate keypair.
184 mkdir -vp /etc/rosenpass/peers/$PEER
185 rosenpass gen-keys --force --secret-key /etc/rosenpass/pqsk --public-key /etc/rosenpass/pqpk
186
187 # Set up SSH key.
188 mkdir -p /root/.ssh
189 cp ${snakeOilPrivateKey} /root/.ssh/id_ecdsa
190 chmod 0400 /root/.ssh/id_ecdsa
191
192 # Copy public key to other peer.
193 # shellcheck disable=SC2029
194 ssh -o StrictHostKeyChecking=no $PEER "mkdir -pv /etc/rosenpass/peers/$HOST"
195 scp /etc/rosenpass/pqpk "$PEER:/etc/rosenpass/peers/$HOST/pqpk"
196 '';
197 })
198 ];
199 };
200
201 # Use kmscon <https://www.freedesktop.org/wiki/Software/kmscon/>
202 # to provide a slightly nicer console, and while we're at it,
203 # also use a nice font.
204 # With kmscon, we can for example zoom in/out using [Ctrl] + [+]
205 # and [Ctrl] + [-]
206 niceConsoleAndAutologin.services.kmscon = {
207 enable = true;
208 autologinUser = "root";
209 fonts = [
210 {
211 name = "Fira Code";
212 package = pkgs.fira-code;
213 }
214 ];
215 };
216 in
217 {
218 server = sshAndKeyGeneration // niceConsoleAndAutologin;
219 client = sshAndKeyGeneration // niceConsoleAndAutologin;
220 };
221}