diff options
Diffstat (limited to 'hosts/poweredge')
| -rw-r--r-- | hosts/poweredge/bitcoind.nix | 41 | ||||
| -rw-r--r-- | hosts/poweredge/configuration.nix | 45 | ||||
| -rw-r--r-- | hosts/poweredge/ddns-updater.nix | 22 | ||||
| -rw-r--r-- | hosts/poweredge/filebrowser.nix | 48 | ||||
| -rw-r--r-- | hosts/poweredge/fileshares.nix | 47 | ||||
| -rw-r--r-- | hosts/poweredge/gitea.nix | 41 | ||||
| -rw-r--r-- | hosts/poweredge/hardware-configuration.nix | 50 | ||||
| -rw-r--r-- | hosts/poweredge/immich.nix | 78 | ||||
| -rw-r--r-- | hosts/poweredge/jellyfin.nix | 91 | ||||
| -rw-r--r-- | hosts/poweredge/lgtm.nix | 201 | ||||
| -rw-r--r-- | hosts/poweredge/networking.nix | 157 | ||||
| -rw-r--r-- | hosts/poweredge/notification-mailer.nix | 27 | ||||
| -rw-r--r-- | hosts/poweredge/nvidia.nix | 22 | ||||
| -rw-r--r-- | hosts/poweredge/resources/secrets/ddns-updater-config.yaml | 16 | ||||
| -rw-r--r-- | hosts/poweredge/resources/secrets/grafana.yaml | 16 | ||||
| -rw-r--r-- | hosts/poweredge/resources/secrets/mailer-pass.yaml | 16 | ||||
| -rw-r--r-- | hosts/poweredge/resources/secrets/router.yaml | 17 | ||||
| -rw-r--r-- | hosts/poweredge/resources/secrets/transmission.yaml | 17 | ||||
| -rw-r--r-- | hosts/poweredge/router-hosts.nix | 84 | ||||
| -rw-r--r-- | hosts/poweredge/transmission.nix | 118 |
20 files changed, 1154 insertions, 0 deletions
diff --git a/hosts/poweredge/bitcoind.nix b/hosts/poweredge/bitcoind.nix new file mode 100644 index 0000000..e282234 --- /dev/null +++ b/hosts/poweredge/bitcoind.nix @@ -0,0 +1,41 @@ +{ + 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 + ''; + }; + + system.stateVersion = "25.11"; + }; + }; +} diff --git a/hosts/poweredge/configuration.nix b/hosts/poweredge/configuration.nix new file mode 100644 index 0000000..c15f222 --- /dev/null +++ b/hosts/poweredge/configuration.nix @@ -0,0 +1,45 @@ +{ config, lib, pkgs, ... }: { + imports = [ + ./bitcoind.nix + ./ddns-updater.nix + ./filebrowser.nix + ./fileshares.nix + ./gitea.nix + ./immich.nix + ./jellyfin.nix + ./lgtm.nix + ./networking.nix + ./nvidia.nix + ./transmission.nix + #./notification-mailer.nix # TODO move some of this stuff to archetype + ]; + + # Allow unfree packages for nvidia/cuda + nixpkgs.config.allowUnfree = true; + + # 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; + }; + }; + + # 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..818b3df --- /dev/null +++ b/hosts/poweredge/filebrowser.nix @@ -0,0 +1,48 @@ +{ + 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, ... }: let + publicPort = 9000; + in { + # Network + networking.interfaces.eth0.useDHCP = true; + networking.firewall.allowedTCPPorts = [ 80 publicPort ]; # Caddy (private + public) + + # Filebrowser + services.filebrowser.enable = true; + + # Reverse proxy + services.caddy = { + enable = true; + # Private + virtualHosts.":80".extraConfig = '' + reverse_proxy localhost:8080 + ''; + # Public (not sure why toString is needed but ok) + virtualHosts.":${toString publicPort}".extraConfig = '' + @shared { + path /share/* + path /static/* + path /api/public/* + } + + reverse_proxy @shared localhost:8080 + ''; + }; + + system.stateVersion = "26.05"; + }; + }; +} diff --git a/hosts/poweredge/fileshares.nix b/hosts/poweredge/fileshares.nix new file mode 100644 index 0000000..c5fca71 --- /dev/null +++ b/hosts/poweredge/fileshares.nix @@ -0,0 +1,47 @@ +{ + # Configure services + services._fileShares = { + enable = true; + smb.openFirewall = true; + nfs.openFirewall = true; + }; + # Shares + services._fileShares.shares = { + PS2 = { + path = "/media/ingens/games/ps2"; + smb = { + enable = true; + allowUser = "ps2"; + extraOptions = { + "min protocol" = "NT1"; + "max protocol" = "NT1"; + }; + }; + }; + 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; + }; + }; + + # ps2 user + users.users.ps2 = { + isSystemUser = true; + password = "ps2"; + group = "ps2"; + }; + users.groups.ps2 = {}; +} 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..444a0f2 --- /dev/null +++ b/hosts/poweredge/immich.nix @@ -0,0 +1,78 @@ +{ 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, pkgs, config, ... }: { + imports = [ ./nvidia.nix ]; + + # Network + networking.interfaces.eth0.useDHCP = true; + networking.firewall.allowedTCPPorts = [ 80 ]; # Caddy + + # Immich + services.immich = { + enable = true; + 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 + machine-learning.enable = true; + }; + environment.systemPackages = with pkgs; [ immich immich-cli ]; + + # 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/lgtm.nix b/hosts/poweredge/lgtm.nix new file mode 100644 index 0000000..6f43c02 --- /dev/null +++ b/hosts/poweredge/lgtm.nix @@ -0,0 +1,201 @@ +{ pkgs, config, ... }: { + # Log shipping via Alloy + services.alloy.enable = true; + + environment.etc."alloy/config.alloy".text = '' + loki.source.journal "journal" { + forward_to = [loki.write.default.receiver] + relabel_rules = loki.relabel.journal.rules + max_age = "12h" + } + + loki.relabel "journal" { + forward_to = [] + rule { + source_labels = ["__journal__systemd_unit"] + target_label = "unit" + } + } + + loki.write "default" { + endpoint { + url = "http://lgtm:3100/loki/api/v1/push" + } + } + ''; + + # Export prometheus data from host for container to access + services.prometheus.exporters.smartctl = { + enable = true; + port = 9633; + listenAddress = "0.0.0.0"; + }; + + services.prometheus.exporters.node = { + enable = true; + port = 9100; + listenAddress = "0.0.0.0"; + enabledCollectors = [ "systemd" "diskstats" "filesystem" "meminfo" "cpu" ]; + # zfs + hwmon already on by default, no need to list them + }; + + # # Host config — zpool_influxdb needs real pool access + # services.telegraf = { + # enable = true; + # extraConfig = { + # agent.interval = "30s"; + # + # inputs.execd = [{ + # command = [ "${pkgs.zpool_influxdb}/bin/zpool_influxdb" "--execd" ]; + # signal = "none"; + # data_format = "influx"; + # }]; + # + # outputs.prometheus_client = [{ + # listen = ":9273"; + # metric_version = 2; + # }]; + # }; + # }; + + # Allow ports for prometheus + networking.firewall.allowedTCPPorts = [ 9100 9273 9633 ]; # at least reachable from container subnet + + # Secret key for grafana + sops.secrets.grafana-secret-key = { sopsFile = ./resources/secrets/grafana.yaml; key = "secret_key"; }; # Office + + # Container + containers.grafana = { + autoStart = true; + privateNetwork = true; + hostBridge = "br-lan0"; + localMacAddress = "02:00:00:00:00:06"; + + # Bind wg0-router secret to container + bindMounts."/run/secrets/grafana-secret_key" = { + hostPath = config.sops.secrets.grafana-secret-key.path; + isReadOnly = true; + }; + + config = { lib, pkgs, config, ... }: { + # Network + networking.enableIPv6 = false; + networking.interfaces.eth0.useDHCP = true; + networking.firewall.allowedTCPPorts = [ 80 3100 9090 ]; # Caddy (grafana) + loki + prometheus + + # Loki + services.loki = { + enable = true; + configuration = { + server = { + http_listen_port = 3100; + http_listen_address = "0.0.0.0"; # explicit, since the host's Alloy needs to reach it to push logs + }; + auth_enabled = false; + + common = { + path_prefix = "/var/lib/loki"; + storage.filesystem = { + chunks_directory = "/var/lib/loki/chunks"; + rules_directory = "/var/lib/loki/rules"; + }; + ring = { + instance_addr = "127.0.0.1"; + kvstore.store = "inmemory"; + }; + replication_factor = 1; + }; + + schema_config.configs = [{ + from = "2024-01-01"; + store = "tsdb"; + object_store = "filesystem"; + schema = "v13"; + index = { prefix = "index_"; period = "24h"; }; + }]; + }; + }; + + # Prometheus + services.prometheus = { + enable = true; + scrapeConfigs = [ + { + job_name = "smartctl"; + static_configs = [{ targets = [ "poweredge:9633" ]; }]; + } + { + job_name = "node"; + static_configs = [{ targets = [ "poweredge:9100" ]; }]; + } + { + job_name = "zpool"; + static_configs = [{ targets = [ "poweredge:9273" ]; }]; + } + ]; + }; + + # Grafana + # https://wiki.nixos.org/wiki/Grafana + services.grafana = { + enable = true; + settings.server = { http_addr = "0.0.0.0"; http_port = 3000; }; + provision = { + enable = true; + + # Creates a *mutable* dashboard provider, pulling from /etc/grafana-dashboards. + # With this, you can manually provision dashboards from JSON with `environment.etc` like below. + dashboards.settings.providers = [{ + name = "my dashboards"; + disableDeletion = true; + options = { + path = "/etc/grafana-dashboards"; + foldersFromFilesStructure = true; + }; + }]; + + datasources.settings.datasources = [ + { name = "Prometheus"; type = "prometheus"; url = "http://127.0.0.1:9090"; isDefault = true; } + { name = "Loki"; type = "loki"; url = "http://127.0.0.1:3100"; } + ]; + }; + }; + + # Grafana secret key + systemd.services.grafana.serviceConfig = { + LoadCredential = [ + "secret_key:/run/secrets/grafana-secret_key" + ]; + }; + services.grafana.settings.security.secret_key = "$__file{/run/credentials/grafana.service/secret_key}"; + + # Grafana dashboards + environment.etc = let + mkDashDl = id: rev: "https://grafana.com/api/dashboards/${toString id}/revisions/${toString rev}/download"; + in { + "grafana-dashboards/node-exporter-full.json".source = pkgs.fetchurl { + url = mkDashDl 1860 45; + sha256 = "sha256-GExrdAnzBtp1Ul13cvcZRbEM6iOtFrXXjEaY6g6lGYY="; + }; + "grafana-dashboards/smartctl-exporter.json".source = pkgs.fetchurl { + url = mkDashDl 22604 2; + sha256 = "sha256-ci8WE23fZ+ltEKFoUdNNVXsUIV0jqtas79ia2lYIo88="; + }; + "grafana-dashboards/zfs-pool-metrics.json".source = pkgs.fetchurl { + url = mkDashDl 15362 1; + sha256 = "sha256-latX1gwRjdpIalT5kpm9MfDaaXFLZCJ0ZP8CZJo8guI="; + }; + }; + + # Reverse proxy + services.caddy = { + enable = true; + virtualHosts.":80".extraConfig = '' + reverse_proxy localhost:3000 + ''; + }; + + system.stateVersion = "26.05"; + }; + }; +} diff --git a/hosts/poweredge/networking.nix b/hosts/poweredge/networking.nix new file mode 100644 index 0000000..bf0aa1c --- /dev/null +++ b/hosts/poweredge/networking.nix @@ -0,0 +1,157 @@ +let + hostIp = "192.168.1.10"; +in { 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 =[ + { # Bitcoin p2p + sourcePort = 8333; + proto = "tcp"; + destination = "192.168.1.44:8333"; + } + { # Filebrowser + sourcePort = 19045; + proto = "tcp"; + destination = "192.168.1.45:9000"; + } + ]; + 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" + } + ''; + }; + }; + }; + }; + + # 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" ]; + }; + + 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..257a46c --- /dev/null +++ b/hosts/poweredge/nvidia.nix @@ -0,0 +1,22 @@ +{ config, pkgs, ... }: { + # Configure nixpkgs for nvidia/cuda + nixpkgs.config.allowUnfree = true; + nixpkgs.config.cudaSupport = true; + + # 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..edf7b41 --- /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:4qwFVFszUQgBquAcgA4dB0TdBmTxprZLaL7+FQjrqG8avDFNxZyUFc9fHXiNRsvbgr+UKGVhdOast0y6kOaeQqqPiHLqQD/MN5wu8+SLFTaNppOKiwsjrK9x43GCxRT/ytmQWgb3pBVf7ruwbmykjDhZ577f3yhnNYhJuHoGLvTdJ18pxSE6DaukrJTnwa6BhxKv62IGlGHLYi5u9keR3LFCBwYyAIEjU9E+Na7mZ4cM2Y61/e0KTekMcQHqlBqvWmIDbdnLsoC/JVl/A8gIlU1TiOqo7geiUte/wwzc3dtkK+WQWuvaDrdtWdsUaKJEFPZhmx6KWZk4gTMdIlbP+KMm0X39rXIkMMyfWqs2rklnwm+BIg1vayQYsuAH3DXofxNlWcOEsjvnw19kPy8CrYOFAx2tPeyUWs1mFEQYeB0gafvRrFJfMUkh4uQuJnznjJH+8fmVM66EA6GLSRFUsjK/v4yEKftQQkYKuWRO9MpO5kTYo0YiSO9xOPbxNe+VVC1oJnQxaTeXNV2JZLjce/3+aZ3oFu5KG11yx+OqfRDBdJVcg6jEgVBlNY88ZrNTUUb9tEqlSXwuradURX1GekYwM0fBRTW+f/oiJFsbbBA8SODWl/50HnxcTqkeiYmnB2PA167nFLFOL/MYRh+txXbpcH4lwoPshPjGnM3XGxUxNIU+xqFFLHn/h6WhiyUZ6SkRgNGaGQ4w6RuRvZm3aHb0wAW0pwOnPoMbh2XDVTkdjFLxmJpAUHfy2W+FIYAGIU0DcT3/r2dW91hDSzjnysRxrPRiOc37nvsrqxjZd0+WI4KtHhpgxGkPyoV6CcFcKr7GrImlh2cVDr3oxrUHI9JrmD7VWKJV/ws6tSZsfHc=,iv:zXw3y9MIyaw/zXXl4CgavbP7QbAritZdhAWBL40vhtw=,tag:SXsG2HtActAlJrt7foqldA==,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-06-22T19:36:47Z" + mac: ENC[AES256_GCM,data:Dvl3nQrqpPiYUfRHJuLQCbt0QF1duV0/E+S8A2kQ3zjP5NxmO4lKFRXgcWwsDzm+IQl4pcIMQ+FqU/C7ujTqxQ3/PSRx4o51POWDwJA5DmNpVNzH87BuXDxXPiItlPKzAfpbs8TwGCIOvmOcEP7Pd55eWJucRAq29N6nnixFHHQ=,iv:G91DcLnshSlBrtTTyJdJyuOa4wgjNhWsoVRDUk+WJOU=,tag:9Fcj8Oxu5q/u9pAiU18xwg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.1 diff --git a/hosts/poweredge/resources/secrets/transmission.yaml b/hosts/poweredge/resources/secrets/transmission.yaml new file mode 100644 index 0000000..5945804 --- /dev/null +++ b/hosts/poweredge/resources/secrets/transmission.yaml @@ -0,0 +1,17 @@ +ovpn-auth: ENC[AES256_GCM,data:3F3/4v0xOsdWguywaKxE6IB6pzHONWk6F6Es,iv:7ZJUyeb0EZTVkTIk5W2rhBjXmUHm2TLe5/YKgOJiqPA=,tag:idiz76VtruJ2NOhUhtbXjw==,type:str] +ovpn-config: ENC[AES256_GCM,data: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,iv:Uy0Z4W4PUcwfkGfR6Mtf6AWShsU3dmLz+H9JOJd463c=,tag:B1KAAANSnRvb0ECwYP0QxQ==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVNkthU0srVEFZMk43ZFFu + dEJ3QkJyMEJ2OUhCR1Nwc3pQSHEzaXhjZFRzCm9idXlpUVM2WTRXQWY2SzFJei93 + bzdEdEdiS1NHYy9mYWY3UU5xcmJMN2MKLS0tIGtSblFnZDdHK1ltUTNyWUFVdFd5 + czZxZzUvSjlzcmtZRURleURwWGZVSjQK+DsUTq8yTkKZGK2rj1c9tFPm9nRoAjkQ + D5HFkN8uRVTB91JEx077I8VZLXJgJ6HFGOpW2EGV4nDilL9YP288Eg== + -----END AGE ENCRYPTED FILE----- + recipient: age1zfvmt2avdlfz0fvchczplc84u7m8vqausm7zytl9s4x9m9yax4cqy30zpz + lastmodified: "2026-06-20T18:05:47Z" + mac: ENC[AES256_GCM,data:+w9BPkacf5X9hJhh5Bmm8nf5eANZs07K/BS3c1q6jVdUoHPkVbOz0EbAEQkheW8xQAH5lIQl0D5GXSbGyhLnV3mKwL3RgV532aAY02IT68ygDvFPx6ziBEmqBlWaTZ3DopKOqPzje0olMuefYF1Li1LihmLEX3kMxwBFOr8oSn0=,iv:Lkq8NdRjbMI21bfVVoq08kgZQmgdFrh+xKgd8j/VAkc=,tag:bMwLv5ABrMSzwq5t6/s65g==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.1 diff --git a/hosts/poweredge/router-hosts.nix b/hosts/poweredge/router-hosts.nix new file mode 100644 index 0000000..2a08558 --- /dev/null +++ b/hosts/poweredge/router-hosts.nix @@ -0,0 +1,84 @@ +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.50"; + 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 = "12:eb:b6:13:f9:e2"; + staticIp = "192.168.1.5"; + }; + # Desktops + T495 = { + macAddress = "04:33:c2:9d:34:74"; + staticIp = "192.168.1.11"; + }; + 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"; + }; + # Services + gnuslashprinter = { + macAddress = "00:23:24:5b:f0:6d"; + staticIp = "192.168.1.40"; + }; + 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:00:07"; + staticIp = "192.168.1.47"; + }; + }; + dns.hostOverrides = { + "router.${localDomain}" = "192.168.1.1"; + "poweredge.${localDomain}" = "192.168.1.10"; + }; +in { + services._router.dnsDhcpConfig = { + inherit localDomain dhcp dns; + }; + networking.hosts."192.168.1.1" = [ "router.${localDomain}" "router" ]; +} diff --git a/hosts/poweredge/transmission.nix b/hosts/poweredge/transmission.nix new file mode 100644 index 0000000..cded95d --- /dev/null +++ b/hosts/poweredge/transmission.nix @@ -0,0 +1,118 @@ +{ config, pkgs, ... }: { + # Secrets + sops.secrets.transmission-ovpn-config = { sopsFile = ./resources/secrets/transmission.yaml; key = "ovpn-config"; }; + sops.secrets.transmission-ovpn-auth = { sopsFile = ./resources/secrets/transmission.yaml; key = "ovpn-auth"; }; + + # Container + containers.transmission = let + home = "/var/lib/transmission"; + download-dir = "${home}/complete"; + incomplete-dir = "${home}/incomplete"; + in { + autoStart = true; + privateNetwork = true; + enableTun = true; # OpenVPN requires + hostBridge = "br-lan0"; + localMacAddress = "02:00:00:00:00:07"; + + # Download dirs + bindMounts = { + "${download-dir}" = { + hostPath = "/media/ingens/media/.incomplete"; + isReadOnly = false; + }; + "${incomplete-dir}" = { + hostPath = "/media/ingens/media/.complete"; + isReadOnly = false; + }; + }; + + # Bind secrets + bindMounts."/run/secrets/ovpn-config.ovpn" = { + hostPath = config.sops.secrets.transmission-ovpn-config.path; + isReadOnly = true; + }; + bindMounts."/run/secrets/ovpn-auth" = { + hostPath = config.sops.secrets.transmission-ovpn-auth.path; + isReadOnly = true; + }; + + config = { lib, config, ... }: { + # Network + networking.enableIPv6 = false; # Prevent ip leaks + networking.interfaces.eth0.useDHCP = true; + networking.firewall.interfaces = { + eth0.allowedTCPPorts = [ 80 ]; # RPC interface + # Torrent ports + tun0 = { + allowedTCPPorts = [ 51413 ]; + allowedUDPPorts = [ 51413 ]; + }; + }; + + # Transmission + services.transmission = { + inherit home; + enable = true; + + settings = { + inherit download-dir incomplete-dir; + }; + }; + + # 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 + ''; + }; + + # OpenVPN + services.openvpn.servers.main = { + config = '' + config /run/secrets/ovpn-config.ovpn + auth-user-pass /run/secrets/ovpn-auth + ''; + autoStart = true; + updateResolvConf = true; + }; + + # VPN killswitch + 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) + + # Resolve server ip from host + while [ -z "$SERVER_IP" ]; do + sleep 3 + SERVER_IP=$(${pkgs.getent}/bin/getent hosts "$SERVER_HOST" 2>/dev/null | ${pkgs.gawk}/bin/awk '{print $1}') + echo "SERVER_IP: $SERVER_IP" + done + + # Only allow out traffic from tun0 + ${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 # Exception: allow established connections + ${pkgs.iptables}/bin/iptables -A OUTPUT -p udp -d "$SERVER_IP" --dport "$SERVER_PORT" -j ACCEPT + + # Allow DNS + DNS_IP=$(${pkgs.gawk}/bin/awk '/^nameserver /{print $2; exit}' /etc/resolv.conf) + ${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 + + # Allow transmission RPC + ''; + + system.stateVersion = "26.05"; + }; + }; +} |
