summaryrefslogtreecommitdiffstats
path: root/hosts/poweredge
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/poweredge')
-rw-r--r--hosts/poweredge/bitcoind.nix42
-rw-r--r--hosts/poweredge/configuration.nix48
-rw-r--r--hosts/poweredge/ddns-updater.nix22
-rw-r--r--hosts/poweredge/filebrowser.nix36
-rw-r--r--hosts/poweredge/fileshares.nix68
-rw-r--r--hosts/poweredge/gitea.nix41
-rw-r--r--hosts/poweredge/hardware-configuration.nix50
-rw-r--r--hosts/poweredge/immich.nix89
-rw-r--r--hosts/poweredge/jellyfin.nix91
-rw-r--r--hosts/poweredge/metrics.nix43
-rw-r--r--hosts/poweredge/navidrome.nix51
-rw-r--r--hosts/poweredge/networking.nix167
-rw-r--r--hosts/poweredge/notification-mailer.nix27
-rw-r--r--hosts/poweredge/nvidia.nix29
-rw-r--r--hosts/poweredge/resources/secrets/ddns-updater-config.yaml16
-rw-r--r--hosts/poweredge/resources/secrets/grafana.yaml16
-rw-r--r--hosts/poweredge/resources/secrets/mailer-pass.yaml16
-rw-r--r--hosts/poweredge/resources/secrets/router.yaml17
-rw-r--r--hosts/poweredge/resources/secrets/slskd.yaml16
-rw-r--r--hosts/poweredge/resources/secrets/vpn-router.yaml17
-rw-r--r--hosts/poweredge/router-hosts.nix48
-rw-r--r--hosts/poweredge/slskd.nix91
-rw-r--r--hosts/poweredge/transmission.nix75
-rw-r--r--hosts/poweredge/vpn-router.nix121
24 files changed, 1237 insertions, 0 deletions
diff --git a/hosts/poweredge/bitcoind.nix b/hosts/poweredge/bitcoind.nix
new file mode 100644
index 0000000..17a91f2
--- /dev/null
+++ b/hosts/poweredge/bitcoind.nix
@@ -0,0 +1,42 @@
+{
+ containers.bitcoind = {
+ autoStart = true;
+ ephemeral = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:00:04";
+
+ # Host path
+ bindMounts = {
+ "/var/lib/bitcoind-main" = {
+ hostPath = "/media/ingens/bitcoin";
+ isReadOnly = false;
+ };
+ };
+
+ config = { lib, pkgs, config, ... }: let
+ p2pPort = 8333;
+ rpcPort = 8332;
+ in {
+ # Network
+ networking.enableIPv6 = false;
+ networking.interfaces.eth0.useDHCP = true;
+ networking.firewall.allowedTCPPorts = [ p2pPort rpcPort ];
+
+ # Bitcoin
+ services.bitcoind."main" = {
+ enable = true;
+ port = p2pPort;
+ rpc.port = rpcPort;
+ rpc.users.timmy.passwordHMAC = "7557196eec6e3e062a57b0720abff7cf$617211e94b964f548cfbb5c23590f690c2940e424fffe72485276493f1cb0b29";
+ extraConfig = ''
+ rpcallowip=192.168.1.0/24
+ rpcbind=0.0.0.0
+ upnp=0
+ '';
+ };
+
+ system.stateVersion = "25.11";
+ };
+ };
+}
diff --git a/hosts/poweredge/configuration.nix b/hosts/poweredge/configuration.nix
new file mode 100644
index 0000000..ba93a37
--- /dev/null
+++ b/hosts/poweredge/configuration.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, ... }: {
+ imports = [
+ ./bitcoind.nix
+ ./ddns-updater.nix
+ ./filebrowser.nix
+ ./fileshares.nix
+ ./gitea.nix
+ ./immich.nix
+ ./jellyfin.nix
+ ./metrics.nix
+ ./navidrome.nix
+ ./networking.nix
+ ./nvidia.nix
+ ./transmission.nix
+ ./vpn-router.nix
+ ./slskd.nix
+ #./notification-mailer.nix # TODO move some of this stuff to archetype
+ ];
+
+ # Setup bootloader
+ boot._loader.enable = true;
+
+ # Enable common options
+ _archetypes = {
+ profiles = {
+ headless = {
+ enable = true;
+ home.users.timmy.enable = true;
+ };
+ zfs.enable = true;
+ cuda.enable = true;
+ };
+ collections = {
+ music.beets.enable = true;
+ };
+ };
+
+ # Import zfs pools
+ boot.zfs.extraPools = [ "ingens" "memoria" ];
+
+ # Enable user timmy
+ _users.timmy.enable = true;
+
+ # Without this, "ZFS requires networking.hostId to be set" will be raised
+ networking.hostId = "4d9e002f";
+
+ system.stateVersion = "25.11";
+}
diff --git a/hosts/poweredge/ddns-updater.nix b/hosts/poweredge/ddns-updater.nix
new file mode 100644
index 0000000..319e3c4
--- /dev/null
+++ b/hosts/poweredge/ddns-updater.nix
@@ -0,0 +1,22 @@
+{ config, lib, ... }: let
+ credential = "config";
+in {
+ # Config for ddns-updater, owned by the ddns-updater systemd service user
+ sops.secrets.ddns-updater-config.sopsFile = ./resources/secrets/ddns-updater-config.yaml;
+
+ # Load secret as a credential in systemd service
+ systemd.services.ddns-updater.serviceConfig = {
+ LoadCredential = [
+ "${credential}:${config.sops.secrets.ddns-updater-config.path}"
+ ];
+ };
+
+ # Enable ddns updater
+ services.ddns-updater = {
+ enable = true;
+ environment = {
+ SERVER_ENABLED="no";
+ CONFIG_FILEPATH = "%d/${credential}";
+ };
+ };
+}
diff --git a/hosts/poweredge/filebrowser.nix b/hosts/poweredge/filebrowser.nix
new file mode 100644
index 0000000..681a6a6
--- /dev/null
+++ b/hosts/poweredge/filebrowser.nix
@@ -0,0 +1,36 @@
+{
+ containers.filebrowser = {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:00:05";
+
+ # Host path
+ bindMounts = {
+ "/var/lib/filebrowser/data" = {
+ hostPath = "/media";
+ isReadOnly = true;
+ };
+ };
+
+ config = { lib, pkg, config, ... }: {
+ # Network
+ networking.interfaces.eth0.useDHCP = true;
+ networking.firewall.allowedTCPPorts = [ 80 8080 ]; # Caddy + filebrowser
+
+ # Filebrowser
+ services.filebrowser.enable = true;
+ services.filebrowser.settings.address = "0.0.0.0";
+
+ # Reverse proxy
+ services.caddy = {
+ enable = true;
+ virtualHosts.":80".extraConfig = ''
+ reverse_proxy localhost:8080
+ '';
+ };
+
+ system.stateVersion = "26.05";
+ };
+ };
+}
diff --git a/hosts/poweredge/fileshares.nix b/hosts/poweredge/fileshares.nix
new file mode 100644
index 0000000..faf0f83
--- /dev/null
+++ b/hosts/poweredge/fileshares.nix
@@ -0,0 +1,68 @@
+{
+ # Configure services
+ services._fileShares = {
+ enable = true;
+ smb.openFirewall = true;
+ nfs.openFirewall = true;
+ };
+ # Shares
+ services._fileShares.shares = {
+ WinBackups = {
+ path = "/media/ingens/backups/windows";
+ smb.enable = true;
+ };
+ pictures = {
+ path = "/media/ingens/pictures";
+ nfs.enable = true;
+ };
+ tapes = {
+ path = "/media/ingens/tapes";
+ nfs.enable = true;
+ smb.enable = true;
+ };
+ backups = {
+ path = "/media/ingens/backups";
+ nfs.enable = true;
+ };
+ games = {
+ path = "/media/ingens/games";
+ nfs.enable = true;
+ };
+ documents = {
+ path = "/media/ingens/documents";
+ nfs.enable = true;
+ smb.enable = true;
+ };
+ };
+
+ # retro shares
+ containers.retronas = {
+ autoStart = true;
+ bindMounts = {
+ "/media/ingens/games" = {
+ hostPath = "/media/ingens/games";
+ isReadOnly = false;
+ };
+ };
+
+ config = { config, pkgs, ... }: {
+ imports = [ ../../nixos/services/fileshares.nix ]; # Get fileshares module
+
+ services._fileShares = {
+ enable = true;
+ smb = {
+ port = 1445;
+ enableLegacyNT1 = true; # purpose of this container
+ openFirewall = true;
+ };
+ };
+ services._fileShares.shares = {
+ PS2 = {
+ path = "/media/ingens/games/ps2";
+ smb.enable = true;
+ smb.allowGuests = true;
+ };
+ };
+ };
+ };
+}
diff --git a/hosts/poweredge/gitea.nix b/hosts/poweredge/gitea.nix
new file mode 100644
index 0000000..4c800df
--- /dev/null
+++ b/hosts/poweredge/gitea.nix
@@ -0,0 +1,41 @@
+{
+ containers.gitea = {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:00:03";
+
+ # TODO make a mountpoint for git repositories
+
+ config = { lib, pkgs, config, ... }: {
+ # Network
+ networking.interfaces.eth0.useDHCP = true;
+ networking.firewall.allowedTCPPorts = [ 80 22 ]; # Caddy + ssh
+
+ # Gitea
+ services.gitea = {
+ enable = true;
+ #user = "git"; # So ssh cloning uses git@gitea
+ settings = {
+ repository = {
+ ENABLE_PUSH_CREATE_USER = true;
+ ENABLE_PUSH_CREATE_ORG = true;
+ };
+ };
+ };
+
+ # SSH for repositories
+ services.openssh.enable = true;
+
+ # Reverse proxy
+ services.caddy = {
+ enable = true;
+ virtualHosts.":80".extraConfig = ''
+ reverse_proxy localhost:3000
+ '';
+ };
+
+ system.stateVersion = "25.11";
+ };
+ };
+}
diff --git a/hosts/poweredge/hardware-configuration.nix b/hosts/poweredge/hardware-configuration.nix
new file mode 100644
index 0000000..0fcc098
--- /dev/null
+++ b/hosts/poweredge/hardware-configuration.nix
@@ -0,0 +1,50 @@
+# Do not modify this file! It was generated by ‘nixos-generate-config’
+# and may be overwritten by future invocations. Please make changes
+# to /etc/nixos/configuration.nix instead.
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+ imports =
+ [ (modulesPath + "/installer/scan/not-detected.nix")
+ ];
+
+ boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "megaraid_sas" "usbhid" "usb_storage" "sd_mod" "sr_mod" ];
+ boot.initrd.kernelModules = [ ];
+ boot.kernelModules = [ "kvm-intel" ];
+ boot.extraModulePackages = [ ];
+
+ fileSystems."/" =
+ { device = "zpool/root";
+ fsType = "zfs";
+ options = [ "zfsutil" ];
+ };
+
+ fileSystems."/nix" =
+ { device = "zpool/nix";
+ fsType = "zfs";
+ options = [ "zfsutil" ];
+ };
+
+ fileSystems."/var" =
+ { device = "zpool/var";
+ fsType = "zfs";
+ options = [ "zfsutil" ];
+ };
+
+ fileSystems."/home" =
+ { device = "zpool/home";
+ fsType = "zfs";
+ options = [ "zfsutil" ];
+ };
+
+ fileSystems."/boot" =
+ { device = "/dev/disk/by-uuid/D083-98C0";
+ fsType = "vfat";
+ options = [ "fmask=0022" "dmask=0022" ];
+ };
+
+ swapDevices = [ ];
+
+ nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
+ hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+}
diff --git a/hosts/poweredge/immich.nix b/hosts/poweredge/immich.nix
new file mode 100644
index 0000000..ff0275c
--- /dev/null
+++ b/hosts/poweredge/immich.nix
@@ -0,0 +1,89 @@
+{ pkgs, ... }: let
+ onnxruntimeCuda = pkgs.onnxruntime.override { cudaSupport = true; };
+in {
+ containers.immich = {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:00:01";
+
+ # Host path
+ bindMounts = {
+ "/var/lib/immich" = {
+ hostPath = "/media/ingens/immich";
+ isReadOnly = false;
+ };
+ };
+
+ # GPU
+ allowedDevices = [
+ { node = "/dev/nvidia0"; modifier = "rw"; }
+ { node = "/dev/nvidiactl"; modifier = "rw"; }
+ { node = "/dev/nvidia-uvm"; modifier = "rw"; }
+ { node = "/dev/nvidia-uvm-tools"; modifier = "rw"; }
+ { node = "/dev/nvidia-modeset"; modifier = "rw"; }
+ ];
+
+ bindMounts = {
+ # NVENC/NVDEC - video transcoding
+ "/dev/nvidia0" = { hostPath = "/dev/nvidia0"; isReadOnly = false; };
+ "/dev/nvidiactl" = { hostPath = "/dev/nvidiactl"; isReadOnly = false; };
+ # CUDA - required for ML inference
+ "/dev/nvidia-uvm" = { hostPath = "/dev/nvidia-uvm"; isReadOnly = false; };
+ "/dev/nvidia-uvm-tools" = { hostPath = "/dev/nvidia-uvm-tools"; isReadOnly = false; };
+ # Modeset - needed by some driver paths
+ "/dev/nvidia-modeset" = { hostPath = "/dev/nvidia-modeset"; isReadOnly = false; };
+ };
+
+ config = { lib, config, ... }: {
+ imports = [ ./nvidia.nix ];
+
+ # Network
+ networking.interfaces.eth0.useDHCP = true;
+ networking.firewall.allowedTCPPorts = [ 80 2283 ]; # Caddy + immich
+
+ # Immich
+ services.immich = {
+ enable = true;
+ host = "0.0.0.0";
+ package = pkgs.unstable.immich;
+ accelerationDevices = [
+ "/dev/nvidia0"
+ "/dev/nvidiactl"
+ "/dev/nvidia-uvm"
+ "/dev/nvidia-uvm-tools"
+ "/dev/nvidia-modeset"
+ ];
+
+ # Tell immich-server to use NVENC for transcoding
+ environment = {
+ NVIDIA_VISIBLE_DEVICES = "all";
+ NVIDIA_DRIVER_CAPABILITIES = "compute,video,utility";
+ };
+
+ # Enable the ML microservice with CUDA
+ # FIXME Disabled since the ML service w/ CUDA uses almost all the vram on my gpu
+ machine-learning.enable = false;
+ };
+ environment.systemPackages = with pkgs.unstable; [ immich immich-cli ];
+
+ # TODO IPP is missing a lot of features right now. Check back later
+ ## Immich public proxy
+ #services.immich-public-proxy = {
+ # enable = true;
+ # immichUrl = "http://localhost:2283";
+ # package = pkgs.unstable.immich-public-proxy;
+ #};
+
+ # Reverse proxy
+ services.caddy = {
+ enable = true;
+ virtualHosts.":80".extraConfig = ''
+ reverse_proxy localhost:2283
+ '';
+ };
+
+ system.stateVersion = "25.11";
+ };
+ };
+}
diff --git a/hosts/poweredge/jellyfin.nix b/hosts/poweredge/jellyfin.nix
new file mode 100644
index 0000000..761c3ae
--- /dev/null
+++ b/hosts/poweredge/jellyfin.nix
@@ -0,0 +1,91 @@
+{
+ containers.jellyfin = {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:00:02";
+ bindMounts = {
+ "/media" = {
+ hostPath = "/media/ingens/media";
+ isReadOnly = true;
+ };
+ };
+
+ # GPU
+ allowedDevices = [
+ { node = "/dev/nvidia0"; modifier = "rw"; }
+ { node = "/dev/nvidiactl"; modifier = "rw"; }
+ { node = "/dev/nvidia-uvm"; modifier = "rw"; }
+ { node = "/dev/nvidia-uvm-tools"; modifier = "rw"; }
+ { node = "/dev/nvidia-modeset"; modifier = "rw"; }
+ ];
+
+ bindMounts = {
+ # NVENC/NVDEC - video transcoding
+ "/dev/nvidia0" = { hostPath = "/dev/nvidia0"; isReadOnly = false; };
+ "/dev/nvidiactl" = { hostPath = "/dev/nvidiactl"; isReadOnly = false; };
+ # CUDA - required for ML inference
+ "/dev/nvidia-uvm" = { hostPath = "/dev/nvidia-uvm"; isReadOnly = false; };
+ "/dev/nvidia-uvm-tools" = { hostPath = "/dev/nvidia-uvm-tools"; isReadOnly = false; };
+ # Modeset - needed by some driver paths
+ "/dev/nvidia-modeset" = { hostPath = "/dev/nvidia-modeset"; isReadOnly = false; };
+ };
+
+ config = { lib, config, pkgs, ... }: {
+ imports = [ ./nvidia.nix ];
+
+ # Network
+ networking.interfaces.eth0.useDHCP = true;
+ networking.firewall.allowedTCPPorts = [ 80 ]; # Caddy
+
+ # Jellyfin
+ services.jellyfin = {
+ enable = true;
+ openFirewall = true;
+
+ hardwareAcceleration = {
+ enable = true;
+ type = "nvenc";
+ device = "/dev/nvidia0"; # Required by the module's assertion, unused in encoding.xml for nvenc
+ };
+
+ forceEncodingConfig = true; # Override encoding.xml
+
+ transcoding = {
+ enableHardwareEncoding = true;
+
+ hardwareEncodingCodecs = {
+ hevc = true; # Pascal 4th-gen NVENC supports HEVC 8/10-bit encode
+ av1 = false; # NOT supported on Pascal — do not enable
+ };
+
+ hardwareDecodingCodecs = {
+ h264 = true;
+ hevc = true;
+ hevc10bit = true;
+ vp8 = true;
+ vp9 = true;
+ mpeg2 = true;
+ vc1 = true;
+ av1 = false; # NOT supported on Pascal NVDEC
+ hevcRExt10bit = false; # Range Extensions decode is a newer-gen feature, skip on Pascal
+ hevcRExt12bit = false;
+ };
+
+ encodingPreset = "medium";
+ enableToneMapping = true; # works via CUDA-based tonemap_cuda filter on Pascal+
+ };
+ };
+
+ # Reverse proxy
+ services.caddy = {
+ enable = true;
+ virtualHosts.":80".extraConfig = ''
+ reverse_proxy localhost:8096
+ '';
+ };
+
+ system.stateVersion = "26.05";
+ };
+ };
+}
diff --git a/hosts/poweredge/metrics.nix b/hosts/poweredge/metrics.nix
new file mode 100644
index 0000000..edf3da9
--- /dev/null
+++ b/hosts/poweredge/metrics.nix
@@ -0,0 +1,43 @@
+{ config, inputs, ... }: {
+ imports = [ inputs.telemetry.nixosModules.collectors ];
+
+ services.telemetry.collectors = {
+ enable = true;
+ logs.lokiUrl = "http://metrics:3100"; # The container
+ };
+
+ containers.metrics = {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:00:06";
+ ephemeral = true;
+
+ # The module reads whatever path you point secretKeyFile at.
+ bindMounts = {
+ "/var/lib/prometheus2" = { hostPath = "/nix/persist/metrics/prometheus2"; isReadOnly = false; };
+ "/var/lib/loki" = { hostPath = "/nix/persist/metrics/loki"; isReadOnly = false; };
+ "/var/lib/grafana" = { hostPath = "/nix/persist/metrics/grafana"; isReadOnly = false; };
+ };
+
+ config = { ... }: {
+ imports = [ inputs.telemetry.nixosModules.server ];
+
+ networking.enableIPv6 = false;
+ networking.interfaces.eth0.useDHCP = true;
+
+ services.telemetry.server = {
+ enable = true;
+ collectorHosts = [ "poweredge" ];
+ grafana.generateSecretKey = true;
+ proxy.enable = true;
+ };
+
+ # Pin loki uid+gid
+ users.users.loki = { uid = 3100; group = "loki"; isSystemUser = true; };
+ users.groups.loki.gid = 3100;
+
+ system.stateVersion = "26.05";
+ };
+ };
+}
diff --git a/hosts/poweredge/navidrome.nix b/hosts/poweredge/navidrome.nix
new file mode 100644
index 0000000..fa40ed4
--- /dev/null
+++ b/hosts/poweredge/navidrome.nix
@@ -0,0 +1,51 @@
+{
+ containers.navidrome = {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:00:08";
+
+ bindMounts = {
+ "/media/music" = {
+ hostPath = "/media/ingens/media/music";
+ isReadOnly = true;
+ };
+ #"/media/podcasts" = {
+ # hostPath = "/media/ingens/media/podcasts";
+ # isReadOnly = true;
+ #};
+ };
+
+ config = { lib, config, pkgs, ... }: {
+ # Network
+ networking.interfaces.eth0.useDHCP = true;
+ networking.firewall.allowedTCPPorts = [ 80 ]; # Caddy
+
+ # Navidrome
+ services.navidrome = {
+ enable = true;
+
+ plugins = with pkgs.navidromePlugins; [
+ #audiomuseai
+ listenbrainz-daily-playlist
+ #lyrics-plugin
+ ];
+
+ settings.MusicFolder = "/media/music/library";
+
+ # The lyrics plugin bundle is named lyrics-plugin.ndp.
+ settings.LyricsPriority = ".ttml,.yaml,.yml,.elrc,.srt,lyrics-plugin,embedded,.lrc,.txt";
+ };
+
+ # Reverse proxy
+ services.caddy = {
+ enable = true;
+ virtualHosts.":80".extraConfig = ''
+ reverse_proxy localhost:4533
+ '';
+ };
+
+ system.stateVersion = "26.05";
+ };
+ };
+}
diff --git a/hosts/poweredge/networking.nix b/hosts/poweredge/networking.nix
new file mode 100644
index 0000000..8be3f4d
--- /dev/null
+++ b/hosts/poweredge/networking.nix
@@ -0,0 +1,167 @@
+let
+ hostIp = "192.168.1.10";
+in { pkgs, config, ... }: {
+ networking = {
+ # Label lan and wan interfaces
+ _interfaceLabels = {
+ enable = true;
+ interfaces.lan0 = "50:9a:4c:5d:c3:7a";
+ interfaces.wan0 = "50:9a:4c:5d:c3:7b";
+ };
+ # Create bridged lan interface for all containers
+ bridges.br-lan0.interfaces = [ "lan0" ];
+ # Disable dhcp on router interfaces
+ interfaces = {
+ veth-router-lan.useDHCP = false;
+ vb-router-lan0.useDHCP = false;
+ };
+ # Configure network
+ defaultGateway = "10.255.255.1"; # Read explaination for veth-router-lan below
+ nameservers = [ "192.168.1.1" ]; # DNS will only be available from this ip address THROUGH the default gateway
+ # br-lan0 will be the interface used for networking on poweredge host
+ interfaces.br-lan0.ipv4.addresses = [{
+ address = hostIp;
+ prefixLength = 24;
+ }];
+ };
+
+ # Secrets
+ sops.secrets.router-wg0 = { sopsFile = ./resources/secrets/router.yaml; key = "wg0"; }; # Office
+ sops.secrets.router-wg1 = { sopsFile = ./resources/secrets/router.yaml; key = "wg1"; }; # Remote access
+
+ # Router container
+ containers.router = {
+ autoStart = true;
+ ephemeral = true;
+ privateNetwork = true;
+ # Pass wan0 directly into container since it isn't needed elsewhere
+ interfaces = [ "wan0" ];
+ # Setup router lan0
+ # NOTE: Host/container communication is not possible through a hostBridge interface
+ extraVeths.vb-router-lan0.hostBridge = "br-lan0";
+ # Setup virtual host-router bridge interface.
+ # This is the default gateway for host/container communication since
+ # communication isn't possible through hostBridge interfaces.
+ # This is essentially equivalent to connecting the host to the
+ # container with a virtual ethernet cable on a separate interface.
+ extraVeths.veth-router-lan = {
+ hostAddress = "10.255.255.2";
+ localAddress = "10.255.255.1";
+ };
+ # Bind secrets to container
+ bindMounts."/run/secrets/wg0" = {
+ hostPath = config.sops.secrets.router-wg0.path;
+ isReadOnly = true;
+ };
+ bindMounts."/run/secrets/wg1" = {
+ hostPath = config.sops.secrets.router-wg1.path;
+ isReadOnly = true;
+ };
+
+ config = { lib, config, ... }: {
+ imports = [
+ ../../nixos/services/router
+ ./router-hosts.nix # Contains dhcp config + static leases + overrides
+ ];
+
+ networking = {
+ # Set ip addresses
+ enableIPv6 = false;
+ interfaces = {
+ vb-router-lan0.ipv4.addresses = [{
+ address = "192.168.1.1";
+ prefixLength = 24;
+ }];
+ wan0.useDHCP = true;
+ };
+ # Setup wireguard
+ wg-quick.interfaces = {
+ wg0.configFile = "/run/secrets/wg0";
+ wg1.configFile = "/run/secrets/wg1";
+ };
+ # NAT (port-forwarding) rules
+ nat.forwardPorts = [
+ { sourcePort = 8333; proto = "tcp"; destination = "192.168.1.44:8333"; } # Bitcoin p2p
+ ];
+ firewall.allowedUDPPorts = [ 51820 ]; # Allow wg1 running on router host through w/o NAT
+ # Additional advanced rules
+ # TODO add multi NAT feature to router service (this is just a normal nat rule)
+ nftables = {
+ enable = true;
+ tables = {
+ # NAT/masquerade wg0 allowing vb-router-lan0 clients to access wg0
+ wg-nat = {
+ family = "ip";
+ content = ''
+ chain post {
+ type nat hook postrouting priority srcnat; policy accept;
+ iifname "vb-router-lan0" oifname "wg0" masquerade comment "vb-router-lan0 => wg0"
+ iifname "veth-router-lan" oifname "wg0" masquerade comment "veth-router-lan => wg0"
+ }
+ '';
+ };
+ # Hairpin for wg1 clients to access the host via veth-router-lan since path is broken otherwise
+ host-hairpin = {
+ family = "ip";
+ content = ''
+ chain pre {
+ type nat hook prerouting priority dstnat; policy accept;
+ iifname "wg1" ip daddr 192.168.1.10 dnat to 10.255.255.2 comment "wg1 => host, redirect via veth"
+ }
+ chain post {
+ type nat hook postrouting priority srcnat; policy accept;
+ ip daddr 10.255.255.2 masquerade comment "wg1 => host, hairpin masquerade"
+ }
+ '';
+ };
+ };
+ };
+ };
+
+ # Setup router
+ services._router = {
+ dnsDhcpConfig.enable = true;
+ routing = {
+ enable = true;
+ interfaces = {
+ lan = [ "vb-router-lan0" "veth-router-lan" ];
+ wan = "wan0";
+ };
+ };
+ };
+
+ services.unbound._blocklists = {
+ enable = true;
+ hageziBlocklists = [ "pro" "nsfw" ];
+ };
+
+
+ environment.systemPackages = with pkgs; [
+ tcpdump
+ ];
+
+ system.stateVersion = "25.11";
+ };
+ };
+
+ # FIXME the following snippet will cause the router container to fail to start:
+ # networking.defaultGateway = {
+ # address = "10.255.255.1";
+ # interface = "veth-router-lan";
+ # };
+ # Journalctl will report:
+ # poweredge container router: Bring veth-router-lan up
+ # poweredge container router: RTNETLINK answers: File exists
+ # poweredge systemd: container@router.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
+ # So the issue nixos is creating an interface with that same name.
+ # As a temporary workaround, the following service is deployed (after the container starts):
+ systemd.services.router-default-route = {
+ after = [ "container@router.service" ];
+ wants = [ "container@router.service" ];
+ serviceConfig.Type = "oneshot";
+ script = ''
+ /run/current-system/sw/bin/ip route replace default via 10.255.255.1 dev veth-router-lan
+ '';
+ wantedBy = [ "multi-user.target" ];
+ };
+}
diff --git a/hosts/poweredge/notification-mailer.nix b/hosts/poweredge/notification-mailer.nix
new file mode 100644
index 0000000..d8fddc7
--- /dev/null
+++ b/hosts/poweredge/notification-mailer.nix
@@ -0,0 +1,27 @@
+{ config, ... }: let
+ serverEmail = "server-notifications@tjkeller.xyz";
+in {
+ # Mailer password secret for mail application password
+ sops.secrets.mailerPassword.sopsFile = ./resources/secrets/mailer-pass.yaml;
+
+ # Enable mta for system event notifications
+ services.mail._mailer = {
+ sender = {
+ host = "mail.tjkeller.xyz";
+ user = serverEmail;
+ from = serverEmail;
+ passwordFile = config.sops.secrets.mailerPassword.path;
+ };
+ recipient = serverEmail;
+ };
+
+ # Enable zed mailer module
+ services.zfs._zedMailer.enable = true;
+
+ # Enable smartd notifications
+ services.smartd.notifications.mail = {
+ enable = true;
+ sender = serverEmail;
+ recipient = serverEmail;
+ };
+}
diff --git a/hosts/poweredge/nvidia.nix b/hosts/poweredge/nvidia.nix
new file mode 100644
index 0000000..5eecc71
--- /dev/null
+++ b/hosts/poweredge/nvidia.nix
@@ -0,0 +1,29 @@
+{ lib, config, pkgs, ... }: {
+ # Configure nixpkgs for nvidia/cuda
+ nixpkgs.config = {
+ cudaSupport = true;
+ allowUnfreePredicate = pkg:
+ pkgs._cuda.lib.allowUnfreeCudaPredicate pkg
+ || builtins.elem (lib.getName pkg) [
+ "nvidia-x11"
+ "nvidia-kernel-modules"
+ ];
+ };
+
+ # NVIDIA
+ services.xserver.videoDrivers = [ "nvidia" ]; # xserver.videoDrivers does not imply X
+ hardware.graphics.enable = true;
+ hardware.nvidia = {
+ modesetting.enable = true; # Required
+ nvidiaSettings = false; # Don't need gui
+ open = false;
+ package = config.boot.kernelPackages.nvidiaPackages.legacy_580; # Support for P600
+ powerManagement.enable = false; # Can cause bugs
+ powerManagement.finegrained = false; # Only works on turing and newer
+ };
+
+ # Packages
+ environment.systemPackages = with pkgs; [
+ config.hardware.nvidia.package # nvidia-smi
+ ];
+}
diff --git a/hosts/poweredge/resources/secrets/ddns-updater-config.yaml b/hosts/poweredge/resources/secrets/ddns-updater-config.yaml
new file mode 100644
index 0000000..3be017b
--- /dev/null
+++ b/hosts/poweredge/resources/secrets/ddns-updater-config.yaml
@@ -0,0 +1,16 @@
+ddns-updater-config: ENC[AES256_GCM,data:vJ3z4R6P1gHKfkm6L2mQl68MKDJwpMNmrAOQo+4GkO2NC6EjKTLoSKhFiaGWVjMm7nrVfYRV+U/6b4VJXV4qURWhsm41t3x8zXAtt0viLC6pv+uMtuxadhU2Zxij4U2bSiMn6sSbfHd3uGIym7FnfOIL3LPEanVMuRUk20a0ZgHBdq1BPk6r5V8AoGfsu1XWHTvnO4ggg9oQPtGhurKTXixTD0Rb1Iv43JXLXqK/O3JGD5h4XbDmXB9eTqiBHUgZ0E4F5SE23L5mO0kI0TNNph2lTHXdfB+5,iv:xFry3gzdvvYh127yhYySvp5UHDa8Y+t/bg2+mwJ/HXo=,tag:pH2CE2l2UpNJiLJ+tjVvqQ==,type:str]
+sops:
+ age:
+ - recipient: age1zfvmt2avdlfz0fvchczplc84u7m8vqausm7zytl9s4x9m9yax4cqy30zpz
+ enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtWitQcVlaTmFVaHIraHlT
+ VFBDVEtlQUlqckN4eFF3YU95N3ZNU3JQcFNzCnkrR2xmTEtyUHRWQlRnTWZSaGVT
+ U0wvcGt6R0w4L3dSakVDVWVpTUhWbWMKLS0tIGVKSXVTL1B2L2FlSkQwSDVYd3Fk
+ WE8rLy81UEU5ZG9SaHRLOHNqOWUzWnMKBFtzJ9frroYk6hoW+1ww/3LpxCEa1Vtr
+ KNNnHKry8lQQDmalN5ZVYMTVAlTnQQ6QE7DxBukUwWYmizQ+BY8HDg==
+ -----END AGE ENCRYPTED FILE-----
+ lastmodified: "2026-03-31T01:47:37Z"
+ mac: ENC[AES256_GCM,data:8ozC5JWR/s3nNK+njc7zO32/7ptd//wuWGWZPHXrPV1iVyYndczGgu0ekEyKeRCn/WwGE5pyt32gy0l2Y+k7j7mV6GJguy6qhltani6Mz2Gfy5sRohn5s2rBDTiSYEVAgGTRt56DLxGD36P6xFPm+wHGspjCzNALrPretuN5xFg=,iv:+/mlXEMEO80pDVpFwZmnyywvHR/V9zHkbloF/e/dJ6Q=,tag:O+Ox0xUzERjeB+VftiUNEg==,type:str]
+ unencrypted_suffix: _unencrypted
+ version: 3.12.1
diff --git a/hosts/poweredge/resources/secrets/grafana.yaml b/hosts/poweredge/resources/secrets/grafana.yaml
new file mode 100644
index 0000000..91b9188
--- /dev/null
+++ b/hosts/poweredge/resources/secrets/grafana.yaml
@@ -0,0 +1,16 @@
+secret_key: ENC[AES256_GCM,data:9GHuENHGk+VKukRuUJi+fxOX+ZFa5q5t5UiGtxSQOEctu9LOJqvHJerFv3U=,iv:6OJTeWbotDVj/ogg9WC170Q4Kdfd1PXf0hHwXd/YaUQ=,tag:qlnEl2uzDuhwOKp17R76Tg==,type:str]
+sops:
+ age:
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrc0tWWHhtM2Q5UC9weSsr
+ WDdaOXJjZGVoekJKaXUyUXhsRXMxMjQrYUFnCnBqTGJEMFF6clQ1RXd5NE9CenQ5
+ U0swNHdNUUxndnlycGVGM0lpY0dFaVEKLS0tIHVlK2MySUJzaGVXd21iWFFzcEJM
+ Nmh3V2hIbUhWNUxlOE1idTdWNWZ6alEKWae75mhvfpTNOcvm7x1zf19QkmZqrAf7
+ jpkyKDE//d4Rn3h4Ogpl5bdXb9RyOoSEJUG+EDPFyynqdodCJ2ucpw==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age1zfvmt2avdlfz0fvchczplc84u7m8vqausm7zytl9s4x9m9yax4cqy30zpz
+ lastmodified: "2026-06-18T22:40:22Z"
+ mac: ENC[AES256_GCM,data:5HoPrQU8Q+SGEH4jQHy1otFXGILqttDpcbv+edNoKI/QFoUE74M4PrGzltlManZstqK3nfWV3HNIcY6GRe8TMP5ANC/8UIXfEDerP1Q0kj6xeyLumMCJ6iCjygF2SzL5YMWqBSsCJvH5O10sWncc2iPnGrQPJ8PksIxkXCe4YYk=,iv:f0WpJwENciWkyDKpGM/PaE2lmXWvNiMAxNpqlCEFhM8=,tag:nQAPTJPYsgRIUkDxktIvmA==,type:str]
+ unencrypted_suffix: _unencrypted
+ version: 3.13.1
diff --git a/hosts/poweredge/resources/secrets/mailer-pass.yaml b/hosts/poweredge/resources/secrets/mailer-pass.yaml
new file mode 100644
index 0000000..331bd66
--- /dev/null
+++ b/hosts/poweredge/resources/secrets/mailer-pass.yaml
@@ -0,0 +1,16 @@
+pass: ENC[AES256_GCM,data:RHOvLwbDIb8FZ+dG66e5U43qR0aXlLLZGAnlbRjSl8hxCMEtJ4940nggiaIV75jCaiWyLutay7MrKPKZBHDZwBIqcJYQRWm1zWGkoZi0/bX38vUFWOpI4qku9fIB2qll,iv:bqEnTagxlRqlAmMgFCtXXCSSlODE598yoV4fU0jSYL8=,tag:c/ZiGCDSb8quDoYiIKbMeQ==,type:str]
+sops:
+ age:
+ - recipient: age1zfvmt2avdlfz0fvchczplc84u7m8vqausm7zytl9s4x9m9yax4cqy30zpz
+ enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEUXlZaUhSUkNGK2xpVzRp
+ OEhYMTV6bnpPZC9tdHZWbnZxcUp6WWVLMnhFCmZmckVBckdRS1g0MjJQdE80S2Js
+ aGlNek1nSmU2aGI4cWVXR0NmbjJwa00KLS0tIDJ3N3BoenQ5ZW02K3BLNWxkWU5y
+ Ym56YzI5Zk9KeFhzZXJXR3NoOUl0ckEKOLweZrk/Pe6BG48+RrwOxyOy0Zb768aZ
+ YIxTBv/qSzZei6VqZHiIwTUEMyE7z3CS0dBFws6q4fB4LfIpv6fiYg==
+ -----END AGE ENCRYPTED FILE-----
+ lastmodified: "2026-03-31T01:37:29Z"
+ mac: ENC[AES256_GCM,data:WIGXvuwB4bcBDfMRrrMQ7faUkxFdreyYiuy6bNPI2pzvvUFTSo/lJTv/DjisSARdYmFHFvdResIXUjg75Sc2I5IrvRxZxnYqx/3z5k/WOFWb8HSKH2H+OUHtLkqWJSCQ9YBuX2tys93mEXgwchPpn4nzVaYBgxZl54F3icX7tsE=,iv:BS9KPGkVaH0G0bAZz6+LR0NDcmqw6khOkih5DyvGyug=,tag:dA9YVL1xEqUqe6hDzOH7XQ==,type:str]
+ unencrypted_suffix: _unencrypted
+ version: 3.12.1
diff --git a/hosts/poweredge/resources/secrets/router.yaml b/hosts/poweredge/resources/secrets/router.yaml
new file mode 100644
index 0000000..4cd3398
--- /dev/null
+++ b/hosts/poweredge/resources/secrets/router.yaml
@@ -0,0 +1,17 @@
+wg0: ENC[AES256_GCM,data:hxZrOTMx2lS7dkQPV48jUbHxoNr32KSM/wG0OPY8mcgHX4FsHStdH/CN4qlkH9co90yFKjwqcJ3fHXaSbIhp3oIYTo1R2/8KggHV+az4skb7v4kzZgU5u5V2B57gIRFrPHBBq1UtDkBBpANr7zypeLPuEzvCQaZiexrsCcET7xwhsEYtVc5WlISMHhi6sFzo1AeEVfwFvIUN9geXJwP3JycYRpaNXOVYAbPwer8fYa6mnFZIk+sruCO85QNoWQ2T1wPiC9U7Ki9q+FOaOzJWGdpiG3nQdeYlDgAwoPRNi3c0J6t3CS9xC+qC0y4esOL7ay4S2dDoL9ftChQjhWaGdq5+LeyXAPlATGs5aFtBMwEecQ5OPXTri2D5m/gVLW/TGmUT5ZVPymm/wAX+m4IWK6X3FylWnlEoUh+4cBO+2zjTbXEccRuJTOEZAGIsEg==,iv:ycsGniDPFOvFFGqCgiyCWkCS8AxFV7JbXgUxDPXAFmk=,tag:uXkB8b7S3sW9noTIK4J/Zw==,type:str]
+wg1: ENC[AES256_GCM,data:RIoKsBtvJ90mw3+Vs3K9MoVoLiGlO9ef1kom8Hkww5SFc9JcVAvs9JUgYrOoc5ATWTK6LPipXaQibWrbHTeBadZnAcRiHeL3/8FOgr8TkzH0KfDL7fd/AKzvJMf/fRZv6DCrY3LRn0PSwkPePiY7aoSpQRxScoR7mU8EOougUUPFolANZAjssKhFUix9bRO6/reU+B3Mzq30ozmMC+J4tsfYHe2beVS0qkLqTEyzh4hsSF3HCAHSUm0NunQwQlo0y2eYR+4tCXNje5X8PqxTRZpqLEFYaUDigdsmXX8DaVulszPnwGaAPsTMcZEGtO3FtPapVDfial3BrSgXZxTaE+a9t5m/F50aNUS1yxQHKd7lNrfI4Q1pX6ktAyW378mFhdT3fygHogkbjMrm947OdenZlA7jKRrsxQ2hVqP6g6x5/910nH7DVLEBkJ85TQfx5bd0eMQvcc/ox6NuCZZFaXCyry5MHZZa0o0Pnw3BW61J56Pxr2t8sraZFhfsTxTOyiL/L5O+PCd0osCn9SBTl8TAJl+/o8wje/GuFGtFkv54pJ9M1e86LKYt/ksU3HDwTDlblmwhtHILdy+ld2XU52ympCu5b9Ix61OTj4sqFYAfob+Ft5lPcbWqbT43HySW9iDTRbtcZVEXuZItrAlnQiXKe3CjQzPizY9P4KoFliUad8qOyvQ6PFG2ExaUL2vwXkQbk0gaCwfhg5HGg/AR2/y4mcey0gVyXJRm4WTSllNom48gEq/ylZcU9hWASNfkHj5W/MXeKbXMsu1u/6tv51anXEdNQTARIu+8zGSDHwL3wRw9hsiz7Fcg98rOrRZPRSjYniLxm2pkXvIZuS5lUUv6bRnEPv/3mw+HBjomjOVhNxdGa21AEf1QtItlBtWTnZawjY3KJumx4GuZmQQ5cunnlUp2Qu2k2jsNDQqhLEK0/gbgLkhi0Eh8lp9xJdQ0iXCqaALs4CcKM/3n/2THmN1Mjq+hjKNwY/TOZdfnyA9NPwQNB3UO5vd8za2/XfeObNRBs+Vd3hV9o1fyynYtHuhYwe0W6AKwmeotS87Jbl5jDu/r5ckpMNM6E+x89Bz95Y6VTh8zWA==,iv:X53OauqcsQEJou7gt/MuqUQ50VwA6hp5ykQ096HJb4c=,tag:k5v+KQtWj6dxjmp/zjvPKw==,type:str]
+sops:
+ age:
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxOFFVRFl2MDhiUVRPdnZJ
+ b3BSZ0pSaG8vR3o3SGlmVENvN3ZObHU3eUc4CmJrdk8rbXpBMWhTZ2hvdlk0VWo1
+ ejdnTDR4bFlUUXc4Smd0NEp6b3crZFUKLS0tIFBsQUdRWjZLSDY1ZlFaRDdLOVhs
+ MFFGY0NSU3NaK2U0U1NndUIyYm54ZWsKZ6q9j1fNaNSzBA0rbZYyUWt3U2V/7/9/
+ FZTKd8mH/uCoxvK8unlVZ9uYNADRh2smp7LwK1AWie/6khAIFqVBeg==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age1zfvmt2avdlfz0fvchczplc84u7m8vqausm7zytl9s4x9m9yax4cqy30zpz
+ lastmodified: "2026-10-09T21:56:37Z"
+ mac: ENC[AES256_GCM,data:yLU812vLFtABilSRFy44EI5YrNYypfoCM11QKZglw+RZcCjLZlm2zd53t3RsH2y0w2cEWsGsyECNWM2iES3F4+HqrBZmlVZ27GBSoGmX8+FzJnPkvlcfIG3F6C03AU7BLApjYmR+Lu0m2WdhwwXcCSvwN0HAKRZF0pHogoS/+6A=,iv:tMdCvkRBd6xbTPMuazNsT8tBSS4A2YOeaG0rRAxMwsI=,tag:KewEK3C9xhEh6YbDPi3GoQ==,type:str]
+ unencrypted_suffix: _unencrypted
+ version: 3.13.3
diff --git a/hosts/poweredge/resources/secrets/slskd.yaml b/hosts/poweredge/resources/secrets/slskd.yaml
new file mode 100644
index 0000000..3f0a43b
--- /dev/null
+++ b/hosts/poweredge/resources/secrets/slskd.yaml
@@ -0,0 +1,16 @@
+env-file: ENC[AES256_GCM,data:HFyQoP9iZMx4ieCk7f36McCTVN+5P+wZ5O+4bztmvIjJkdT4CgzXQDEuDEfDCwPNW25hAR+zV6znW6eO/tDoJndliD0ifl6+xODL9g34nInfocB1+alMjcHFz3Ix9HEMUYkABZ3qxn9DQGagrlRBB0sDMHkW2fRcl2Sd7qY5lhM676jpfF+oh2QTWYn9ZP8aS9FcYB2SaeFox3o=,iv:18IODA/fQBwlJhvNKXuo0kc18w4dEspEz5inusnh1f4=,tag:2jAJr5R5LnKwRAhC5lb5Pg==,type:str]
+sops:
+ age:
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2WitIOGhGVUp3N0pTQkds
+ OUJOQ0hReExNNEhZaXhENURwMGFGN3pBckVjClcvbDY4VUpXeWJXa0lLdWVoazNT
+ S2JjKzFaZ3lwWkZNbnRpOEpmREtMTm8KLS0tIE9yTXQvVzRvVU1YSGRLbWtQaGRB
+ REx6K2loY3JsTmFkNzZzN3FDdzhaaWsKRVP71gq5x0qPmWwhsDgTtDwe5Ip6cg78
+ pvU/OeUmXEdLJAsXFAaCIKPe5QSMaopgKiB9rfXJFM3mn2bhnHNYBQ==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age1zfvmt2avdlfz0fvchczplc84u7m8vqausm7zytl9s4x9m9yax4cqy30zpz
+ lastmodified: "2026-10-28T22:22:21Z"
+ mac: ENC[AES256_GCM,data:lhyXZ2PEZ59C7EfshQ5x09dmLTeAhlEjt5rwpMjdVcicUKioH1GdUD+hjKNAUDCfuBS1Oy/ay0yFGn9TZvMqngbXRgSxx4EY26EssvmfZ6wgp1F8t41+gm0tuQFUkkn51k713O21sTAZS7BfpOyiFCHYD852RbRoENJrgRK6KQk=,iv:Jw2Tw1QwdxY7cTT0UBiVJmJhtp+nNsQttv7G8yHs8Rk=,tag:CwjZpbMi8T4ubNMen0EqYg==,type:str]
+ unencrypted_suffix: _unencrypted
+ version: 3.13.3
diff --git a/hosts/poweredge/resources/secrets/vpn-router.yaml b/hosts/poweredge/resources/secrets/vpn-router.yaml
new file mode 100644
index 0000000..12c109b
--- /dev/null
+++ b/hosts/poweredge/resources/secrets/vpn-router.yaml
@@ -0,0 +1,17 @@
+ovpn-auth: ENC[AES256_GCM,data:9B/JKqliPVmzAiTX9gCayS2lLGqshFONueMz,iv:Yx+6VIP8W0/N059kqL2kAgdYrS8PT/Zk6WU6Tlfm+Kg=,tag:tUQG1Ry8FHQoCPYFcx7PFA==,type:str]
+ovpn-config: ENC[AES256_GCM,data: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,iv:R+LK9n9f87YU5bJMXVyNueE7CuLosFcdXzWReASCV5g=,tag:EXYg43lPkDugSUWJG4KgLw==,type:str]
+sops:
+ age:
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByNVd5TUlvczNnd2pzZnJn
+ eUREOFYrSmVYcGNDQzJqQlJETlk1R3Z1Mkc4Ckl5SFc1Tmdta2RQdkw2eXVTR3FX
+ TjFNTTlVTTgweHg1b1dNWjVXM2wzVU0KLS0tIDk2SkpDUWlwemVXY3Rveld1dXFV
+ ZWVTMjVKWVJaU0cvbjdOZTNaVWdlY28KsmuwUmhYdvcJLHIdBVJ6RKnPGlZmhG8q
+ dzotK3ZTynQPJYGC6n6OD1I3fV6GAYXNaVKafR6ckuJ5n0wbS+jjCg==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age1zfvmt2avdlfz0fvchczplc84u7m8vqausm7zytl9s4x9m9yax4cqy30zpz
+ lastmodified: "2026-10-28T22:18:45Z"
+ mac: ENC[AES256_GCM,data:fHLraNMezdvfGcTKf1kFpvWbDb1NM9FgRBYtG81IFl4HGhqvRwekRLty0UeT02IKk4LAdzs0QwSSDVUlo3Sun/1xqXgYWK0DDkPlNfQQ6IO45YJn9ESAmTFqWTKFdPjUNcl8EeUJdelHoYd/9p4dw4FL/2OTO4zb/XM7IsfZ+FU=,iv:Uq4Mpvk0OtkOnb7bk+sEUDxFeWLA5CZL0MJjrxNZ3gw=,tag:Q5zMdI1C5LQQ8HZRdjAHgA==,type:str]
+ unencrypted_suffix: _unencrypted
+ version: 3.13.3
diff --git a/hosts/poweredge/router-hosts.nix b/hosts/poweredge/router-hosts.nix
new file mode 100644
index 0000000..4e14c01
--- /dev/null
+++ b/hosts/poweredge/router-hosts.nix
@@ -0,0 +1,48 @@
+let
+ localDomain = "home.lan"; # TODO RFC8375 .home.arpa
+ dhcp = {
+ inherit staticLeases;
+ defaultGateway = "192.168.1.1";
+ localhostIp = "192.168.1.1";
+ rangeStart = "192.168.1.65";
+ rangeEnd = "192.168.1.250";
+ };
+ staticLeases = {
+ # Network
+ idrac-7N94GK2 = { macAddress = "50:9a:4c:5d:c3:7c"; staticIp = "192.168.1.2"; };
+ OpenWrt-Attic = { macAddress = "34:98:b5:60:5e:be"; staticIp = "192.168.1.3"; };
+ OpenWrt-Basement = { macAddress = "8c:3b:ad:35:c7:8c"; staticIp = "192.168.1.4"; };
+ ArcherC54 = { macAddress = "14:eb:b6:13:f9:e0"; staticIp = "192.168.1.5"; };
+ vpn-router = { macAddress = "02:00:00:00:01:09"; staticIp = "192.168.1.9"; };
+ # Desktops
+ optiplex = { macAddress = "e4:54:e8:bc:ba:05"; staticIp = "192.168.1.12"; };
+ X230 = { macAddress = "84:3a:4b:60:34:c4"; staticIp = "192.168.1.13"; };
+ # Appliances
+ # TODO add epson printer
+ p1s = { macAddress = "3c:0f:02:d5:23:64"; staticIp = "192.168.1.39"; };
+ gnuslashprinter = { macAddress = "00:23:24:5b:f0:6d"; staticIp = "192.168.1.40"; };
+ # Services
+ immich = { macAddress = "02:00:00:00:00:01"; staticIp = "192.168.1.41"; };
+ jellyfin = { macAddress = "02:00:00:00:00:02"; staticIp = "192.168.1.42"; };
+ gitea = { macAddress = "02:00:00:00:00:03"; staticIp = "192.168.1.43"; };
+ bitcoind = { macAddress = "02:00:00:00:00:04"; staticIp = "192.168.1.44"; };
+ filebrowser = { macAddress = "02:00:00:00:00:05"; staticIp = "192.168.1.45"; };
+ grafana = { macAddress = "02:00:00:00:00:06"; staticIp = "192.168.1.46"; };
+ transmission = { macAddress = "02:00:00:00:01:47"; staticIp = "192.168.1.47"; };
+ navidrome = { macAddress = "02:00:00:00:00:08"; staticIp = "192.168.1.48"; };
+ slskd = { macAddress = "02:00:00:00:01:51"; staticIp = "192.168.1.51"; };
+ };
+ localDomainOverrides = {
+ "router.${localDomain}" = "192.168.1.1"; # FIXME ping router on separate host returns 127.0.0.2
+ "poweredge.${localDomain}" = "192.168.1.10";
+ };
+in {
+ imports = [ ../poweredge-pro/dns-overrides.nix ]; # Import dns overrides from poweredge pro for use with tun
+ services._router.dnsDhcpConfig = {
+ inherit dhcp;
+ localResolution = {
+ inherit localDomain localDomainOverrides;
+ };
+ };
+ networking.hosts."192.168.1.1" = [ "router.${localDomain}" "router" ];
+}
diff --git a/hosts/poweredge/slskd.nix b/hosts/poweredge/slskd.nix
new file mode 100644
index 0000000..542757f
--- /dev/null
+++ b/hosts/poweredge/slskd.nix
@@ -0,0 +1,91 @@
+{ config, ... }: {
+ # Secrets
+ sops.secrets.slskd-env = { sopsFile = ./resources/secrets/slskd.yaml; key = "env-file"; };
+
+ # Container
+ containers.slskd = let
+ home = "/var/lib/slskd";
+ download-dir = "${home}/complete";
+ incomplete-dir = "${home}/incomplete";
+ in {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:01:51";
+
+ # Download dirs, shared with transmission's media tree, plus a
+ # read-only mount of the whole library for Soulseek sharing
+ bindMounts = {
+ "${download-dir}" = {
+ hostPath = "/media/ingens/media/dls/slskd/complete";
+ isReadOnly = false;
+ };
+ "${incomplete-dir}" = {
+ hostPath = "/media/ingens/media/dls/slskd/incomplete";
+ isReadOnly = false;
+ };
+ "/srv" = {
+ hostPath = "/media/ingens/media/dls/slskd/srv";
+ isReadOnly = false;
+ };
+ "/media/ingens/media" = {
+ hostPath = "/media/ingens/media";
+ isReadOnly = true;
+ };
+ };
+
+ # Bind secrets
+ bindMounts."/run/secrets/slskd.env" = {
+ hostPath = config.sops.secrets.slskd-env.path;
+ isReadOnly = true;
+ };
+
+ config = { lib, config, pkgs, ... }: {
+ # Network
+ networking = {
+ enableIPv6 = false; # Prevent ip leaks
+ firewall.allowedTCPPorts = [ 80 50300 ]; # Web ui + soulseek listen port tcp
+ firewall.allowedUDPPorts = [ 50300 ]; # soulseek listen port udp
+ # DHCP (see transmission.nix)
+ interfaces.eth0.useDHCP = true;
+ dhcpcd.extraConfig = ''
+ interface eth0
+ static routers=192.168.1.9
+ static domain_name_servers=9.9.9.9 1.1.1.1
+ '';
+ };
+
+ # slskd
+ services.slskd = {
+ enable = true;
+ environmentFile = "/run/secrets/slskd.env";
+ settings = {
+ directories = {
+ downloads = download-dir;
+ incomplete = incomplete-dir;
+ };
+ shares.directories = [ "/srv" ];
+ transfers = {
+ upload.slots = 5;
+ upload.speed_limit = 0;
+ download.slots = 5;
+ download.speed_limit = 0;
+ };
+ flags.no_version_check = true;
+ };
+ };
+
+ users.users.slskd.uid = 1000; # uid is not set otherwise
+
+ # Reverse proxy
+ services.caddy = {
+ enable = true;
+ virtualHosts.":80".extraConfig = ''
+ reverse_proxy localhost:5030
+ '';
+ };
+
+ system.stateVersion = "26.05";
+ };
+ };
+}
diff --git a/hosts/poweredge/transmission.nix b/hosts/poweredge/transmission.nix
new file mode 100644
index 0000000..047a3ab
--- /dev/null
+++ b/hosts/poweredge/transmission.nix
@@ -0,0 +1,75 @@
+{
+ # Container
+ containers.transmission = let
+ home = "/var/lib/transmission";
+ download-dir = "${home}/Downloads";
+ incomplete-dir = "${home}/.incomplete";
+ in {
+ autoStart = true;
+ privateNetwork = true;
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:01:47";
+
+ # Download dirs
+ bindMounts = {
+ "${download-dir}" = {
+ hostPath = "/media/ingens/media/dls/transmission/complete";
+ isReadOnly = false;
+ };
+ "${incomplete-dir}" = {
+ hostPath = "/media/ingens/media/dls/transmission/incomplete";
+ isReadOnly = false;
+ };
+ "/media/ingens/media" = {
+ hostPath = "/media/ingens/media";
+ isReadOnly = false;
+ };
+ };
+
+ config = { lib, ... }: {
+ # Network
+ networking = {
+ enableIPv6 = false; # Prevent ip leaks
+ firewall.allowedTCPPorts = [ 80 51413 ]; # RPC interface + transmission tcp
+ firewall.allowedUDPPorts = [ 51413 ]; # transmission udp
+ # Override dhcp server settings for default route and dns servers with static settings
+ interfaces.eth0.useDHCP = true;
+ dhcpcd.extraConfig = ''
+ interface eth0
+ static routers=192.168.1.9
+ static domain_name_servers=9.9.9.9 1.1.1.1
+ '';
+ #defaultGateway = "192.168.1.9"; # Doesn't work, see above
+ #nameservers = [ "9.9.9.9" "1.1.1.1" ]; # Does work, above prevents using dhcp default nameserver
+ };
+
+ # Transmission
+ services.transmission = {
+ inherit home;
+ enable = true;
+ user = "transmission";
+
+ settings = {
+ inherit download-dir incomplete-dir;
+ downloadDirPermissions = "775";
+ };
+ };
+
+ # TODO remove (#258793)
+ systemd.services.transmission.serviceConfig = {
+ RootDirectoryStartOnly = lib.mkForce null;
+ RootDirectory = lib.mkForce null;
+ };
+
+ # Reverse proxy
+ services.caddy = {
+ enable = true;
+ virtualHosts.":80".extraConfig = ''
+ reverse_proxy localhost:9091
+ '';
+ };
+
+ system.stateVersion = "26.05";
+ };
+ };
+}
diff --git a/hosts/poweredge/vpn-router.nix b/hosts/poweredge/vpn-router.nix
new file mode 100644
index 0000000..ac7ce16
--- /dev/null
+++ b/hosts/poweredge/vpn-router.nix
@@ -0,0 +1,121 @@
+{ lib, config, pkgs, ... }: {
+ # Secrets
+ sops.secrets.vpn-router-ovpn-config = { sopsFile = ./resources/secrets/vpn-router.yaml; key = "ovpn-config"; };
+ sops.secrets.vpn-router-ovpn-auth = { sopsFile = ./resources/secrets/vpn-router.yaml; key = "ovpn-auth"; };
+
+ # Container
+ containers.vpn-router = {
+ autoStart = true;
+ privateNetwork = true;
+ enableTun = true; # OpenVPN requires
+ hostBridge = "br-lan0";
+ localMacAddress = "02:00:00:00:01:09";
+
+ # Bind secrets
+ bindMounts = {
+ "/run/secrets/ovpn-config.ovpn" = {
+ hostPath = config.sops.secrets.vpn-router-ovpn-config.path;
+ isReadOnly = true;
+ };
+ "/run/secrets/ovpn-auth" = {
+ hostPath = config.sops.secrets.vpn-router-ovpn-auth.path;
+ isReadOnly = true;
+ };
+ };
+
+ config = { lib, pkgs, ... }: {
+
+ # Networking
+ networking = {
+ enableIPv6 = false; # Prevent ip leaks
+ interfaces.eth0.useDHCP = true;
+ };
+
+ # Router config
+ # This container acts as a router that clients can connect to via default
+ # route. All traffic is forwarded over the VPN tunnel instead of WAN.
+ # This allows multiple containers to share the same VPN connection.
+ boot.kernel.sysctl."net.ipv4.ip_forward" = 1; # Enable IPv4 forwarding
+ networking.nat = {
+ enable = true;
+ externalInterface = "tun0";
+ internalInterfaces = [ "eth0" ];
+ # Forward ports over VPN
+ forwardPorts = [
+ { sourcePort = 51413; proto = "tcp"; destination = "192.168.1.47:51413"; } # Transmission TCP
+ { sourcePort = 51413; proto = "udp"; destination = "192.168.1.47:51413"; } # Transmission UDP
+ { sourcePort = 50300; proto = "tcp"; destination = "192.168.1.51:50300"; } # Slskd TCP
+ { sourcePort = 50300; proto = "udp"; destination = "192.168.1.51:50300"; } # Slskd UDP
+ ];
+ };
+
+ # OpenVPN
+ services.openvpn.servers.main = {
+ config = ''
+ config /run/secrets/ovpn-config.ovpn
+ auth-user-pass /run/secrets/ovpn-auth
+
+ # The server pushes tun-mtu 1500. A full 1500 byte packet plus
+ # AES-256-GCM/UDP/IP encapsulation (~52 bytes) is ~1552 on the wire,
+ # so it has to be IP fragmented to cross a 1500 byte path -- and the
+ # fragments get dropped upstream, blackholing every full size packet
+ # while small ones (dns, tcp handshakes) still pass. Clamp the TCP MSS
+ # so the encapsulated datagram never exceeds this, in both directions.
+ # Must come after the `config` include above so it wins.
+ #mssfix 1400
+ '';
+ autoStart = true;
+ updateResolvConf = true;
+ };
+
+ # VPN "killswitch", split across two chains:
+ networking.firewall.extraCommands = ''
+ # Get domain name host and port from ovpn config
+ SERVER_HOST=$(${pkgs.gawk}/bin/awk '/^remote /{print $2;exit}' /run/secrets/ovpn-config.ovpn)
+ SERVER_PORT=$(${pkgs.gawk}/bin/awk '/^remote /{print $3;exit}' /run/secrets/ovpn-config.ovpn)
+ [ -n "$SERVER_PORT" ] || SERVER_PORT=1194
+
+ # Router's own traffic: only lo, tun0, and established connections
+ ${pkgs.iptables}/bin/iptables -P OUTPUT DROP
+ ${pkgs.iptables}/bin/iptables -A OUTPUT -o lo -j ACCEPT
+ ${pkgs.iptables}/bin/iptables -A OUTPUT -o tun0 -j ACCEPT
+ ${pkgs.iptables}/bin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
+
+ # Get dns ip from resolv.conf
+ DNS_IP=$(${pkgs.gawk}/bin/awk '/^nameserver /{ if ($2 !~ /:/) { print $2; exit } }' /etc/resolv.conf)
+
+ # Allow dns
+ ${pkgs.iptables}/bin/iptables -A OUTPUT -o eth0 -p udp -d "$DNS_IP" --dport 53 -j ACCEPT
+ ${pkgs.iptables}/bin/iptables -A OUTPUT -o eth0 -p tcp -d "$DNS_IP" --dport 53 -j ACCEPT
+
+ # Resolve server ips from host
+ SERVER_IPS=""
+ ATTEMPT=0
+ while [ -z "$SERVER_IPS" ] && [ "$ATTEMPT" -lt 10 ]; do
+ ATTEMPT=$((ATTEMPT + 1))
+ SERVER_IPS=$(${pkgs.getent}/bin/getent ahostsv4 "$SERVER_HOST" 2>/dev/null | ${pkgs.gawk}/bin/awk '/STREAM/{print $1}')
+ [ -n "$SERVER_IPS" ] || sleep 3
+ done
+
+ # Die if no ips are resolved
+ if [ -z "$SERVER_IPS" ]; then
+ echo "killswitch: could not resolve '$SERVER_HOST' after $ATTEMPT attempts, failing closed with no vpn egress"
+ exit 1
+ fi
+
+ # Allow udp traffic to each vpn server ip (for openvpn udp tunnels only, strictly better than tcp)
+ for SERVER_IP in $SERVER_IPS; do
+ echo "killswitch: allowing $SERVER_IP:$SERVER_PORT/udp ($SERVER_HOST)"
+ ${pkgs.iptables}/bin/iptables -A OUTPUT -p udp -d "$SERVER_IP" --dport "$SERVER_PORT" -j ACCEPT
+ done
+
+ # Anything forwarded through this router: only allowed to reach tun0
+ ${pkgs.iptables}/bin/iptables -P FORWARD DROP
+ ${pkgs.iptables}/bin/iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
+ ${pkgs.iptables}/bin/iptables -A FORWARD -i eth0 -o tun0 -j ACCEPT
+ '';
+
+ system.stateVersion = "26.05";
+ };
+ };
+}