Nix configurations for my homelab

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.pink 54241a60 d036c7ff

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