1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
|
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkIf mkOption;
inherit (lib.types)
nullOr
path
bool
listOf
str
submodule
;
cfg = config.services.yggdrasil;
# Paths for persistent keys
stateDir = "/var/lib/yggdrasil";
persistentKeyPath = "${stateDir}/private.pem";
legacyKeysPath = "${stateDir}/keys.json";
# Determine which key path to use:
# 1. If PrivateKeyPath is explicitly set, use that
# 2. If persistentKeys is enabled, use the auto-generated key path
effectiveKeyPath =
if cfg.settings.PrivateKeyPath != null then
cfg.settings.PrivateKeyPath
else if cfg.persistentKeys then
persistentKeyPath
else
null;
baseSettings =
cfg.settings
// lib.optionalAttrs (effectiveKeyPath != null) {
PrivateKeyPath = "/run/credentials/yggdrasil.service/private-key";
};
# Remove null values that yggdrasil doesn't expect
cleanSettings = lib.filterAttrs (n: v: v != null) baseSettings;
# Generate configuration file from user settings
configFile = pkgs.writers.writeJSON "yggdrasil.conf" cleanSettings;
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "yggdrasil" "config" ]
[ "services" "yggdrasil" "settings" ]
)
];
options = {
services.yggdrasil = {
enable = lib.mkEnableOption "the yggdrasil system service";
settings = mkOption {
type = submodule {
freeformType = (pkgs.formats.json { }).type;
options = {
PrivateKeyPath = mkOption {
type = nullOr path;
default = null;
example = "/run/secrets/yggdrasil-private-key";
description = ''
Path to the private key file on the host system.
When specified, the key will be loaded via systemd credentials
for secure access by the yggdrasil service.
Warning: Do not put private keys directly in the Nix store
as they would be world-readable!
'';
};
Peers = mkOption {
type = listOf str;
default = [ ];
example = [
"tcp://aa.bb.cc.dd:eeeee"
"tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff"
];
description = ''
List of outbound peer connection strings.
Connection strings can contain options, see the yggdrasil documentation.
'';
};
Listen = mkOption {
type = listOf str;
default = [ ];
example = [
"tcp://0.0.0.0:xxxxx"
"tls://[::]:yyyyy"
];
description = ''
Listen addresses for incoming connections.
You need listeners to accept incoming peerings from non-local nodes.
'';
};
AllowedPublicKeys = mkOption {
type = listOf str;
default = [ ];
description = ''
List of peer public keys to allow incoming peering connections from.
If left empty, all connections are allowed by default.
'';
};
};
};
default = { };
example = {
Peers = [
"tcp://aa.bb.cc.dd:eeeee"
"tcp://[aaaa:bbbb:cccc:dddd::eeee]:fffff"
];
Listen = [
"tcp://0.0.0.0:xxxxx"
];
PrivateKeyPath = "/run/secrets/yggdrasil-key";
IfName = "ygg0";
IfMTU = 65535;
};
description = ''
Configuration for yggdrasil, as a structured Nix attribute set.
If you specify settings here, they will be used as persistent
configuration and Yggdrasil will retain the same configuration
(including IPv6 address if keys are provided) across restarts.
If no settings are specified, ephemeral keys are generated
and the Yggdrasil interface will have a random IPv6 address
each time the service is started.
Use {option}`settings.PrivateKeyPath` to securely load private
keys from files owned by root via systemd credentials.
The most important options have dedicated NixOS options above.
You can also specify any other yggdrasil configuration option directly.
For a complete list of available options, see:
https://yggdrasil-network.github.io/configurationref.html
You can use the command `nix-shell -p yggdrasil --run "yggdrasil -genconf"`
to generate default configuration values with documentation.
'';
};
group = mkOption {
type = nullOr str;
default = null;
example = "wheel";
description = "Group to grant access to the Yggdrasil control socket. If `null`, only root can access the socket.";
};
openMulticastPort = mkOption {
type = bool;
default = false;
description = ''
Whether to open the UDP port used for multicast peer discovery. The
NixOS firewall blocks link-local communication, so in order to make
incoming local peering work you will also need to configure
`MulticastInterfaces` in your Yggdrasil configuration
({option}`settings`). You will then have to
add the ports that you configure there to your firewall configuration
({option}`networking.firewall.allowedTCPPorts` or
{option}`networking.firewall.interfaces.<name>.allowedTCPPorts`).
'';
};
denyDhcpcdInterfaces = mkOption {
type = listOf str;
default = [ ];
example = [ "tap*" ];
description = ''
Disable the DHCP client for any interface whose name matches
any of the shell glob patterns in this list. Use this
option to prevent the DHCP client from broadcasting requests
on the yggdrasil network. It is only necessary to do so
when yggdrasil is running in TAP mode, because TUN
interfaces do not support broadcasting.
'';
};
package = lib.mkPackageOption pkgs "yggdrasil" { };
persistentKeys = lib.mkEnableOption ''
automatic generation and persistence of keys.
If enabled, a private key will be generated on first startup and stored
at ${persistentKeyPath}. This ensures the Yggdrasil node retains the same
IPv6 address across reboots.
If you have existing keys from a previous installation (in the old
keys.json format at ${legacyKeysPath}), they will be automatically
migrated to the new PEM format on first startup.
Note: This option is mutually exclusive with {option}`settings.PrivateKeyPath`.
If you want to use externally managed keys, use {option}`settings.PrivateKeyPath`
instead
'';
extraArgs = mkOption {
type = listOf str;
default = [ ];
example = [
"-loglevel"
"info"
];
description = "Extra command line arguments.";
};
};
};
config = mkIf cfg.enable (
let
binYggdrasil = "${cfg.package}/bin/yggdrasil";
in
{
assertions = [
{
assertion = config.networking.enableIPv6;
message = "networking.enableIPv6 must be true for yggdrasil to work";
}
{
assertion = !(cfg.settings ? PrivateKey);
message = ''
services.yggdrasil.settings.PrivateKey is not supported because it
would be stored in the world-readable Nix store.
Use services.yggdrasil.settings.PrivateKeyPath instead to securely load the private key from a file.
'';
}
{
assertion = !(cfg.persistentKeys && cfg.settings.PrivateKeyPath != null);
message = ''
services.yggdrasil.persistentKeys and services.yggdrasil.settings.PrivateKeyPath
are mutually exclusive. Use only one of them.
'';
}
];
# One-shot service to generate or migrate persistent keys
systemd.services.yggdrasil-persistent-keys = lib.mkIf cfg.persistentKeys {
description = "Generate or migrate Yggdrasil persistent keys";
wantedBy = [ "multi-user.target" ];
before = [ "yggdrasil.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [
cfg.package
pkgs.jq
];
script = ''
set -euo pipefail
# Create state directory with secure permissions
mkdir -p ${stateDir}
chmod 700 ${stateDir}
# If new format key already exists, nothing to do
if [ -f ${persistentKeyPath} ]; then
echo "Persistent key already exists at ${persistentKeyPath}"
exit 0
fi
# Check for legacy keys.json and migrate if found
if [ -f ${legacyKeysPath} ]; then
echo "Found legacy keys at ${legacyKeysPath}, migrating to PEM format..."
# Extract the PrivateKey from the legacy JSON format
PRIVATE_KEY_HEX=$(jq -r '.PrivateKey' ${legacyKeysPath})
if [ -n "$PRIVATE_KEY_HEX" ] && [ "$PRIVATE_KEY_HEX" != "null" ]; then
# Use yggdrasil's built-in -exportkey flag to convert to PEM format
# Create a minimal config with just the private key
echo "{\"PrivateKey\": \"$PRIVATE_KEY_HEX\"}" | yggdrasil -useconf -exportkey > ${persistentKeyPath}
chmod 600 ${persistentKeyPath}
echo "Successfully migrated legacy keys to ${persistentKeyPath}"
echo "You may remove the legacy file ${legacyKeysPath} after verifying the migration"
exit 0
fi
fi
# No existing keys found, generate new ones using yggdrasil
echo "Generating new persistent key at ${persistentKeyPath}..."
yggdrasil -genconf | yggdrasil -useconf -exportkey > ${persistentKeyPath}
chmod 600 ${persistentKeyPath}
echo "Successfully generated new persistent key"
'';
};
systemd.services.yggdrasil = {
description = "Yggdrasil Network Service";
after = [
"network-pre.target"
]
++ lib.optional cfg.persistentKeys "yggdrasil-persistent-keys.service";
wants = [ "network.target" ];
before = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
script =
if cfg.settings != { } || cfg.persistentKeys then
# Use user settings or persistent keys configuration
"exec ${binYggdrasil} -useconffile ${configFile} ${lib.strings.escapeShellArgs cfg.extraArgs}"
else
# Generate and use ephemeral config
"exec ${binYggdrasil} -genconf | ${binYggdrasil} -useconf ${lib.strings.escapeShellArgs cfg.extraArgs}";
serviceConfig = {
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Restart = "always";
DynamicUser = true;
StateDirectory = "yggdrasil";
RuntimeDirectory = "yggdrasil";
RuntimeDirectoryMode = "0750";
LoadCredential = lib.optional (effectiveKeyPath != null) "private-key:${effectiveKeyPath}";
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
MemoryDenyWriteExecute = true;
ProtectControlGroups = true;
ProtectHome = "tmpfs";
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @keyring"
];
}
// (
if (cfg.group != null) then
{
Group = cfg.group;
}
else
{ }
);
};
networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
# Make yggdrasilctl available on the command line.
environment.systemPackages = [ cfg.package ];
}
);
meta = {
doc = ./yggdrasil.md;
maintainers = with lib.maintainers; [
gazally
nagy
pinpox
];
};
}
|