1 { config, lib, pkgs, ... }:
7 cfg = config.services.nebula;
8 enabledNetworks = filterAttrs (n: v: v.enable) cfg.networks;
10 format = pkgs.formats.yaml {};
12 nameToId = netName: "nebula-${netName}";
14 resolveFinalPort = netCfg:
15 if netCfg.listen.port == null then
16 if (netCfg.isLighthouse || netCfg.isRelay) then
29 description = "Nebula network definitions.";
31 type = types.attrsOf (types.submodule {
36 description = "Enable or disable this network.";
39 package = mkPackageOption pkgs "nebula" { };
43 description = "Path to the certificate authority certificate.";
44 example = "/etc/nebula/ca.crt";
49 description = "Path to the host certificate.";
50 example = "/etc/nebula/host.crt";
54 type = types.oneOf [types.nonEmptyStr types.path];
55 description = "Path or reference to the host key.";
56 example = "/etc/nebula/host.key";
59 staticHostMap = mkOption {
60 type = types.attrsOf (types.listOf (types.str));
63 The static host map defines a set of hosts with fixed IP addresses on the internet (or any network).
64 A host can have multiple fixed IP addresses defined here, and nebula will try each when establishing a tunnel.
66 example = { "192.168.100.1" = [ "100.64.22.11:4242" ]; };
69 isLighthouse = mkOption {
72 description = "Whether this node is a lighthouse.";
78 description = "Whether this node is a relay.";
81 lighthouses = mkOption {
82 type = types.listOf types.str;
85 List of IPs of lighthouse hosts this node should report to and query from. This should be empty on lighthouse
86 nodes. The IPs should be the lighthouse's Nebula IPs, not their external IPs.
88 example = [ "192.168.100.1" ];
92 type = types.listOf types.str;
95 List of IPs of relays that this node should allow traffic from.
97 example = [ "192.168.100.1" ];
100 listen.host = mkOption {
103 description = "IP address to listen on.";
106 listen.port = mkOption {
107 type = types.nullOr types.port;
109 defaultText = lib.literalExpression ''
110 if (config.services.nebula.networks.''${name}.isLighthouse ||
111 config.services.nebula.networks.''${name}.isRelay) then
116 description = "Port number to listen on.";
119 tun.disable = mkOption {
123 When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root).
127 tun.device = mkOption {
128 type = types.nullOr types.str;
130 description = "Name of the tun device. Defaults to nebula.\${networkName}.";
133 firewall.outbound = mkOption {
134 type = types.listOf types.attrs;
136 description = "Firewall rules for outbound traffic.";
137 example = [ { port = "any"; proto = "any"; host = "any"; } ];
140 firewall.inbound = mkOption {
141 type = types.listOf types.attrs;
143 description = "Firewall rules for inbound traffic.";
144 example = [ { port = "any"; proto = "any"; host = "any"; } ];
147 settings = mkOption {
151 Nebula configuration. Refer to
152 <https://github.com/slackhq/nebula/blob/master/examples/config.yml>
153 for details on supported values.
155 example = literalExpression ''
171 config = mkIf (enabledNetworks != {}) {
172 systemd.services = mkMerge (mapAttrsToList (netName: netCfg:
174 networkId = nameToId netName;
175 settings = recursiveUpdate {
181 static_host_map = netCfg.staticHostMap;
183 am_lighthouse = netCfg.isLighthouse;
184 hosts = netCfg.lighthouses;
187 am_relay = netCfg.isRelay;
188 relays = netCfg.relays;
192 host = netCfg.listen.host;
193 port = resolveFinalPort netCfg;
196 disabled = netCfg.tun.disable;
197 dev = if (netCfg.tun.device != null) then netCfg.tun.device else "nebula.${netName}";
200 inbound = netCfg.firewall.inbound;
201 outbound = netCfg.firewall.outbound;
204 configFile = format.generate "nebula-config-${netName}.yml" (
206 ((settings.lighthouse.am_lighthouse || settings.relay.am_relay) && settings.listen.port == 0)
208 Nebula network '${netName}' is configured as a lighthouse or relay, and its port is ${builtins.toString settings.listen.port}.
209 You will likely experience connectivity issues: https://nebula.defined.net/docs/config/listen/#listenport
215 # Create the systemd service for Nebula.
216 "nebula@${netName}" = {
217 description = "Nebula VPN service for ${netName}";
218 wants = [ "basic.target" ];
219 after = [ "basic.target" "network.target" ];
220 before = [ "sshd.service" ];
221 wantedBy = [ "multi-user.target" ];
225 ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
227 CapabilityBoundingSet = "CAP_NET_ADMIN";
228 AmbientCapabilities = "CAP_NET_ADMIN";
229 LockPersonality = true;
230 NoNewPrivileges = true;
231 PrivateDevices = false; # needs access to /dev/net/tun (below)
232 DeviceAllow = "/dev/net/tun rw";
233 DevicePolicy = "closed";
235 PrivateUsers = false; # CapabilityBoundingSet needs to apply to the host namespace
237 ProtectControlGroups = true;
239 ProtectHostname = true;
240 ProtectKernelLogs = true;
241 ProtectKernelModules = true;
242 ProtectKernelTunables = true;
243 ProtectProc = "invisible";
244 ProtectSystem = true;
245 RestrictNamespaces = true;
246 RestrictSUIDSGID = true;
250 unitConfig.StartLimitIntervalSec = 0; # ensure Restart=always is always honoured (networks can go down for arbitrarily long)
254 # Open the chosen ports for UDP.
255 networking.firewall.allowedUDPPorts =
256 unique (filter (port: port > 0) (mapAttrsToList (netName: netCfg: resolveFinalPort netCfg) enabledNetworks));
258 # Create the service users and groups.
259 users.users = mkMerge (mapAttrsToList (netName: netCfg:
261 ${nameToId netName} = {
262 group = nameToId netName;
263 description = "Nebula service user for network ${netName}";
268 users.groups = mkMerge (mapAttrsToList (netName: netCfg: {
269 ${nameToId netName} = {};
273 meta.maintainers = with maintainers; [ numinit ];