{ config, lib, pkgs, ... }: let serviceName = "netbird"; servicePort = config.m3ta.ports.get "netbird"; domain = "v.m3ta.dev"; proxyDomain = "p.m3ta.dev"; ipBase = "10.89.0"; ipOffset = 50; # Database configuration dbName = "netbird"; dbUser = "netbird"; dbHost = "${ipBase}.1"; # NetBird config als Nix attribute set netbirdConfig = { server = { listenAddress = ":80"; exposedAddress = "https://${domain}:443"; stunPorts = [3478]; metricsPort = 9090; healthcheckAddress = ":9000"; logLevel = "info"; logFile = "console"; dataDir = "/var/lib/netbird"; auth = { issuer = "https://${domain}/oauth2"; localAuthDisabled = true; signKeyRefreshEnabled = true; dashboardRedirectURIs = [ "https://${domain}/nb-auth" "https://${domain}/nb-silent-auth" ]; cliRedirectURIs = ["http://localhost:53000/"]; }; reverseProxy = { trustedHTTPProxies = ["${ipBase}.1/32"]; }; # Proxy Feature proxy = { enabled = true; domain = proxyDomain; }; store = { engine = "postgres"; postgres = { host = dbHost; port = 5432; database = dbName; username = dbUser; }; }; }; }; # YAML generieren yamlFormat = pkgs.formats.yaml {}; configYamlBase = yamlFormat.generate "netbird-config-base.yaml" netbirdConfig; # Script das Secrets zur Runtime injiziert configGenScript = pkgs.writeShellScript "netbird-gen-config" '' set -euo pipefail AUTH_SECRET=$(cat "$1") DB_PASSWORD=$(cat "$2") ENCRYPTION_KEY=$(cat "$3") ${pkgs.yq-go}/bin/yq eval " .server.authSecret = \"$AUTH_SECRET\" | .server.store.encryptionKey = \"$ENCRYPTION_KEY\" | .server.store.postgres.password = \"$DB_PASSWORD\" " ${configYamlBase} ''; in { age.secrets."${serviceName}-auth-secret".file = ../../../../secrets/${serviceName}-auth-secret.age; age.secrets."${serviceName}-db-password".file = ../../../../secrets/${serviceName}-db-password.age; age.secrets."${serviceName}-encryption-key".file = ../../../../secrets/${serviceName}-encryption-key.age; age.secrets."${serviceName}-dashboard-env".file = ../../../../secrets/${serviceName}-dashboard-env.age; age.secrets."${serviceName}-server-env".file = ../../../../secrets/${serviceName}-server-env.age; age.secrets."${serviceName}-proxy-env".file = ../../../../secrets/${serviceName}-proxy-env.age; # Systemd oneshot Service der die Config generiert systemd.services."${serviceName}-config" = { description = "Generate NetBird config with secrets"; wantedBy = ["multi-user.target"]; before = ["podman-${serviceName}-server.service"]; requiredBy = ["podman-${serviceName}-server.service"]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = pkgs.writeShellScript "netbird-write-config" '' mkdir -p /var/lib/${serviceName} ${configGenScript} \ ${config.age.secrets."${serviceName}-auth-secret".path} \ ${config.age.secrets."${serviceName}-db-password".path} \ ${config.age.secrets."${serviceName}-encryption-key".path} \ > /var/lib/${serviceName}/config.yaml chmod 600 /var/lib/${serviceName}/config.yaml ''; }; }; virtualisation.oci-containers.containers = { "${serviceName}-dashboard" = { image = "netbirdio/dashboard:latest"; autoStart = true; environmentFiles = [config.age.secrets."${serviceName}-dashboard-env".path]; extraOptions = [ "--ip=${ipBase}.${toString ipOffset}" "--network=web" ]; }; "${serviceName}-server" = { image = "netbirdio/netbird-server:latest"; autoStart = true; ports = ["3478:3478/udp"]; environmentFiles = [config.age.secrets."${serviceName}-server-env".path]; volumes = [ "${serviceName}_data:/var/lib/netbird" "/var/lib/${serviceName}/config.yaml:/etc/netbird/config.yaml:ro" ]; cmd = ["--config" "/etc/netbird/config.yaml"]; extraOptions = [ "--ip=${ipBase}.${toString (ipOffset + 1)}" "--network=web" ]; }; "${serviceName}-proxy" = { image = "netbirdio/reverse-proxy:latest"; autoStart = true; ports = ["51820:51820/udp"]; volumes = [ "${serviceName}_proxy_certs:/certs" ]; environmentFiles = [config.age.secrets."${serviceName}-proxy-env".path]; cmd = [ "--domain=p.m3ta.dev" "--mgmt=https://${domain}:443" "--addr=:8443" "--cert-dir=/certs" "--acme-certs" "--trusted-proxies=${ipBase}.1/32" ]; dependsOn = ["${serviceName}-server"]; extraOptions = [ "--ip=${ipBase}.${toString (ipOffset + 2)}" "--network=web" ]; }; }; services.traefik.dynamicConfigOptions = { # HTTP Services und Routers http = { services = { "${serviceName}-dashboard".loadBalancer.servers = [ {url = "http://${ipBase}.${toString ipOffset}:80/";} ]; "${serviceName}-server".loadBalancer.servers = [ {url = "http://${ipBase}.${toString (ipOffset + 1)}:80/";} ]; "${serviceName}-server-h2c".loadBalancer.servers = [ {url = "h2c://${ipBase}.${toString (ipOffset + 1)}:80";} ]; }; routers = { # gRPC (Signal + Management) "${serviceName}-grpc" = { rule = "Host(`${domain}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"; entrypoints = "websecure"; tls.certResolver = "godaddy"; service = "${serviceName}-server-h2c"; priority = 100; }; # Backend (relay, WebSocket, API, OAuth2) "${serviceName}-backend" = { rule = "Host(`${domain}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"; entrypoints = "websecure"; tls.certResolver = "godaddy"; service = "${serviceName}-server"; priority = 100; }; # Dashboard (catch-all, niedrigste Priorität) "${serviceName}-dashboard" = { rule = "Host(`${domain}`)"; entrypoints = "websecure"; tls.certResolver = "godaddy"; service = "${serviceName}-dashboard"; priority = 1; }; }; }; # TCP für Proxy TLS Passthrough tcp = { services."${serviceName}-proxy-tls".loadBalancer.servers = [ {address = "${ipBase}.${toString (ipOffset + 2)}:8443";} ]; routers."${serviceName}-proxy-passthrough" = { entryPoints = ["websecure"]; rule = "HostSNI(`*`)"; service = "${serviceName}-proxy-tls"; priority = 1; tls.passthrough = true; }; }; # ServersTransport für Proxy Protocol v2 (optional) serversTransports."pp-v2" = { proxyProtocol.version = 2; }; }; networking.firewall.allowedUDPPorts = [ 3478 # STUN 51820 # WireGuard für Proxy ]; }