Files
nix-config/modules/nixos/services/nebula/default.nix
2026-04-05 19:10:23 -05:00

149 lines
5.5 KiB
Nix

{
config,
lib,
namespace,
pkgs,
...
}:
with lib;
let
name = "nebula";
cfg = config.${namespace}.services.${name};
ca = config.sops.secrets."${cfg.secretsPrefix}/ca-cert".path;
cert = config.sops.secrets."${cfg.secretsPrefix}/${cfg.hostSecretName}-cert".path;
key = config.sops.secrets."${cfg.secretsPrefix}/${cfg.hostSecretName}-key".path;
nebulaConfig = lib.${namespace}.mkModule {
inherit config name;
description = "nebula overlay network node";
options = {
# -----------------------------------------------------------------------
# Role
# -----------------------------------------------------------------------
isLighthouse = lib.${namespace}.mkBoolOpt false "Act as a Nebula lighthouse";
isRelay = lib.${namespace}.mkBoolOpt false "Act as a Nebula relay node";
# Override the mkModule port default (80) with the nebula default (4242).
port = lib.${namespace}.mkOpt types.port 4242 "UDP port nebula listens on";
# -----------------------------------------------------------------------
# Network identity
# -----------------------------------------------------------------------
networkName =
lib.${namespace}.mkOpt types.str "jallen-nebula"
"Nebula network name (used as the systemd service suffix and interface name)";
# Linux interface names are capped at 15 characters. Nebula derives the
# default tun device name as "nebula.<networkName>" which easily exceeds
# that limit. Set an explicit short name here to avoid silent truncation.
tunDevice =
lib.${namespace}.mkOpt (types.nullOr types.str) null
"Explicit tun device name (max 15 chars). Defaults to nebula0 when unset.";
# -----------------------------------------------------------------------
# SOPS secret location
#
# secretsPrefix — key path prefix inside the SOPS file, e.g. "pi5/nebula"
# The module expects three secrets under this prefix:
# <secretsPrefix>/ca-cert
# <secretsPrefix>/host-cert
# <secretsPrefix>/host-key
#
# secretsFile — path to the SOPS-encrypted YAML that holds the secrets
# -----------------------------------------------------------------------
secretsPrefix = lib.${namespace}.mkOpt types.str "" "SOPS secret key prefix, e.g. \"pi5/nebula\"";
secretsFile =
lib.${namespace}.mkOpt types.str ""
"Path to the SOPS secrets YAML file for this host";
# hostSecretName — the middle segment of the cert/key secret names.
# Secrets are looked up as <secretsPrefix>/<hostSecretName>-cert and
# <secretsPrefix>/<hostSecretName>-key. Defaults to "host"; set to
# e.g. "nas", "lighthouse", or the machine hostname to match existing
# SOPS YAML keys without renaming them.
hostSecretName =
lib.${namespace}.mkOpt types.str "host"
"Secret name segment for cert/key (e.g. \"nas\" looks for nas-cert / nas-key)";
# -----------------------------------------------------------------------
# Peer addressing (ignored on lighthouse nodes)
# -----------------------------------------------------------------------
lighthouses = lib.${namespace}.mkOpt (types.listOf types.str) [
"10.1.1.1"
] "Nebula overlay IPs of lighthouse nodes (leave empty on lighthouses)";
staticHostMap = lib.${namespace}.mkOpt (types.attrsOf (types.listOf types.str)) {
"10.1.1.1" = [ "mjallen.dev:4242" ];
} "Static host map: overlay IP list of public addr:port strings";
# -----------------------------------------------------------------------
# Firewall rules inside the overlay
# -----------------------------------------------------------------------
inboundRules = lib.${namespace}.mkOpt (types.listOf types.attrs) [
{
port = "any";
proto = "any";
host = "any";
}
] "Nebula inbound firewall rules";
outboundRules = lib.${namespace}.mkOpt (types.listOf types.attrs) [
{
port = "any";
proto = "any";
host = "any";
}
] "Nebula outbound firewall rules";
};
moduleConfig = {
environment.systemPackages = with pkgs; [ nebula ];
# Allow users in the wheel group to start/stop the nebula service without
# a password prompt (used by the GNOME panel extension toggle).
security.polkit.extraConfig = ''
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "nebula@${cfg.networkName}.service" &&
(action.lookup("verb") == "start" || action.lookup("verb") == "stop") &&
subject.local == true &&
subject.active == true &&
subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
'';
services.nebula.networks.${cfg.networkName} = {
inherit (cfg)
isLighthouse
isRelay
lighthouses
staticHostMap
;
enable = true;
enableReload = true;
inherit ca cert key;
tun.device = if cfg.tunDevice != null then cfg.tunDevice else "nebula0";
listen = {
host = cfg.listenAddress;
inherit (cfg) port;
};
settings.firewall = {
inbound = cfg.inboundRules;
outbound = cfg.outboundRules;
};
};
};
};
in
{
imports = [
nebulaConfig
./sops.nix
];
}