Nix configurations for my homelab
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Put vpn into a container and put qbittorrent inside that container

Trying to get the vpn to work properly outside of the container was
freaking me out, so we are doing this now

yemou 54241a60 d036c7ff

+211 -136
+1 -1
lutea/config.nix
··· 31 31 ../modules/tools.nix 32 32 ../modules/typst.nix 33 33 ../modules/virtualbox.nix 34 - ../modules/vpn.nix 34 + ../modules/vpn-container.nix 35 35 ]; 36 36 37 37 sops = {
+26
modules/containers.nix
··· 1 + { lib, ... }: 2 + { 3 + options.garden.container = lib.mkOption { 4 + description = "Container configuration"; 5 + type = 6 + with lib.types; 7 + attrsOf ( 8 + submodule ( 9 + { name, ... }: 10 + { 11 + options = { 12 + name = lib.mkOption { 13 + type = str; 14 + description = "Name of the host"; 15 + }; 16 + config = lib.mkOption { 17 + type = with lib.types; listOf attrs; 18 + }; 19 + }; 20 + 21 + config.name = name; 22 + } 23 + ) 24 + ); 25 + }; 26 + }
+71 -53
modules/qbittorrent.nix
··· 1 - { 2 - config, 3 - lib, 4 - pkgs, 5 - ... 6 - }: 1 + { lib, pkgs, ... }: 7 2 { 8 3 environment.persistence."/data/persistent".directories = [ 9 4 { 10 5 directory = "/var/lib/qBittorrent"; 11 6 mode = "0700"; 12 - user = config.services.qbittorrent.user; 13 - group = config.services.qbittorrent.group; 7 + user = "qbittorrent"; 8 + group = "qbittorrent"; 14 9 } 15 10 ]; 16 11 17 - # TODO: Make sure that the qbittorrent service only starts if the torrent interface is up 18 - 19 - systemd.services.protonvpn-qbittorrent-natpmp = { 20 - description = "Get a port and provide it to qBittorrent"; 21 - requires = [ 22 - "network-online.target" 23 - "qbittorrent.service" 24 - ]; 25 - wantedBy = [ "multi-user.target" ]; 26 - serviceConfig = { 27 - ExecStart = "${ 28 - pkgs.writeShellApplication { 29 - name = "protonvpn-natpmp"; 30 - runtimeInputs = with pkgs; [ 31 - curl 32 - gnugrep 33 - jq 34 - libnatpmp 35 - ]; 36 - text = builtins.readFile ../scripts/protonvpn-natpmp.sh; 37 - } 38 - }/bin/protonvpn-natpmp"; 39 - Restart = "on-failure"; 12 + users = { 13 + users.qbittorrent = { 14 + group = "qbittorrent"; 15 + isSystemUser = true; 40 16 }; 17 + groups.qbittorrent = { }; 41 18 }; 42 19 43 - services.qbittorrent = { 44 - enable = true; 45 - webuiPort = 8082; 46 - serverConfig = { 47 - LegalNotice.Accepted = true; 48 - BitTorrent.Session = { 49 - Interface = "vpnt"; 50 - InterfaceName = "vpnt"; 51 - TorrentContentLayout = "Subfolder"; 20 + containers.vpn = { 21 + bindMounts = { 22 + torrents = { 23 + hostPath = "/data/torrents"; 24 + mountPoint = "/torrents"; 25 + isReadOnly = false; 52 26 }; 53 - Network.PortForwardingEnabled = false; 54 - Preferences = { 55 - General.StatusbarExternalIPDisplayed = true; 56 - WebUI = lib.mkMerge [ 57 - (lib.mkIf (config.networking.hostName == "lutea") { LocalHostAuth = false; }) 58 - (lib.mkIf (config.networking.hostName == "lily") { 59 - AuthSubnetWhitelistEnable = true; 60 - AuthSubnetWhitelist = [ 61 - config.garden.lutea.ipv4-local 62 - config.garden.lutea.netbird-ip 63 - ]; 64 - }) 65 - ]; 27 + qbittorrent = { 28 + hostPath = "/var/lib/qBittorrent"; 29 + mountPoint = "/var/lib/qBittorrent"; 30 + isReadOnly = false; 66 31 }; 67 32 }; 68 33 }; 34 + 35 + garden.container.vpn.config = [ 36 + { 37 + networking.firewall.allowedTCPPorts = [ 8082 ]; 38 + 39 + systemd.services.protonvpn-qbittorrent-natpmp = { 40 + description = "Get a port and provide it to qBittorrent"; 41 + requires = [ 42 + "network-online.target" 43 + "qbittorrent.service" 44 + ]; 45 + wantedBy = [ "multi-user.target" ]; 46 + serviceConfig = { 47 + ExecStart = "${ 48 + pkgs.writeShellApplication { 49 + name = "protonvpn-natpmp"; 50 + runtimeInputs = with pkgs; [ 51 + curl 52 + gnugrep 53 + jq 54 + libnatpmp 55 + ]; 56 + text = builtins.readFile ../scripts/protonvpn-natpmp.sh; 57 + } 58 + }/bin/protonvpn-natpmp"; 59 + Restart = "on-failure"; 60 + }; 61 + }; 62 + 63 + services.qbittorrent = { 64 + enable = true; 65 + webuiPort = 8082; 66 + serverConfig = { 67 + LegalNotice.Accepted = true; 68 + BitTorrent.Session = { 69 + DefaultSavePath = "/torrents"; 70 + Interface = "vpn"; 71 + InterfaceName = "vpn"; 72 + TorrentContentLayout = "Subfolder"; 73 + }; 74 + Network.PortForwardingEnabled = false; 75 + Preferences = { 76 + General.StatusbarExternalIPDisplayed = true; 77 + WebUI = { 78 + LocalHostAuth = false; 79 + AuthSubnetWhitelistEnabled = true; 80 + AuthSubnetWhitelist = lib.strings.join ", " [ "192.168.2.1" ]; 81 + }; 82 + }; 83 + }; 84 + }; 85 + } 86 + ]; 69 87 }
+105
modules/vpn-container.nix
··· 1 + { config, lib, ... }: 2 + { 3 + sops.secrets = { 4 + "protonvpn-torrent/private-key" = { 5 + owner = "systemd-network"; 6 + group = "systemd-network"; 7 + }; 8 + "protonvpn-torrent/public-key" = { 9 + owner = "systemd-network"; 10 + group = "systemd-network"; 11 + }; 12 + }; 13 + 14 + imports = [ ./containers.nix ]; 15 + 16 + networking.nat = { 17 + enable = true; 18 + internalInterfaces = [ "ve-vpn" ]; 19 + externalInterface = "enp5s0"; 20 + enableIPv6 = true; 21 + }; 22 + 23 + systemd.network.networks."50-ignore-virtual-interfaces" = { 24 + matchConfig.Name = "ve-*"; 25 + linkConfig.Unmanaged = true; 26 + }; 27 + 28 + containers.vpn = { 29 + autoStart = true; 30 + privateNetwork = true; 31 + hostAddress = "192.168.2.1"; 32 + localAddress = "192.168.2.2"; 33 + hostAddress6 = "fd6c:696c:6163::1"; 34 + localAddress6 = "fd6c:696c:6163::2"; 35 + ephemeral = true; 36 + bindMounts = { 37 + pubkey = { 38 + hostPath = config.sops.secrets."protonvpn-torrent/public-key".path; 39 + mountPoint = "/pubkey"; 40 + isReadOnly = true; 41 + }; 42 + privkey = { 43 + hostPath = config.sops.secrets."protonvpn-torrent/private-key".path; 44 + mountPoint = "/privkey"; 45 + isReadOnly = true; 46 + }; 47 + }; 48 + config = { ... }: lib.mkMerge config.garden.container.vpn.config; 49 + }; 50 + 51 + garden.container.vpn.config = [ 52 + { 53 + networking = { 54 + useHostResolvConf = false; 55 + firewall.checkReversePath = "loose"; 56 + }; 57 + 58 + systemd.network = { 59 + enable = true; 60 + networks = { 61 + "50-vpn-torrent" = { 62 + matchConfig.Name = "vpn"; 63 + address = [ 64 + "2a07:b944::2:2/128" 65 + "10.2.0.2/32" 66 + ]; 67 + gateway = [ 68 + "2a07:b944::2:1" 69 + "10.2.0.1" 70 + ]; 71 + dns = [ 72 + "2a07:b944::2:1" 73 + "10.2.0.1" 74 + ]; 75 + routes = [ 76 + { Destination = "2a07:b944::2:1"; } 77 + { Destination = "10.2.0.1"; } 78 + ]; 79 + }; 80 + }; 81 + netdevs."50-vpn-torrent" = { 82 + netdevConfig = { 83 + Kind = "wireguard"; 84 + Name = "vpn"; 85 + }; 86 + wireguardConfig = { 87 + PrivateKeyFile = /privkey; 88 + RouteTable = "main"; 89 + }; 90 + wireguardPeers = [ 91 + { 92 + PublicKeyFile = /pubkey; 93 + Endpoint = "31.13.189.226:51820"; 94 + AllowedIPs = [ 95 + "::/0" 96 + "0.0.0.0/0" 97 + ]; 98 + } 99 + ]; 100 + }; 101 + }; 102 + } 103 + { system.stateVersion = "25.11"; } 104 + ]; 105 + }
-68
modules/vpn.nix
··· 1 - { config, ... }: 2 - { 3 - sops.secrets = { 4 - "protonvpn-torrent/private-key" = { 5 - sopsFile = ../secrets/lilu.yaml; 6 - owner = "systemd-network"; 7 - group = "systemd-network"; 8 - }; 9 - "protonvpn-torrent/public-key" = { 10 - sopsFile = ../secrets/lilu.yaml; 11 - owner = "systemd-network"; 12 - group = "systemd-network"; 13 - }; 14 - }; 15 - 16 - systemd.network = { 17 - networks."50-vpn-torrent" = { 18 - matchConfig.Name = "vpnt"; 19 - address = [ 20 - "2a07:b944::2:2/128" 21 - "10.2.0.2/32" 22 - ]; 23 - dns = [ 24 - "2a07:b944::2:1" 25 - "10.2.0.1" 26 - ]; 27 - routes = [ 28 - { Destination = "2a07:b944::2:1"; } 29 - { Destination = "10.2.0.1"; } 30 - { 31 - Destination = "::/0"; 32 - Table = 10; 33 - } 34 - { 35 - Destination = "0.0.0.0/0"; 36 - Table = 10; 37 - } 38 - ]; 39 - routingPolicyRules = [ 40 - { 41 - From = "2a07:b944::2:2"; 42 - Table = 10; 43 - } 44 - { 45 - From = "10.2.0.2"; 46 - Table = 10; 47 - } 48 - ]; 49 - }; 50 - netdevs."50-vpn-torrent" = { 51 - netdevConfig = { 52 - Kind = "wireguard"; 53 - Name = "vpnt"; 54 - }; 55 - wireguardConfig.PrivateKeyFile = config.sops.secrets."protonvpn-torrent/private-key".path; 56 - wireguardPeers = [ 57 - { 58 - PublicKeyFile = config.sops.secrets."protonvpn-torrent/public-key".path; 59 - Endpoint = "31.13.189.226:51820"; 60 - AllowedIPs = [ 61 - "0.0.0.0/0" 62 - "::/0" 63 - ]; 64 - } 65 - ]; 66 - }; 67 - }; 68 - }
+2 -5
secrets/lilu.yaml
··· 1 1 y6d-smtp: 2 2 user: ENC[AES256_GCM,data:IZK759k1/F6v,iv:Aj92dOU58OU1zCcCsKeaHzsvWePRo6s8sE5mMMwM4DM=,tag:1V12iaPqjroNBQfaJHlP5Q==,type:str] 3 3 pass: ENC[AES256_GCM,data:q6bhty/EUUYIV+VQ9ZLHNjODOqA=,iv:aJ2+ToXQGLmZtO06ZXBwa6OGt7qil/mSbBG4VI6muRU=,tag:zn4mzLC7+qh40lP07ZEzPQ==,type:str] 4 - protonvpn-torrent: 5 - public-key: ENC[AES256_GCM,data:sCLj4u46lr/ImHyFsgwXcw1UxlTfYYT3W6qKcs8NjISW8t9oNwAPQF71VaE=,iv:6edmr6kB0fIXSFlHaujZnE8Ug3M7n9rXIFQVBvYXwRs=,tag:Qovr/4CLdSL78p+E8G2fiw==,type:str] 6 - private-key: ENC[AES256_GCM,data:cNapKRzpeSJ2c972e8tTRAPwNx0RyHCf39YIUDisAIhYSPN9/zLON5iv4EU=,iv:JlDgAt2nM5YS0xGadfPvRb6c4hs0gX5KgZmBYzhMlfI=,tag:66faJtuinyfZsbVKfcUeFA==,type:str] 7 4 sops: 8 5 age: 9 6 - recipient: age1amaa55e7nusv904a9ucfvtnjlw4srtet42suehey6u3yc4t2xc5sdldepj ··· 24 21 cm43OGNYd1ZnbEM0NjVYY3ZOdi94Sk0Kn8jz57CaoCE3ceFv1TNsYdqW83sqxYiy 25 22 4X21omXCeqpRG5DC2QyAJQE/93lBhsHKIMCraNMaOycPlVQYdyTviA== 26 23 -----END AGE ENCRYPTED FILE----- 27 - lastmodified: "2025-10-09T11:59:47Z" 28 - mac: ENC[AES256_GCM,data:utvW8XNWMuBrtKOfxLFFnXnTM8H/hYFO2pgKnNb+UZF6j36HnfcYb25hUiKLdQ791804LE3mmgR+evtcEl2YFV/8TDfwZTvlVKf6LDJtuYpcrlQYIIWrf6z6EjDwReZOW7my+PmgCNGTZ19mdtgXwSYQOGKX/PyUFEPxTwDCY8A=,iv:oGjRrn9f4aVRnlzIPfa1YthUrXXe7xRntcMjxsheOUI=,tag:p0Dira33uF1Tx2Rx+hiUZQ==,type:str] 24 + lastmodified: "2025-10-25T01:51:34Z" 25 + mac: ENC[AES256_GCM,data:i+ntkoYfBE0BTtGB9KW9YdwPSfBN6yPOWhjb4czccdLjPjlOkGQmJjZRjyzLopSVBcsoV6V+UIgK5Khh2dgemGTyQkx89RNKeQOlanp/XzN2zGb28B31Kfd0QuicjcpijIoXEt5pQ2+x0hQBmsctOdxBDb29ejWDNwHzLn5oWJc=,iv:5VhweSy3s4teDMJw6d4+lozrUYL730wmSJk3MjhLvAA=,tag:gAzDmnNgnLm3LU0SiTn2Og==,type:str] 29 26 unencrypted_suffix: _unencrypted 30 27 version: 3.11.0
+6 -9
secrets/lutea.yaml
··· 2 2 passwordHashes: 3 3 mou: ENC[AES256_GCM,data:DrHL+dy8P4tT+qDXMAxc9d0IuZns5XBAut04eLdkqli9f78i2+7j4B9bmDUSpWePPXLngbTIjrV7S2+WDGlydbN1Uhuez+peRA==,iv:qHXBgvHiRJaWZkWrlT7Es0IfQs/qDfcv6RXoubHQEz0=,tag:XmTAICxNc6odQOjV5NIsNQ==,type:str] 4 4 root: ENC[AES256_GCM,data:nMgGToEB6f0LSQtAcHLwP7DNKv3qFA1NQaPvg7sCxJKW7W34nEtimtghWefbTOyrXsgMSmhZLsDVJ1kgyiet6dfllzB54UuEQg==,iv:bvJJMSI+dVPWB4CwHAH5Hg0DIxtg1xJTlwJVPdxlI8Q=,tag:/Tw4nPnqgsumsz4gxGPkZA==,type:str] 5 - protonvpn-privateKey: ENC[AES256_GCM,data:hxnnEWGNplKbCsBhR9bYFYG9OYImFo//B/dQ9M0khjROhuLKvYS2WxlTBBQ=,iv:2YBQt09Rp37Xu/DZ4lzSHPfLzkNdkJf9BeDpL29dPKk=,tag:BAnZ/pq51Mmmv/lAqXgWvQ==,type:str] 5 + protonvpn-torrent: 6 + private-key: ENC[AES256_GCM,data:jNlSaSrjJN6yvJbnGTxIbysGaU7T6cd70TtD+0KEFYQSOSu8jlFrL/OFxnY=,iv:1K2ecrWevZtVVPVIL80ah965hSIM8IgbWghTpqrJ4dU=,tag:SG4WhFSS3vl9tHc1JR3NZQ==,type:str] 7 + public-key: ENC[AES256_GCM,data:bqplfSvd6Cm5CdQWXOxzQZhWtrvs73c8158IGc04KjuQJfRu7OKnRI8V6RQ=,iv:TxTvfx88FU0a6KHnwoEId3mS9FLyqK3+c8sbkE67334=,tag:oy9cFYir/lV1Yc1pWXhD3w==,type:str] 6 8 sops: 7 - kms: [] 8 - gcp_kms: [] 9 - azure_kv: [] 10 - hc_vault: [] 11 9 age: 12 10 - recipient: age1p55em5e3uk3fprj2mpum7ulrslcqgly63pjsyw2yv6hx99trdsnsvvv9ex 13 11 enc: | ··· 18 16 ZWI2RWEwZllOUDRYV2tCNXZnZFpBS1kKYktM+w+tQbJMcmZBUpuKpeiioChqrWzd 19 17 FU4qWfJw3tEZKdTWECGYaQuCUQm7s+PJBc1HQlxd+eFm8YZMPwoa/Q== 20 18 -----END AGE ENCRYPTED FILE----- 21 - lastmodified: "2025-01-12T07:21:45Z" 22 - mac: ENC[AES256_GCM,data:CxZKQnocEjJYxB8yOkYPh0CWtYY+Fd+fj163ykyz6Hle1RFjlSS8EsG4x/QcxX+6f7rRUCp5eB8LONvdC34iblDy+nCiuwtt1m2ZT5WxKsChiwO18iWW0sPsIMTMykMCcQUE0Z3Ly7Pwt0oilVNuxrTiAeAwK3hMCThZ/EBda1c=,iv:RnRcN2qE+GJ/RhS15Lnv2/VB5daSYD/O3rNABSLuPeA=,tag:TyDGW/6i2wDe1KfEbkiWbg==,type:str] 23 - pgp: [] 19 + lastmodified: "2025-10-25T02:17:34Z" 20 + mac: ENC[AES256_GCM,data:WLH/lMswuhniJ560SxmgbGoIgEY6CWc6Nsm5Fk8hEjk2ME4kXtku3/xrEwOD18U+Q1LyKv+uyan3D6TooLqdb/bLqekoNyB9afwJVOQF5apwGyZYy9GsxkKzLTMj00w7L1Y2j6jsIj74wD/LBy2+ZxcmSxceT3CYdY0dN5NAAOI=,iv:8Yxw793eA0U7mKmc4I70QqqLW0r35lu2Wt0taDIH0tE=,tag:xNZaGnehEdgxPtp3DxMmPg==,type:str] 24 21 unencrypted_suffix: _unencrypted 25 - version: 3.9.2 22 + version: 3.11.0