{ 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." 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: # /ca-cert # /host-cert # /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 /-cert and # /-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 ]; }