diff --git a/flake.nix b/flake.nix index 173b6c6..afdced8 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ imports = [ modules/authelia.nix modules/backup.nix + modules/deluge.nix modules/hledger.nix modules/home-assistant.nix modules/jellyfin.nix @@ -19,6 +20,8 @@ modules/nextcloud-server.nix modules/nginx.nix modules/ssl.nix + modules/tinyproxy.nix + modules/vpn.nix ]; }; diff --git a/modules/deluge.nix b/modules/deluge.nix new file mode 100644 index 0000000..d287f1c --- /dev/null +++ b/modules/deluge.nix @@ -0,0 +1,206 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.shb.deluge; + + fqdn = "${cfg.subdomain}.${cfg.domain}"; +in +{ + options.shb.deluge = { + enable = lib.mkEnableOption "selfhostblocks.deluge"; + + subdomain = lib.mkOption { + type = lib.types.str; + description = "Subdomain under which deluge will be served."; + example = "ha"; + }; + + domain = lib.mkOption { + type = lib.types.str; + description = "domain under which deluge will be served."; + example = "mydomain.com"; + }; + + daemonPort = lib.mkOption { + type = lib.types.int; + description = "Deluge daemon port"; + default = 58846; + }; + + daemonListenPorts = lib.mkOption { + type = lib.types.listOf lib.types.int; + description = "Deluge daemon listen ports"; + default = [ 6881 6889 ]; + }; + + webPort = lib.mkOption { + type = lib.types.int; + description = "Deluge web port"; + default = 8112; + }; + + proxyPort = lib.mkOption { + description = lib.mdDoc "If not null, sets up a deluge to forward all traffic to the Proxy listening at that port."; + type = lib.types.nullOr lib.types.int; + default = null; + }; + + downloadLocation = lib.mkOption { + type = lib.types.str; + description = "Folder where torrents gets downloaded"; + example = "/srv/torrents"; + }; + + oidcEndpoint = lib.mkOption { + type = lib.types.str; + description = "OIDC endpoint for SSO"; + example = "https://authelia.example.com"; + }; + + sopsFile = lib.mkOption { + type = lib.types.path; + description = "Sops file location."; + example = "secrets/torrent.yaml"; + }; + + additionalPlugins = lib.mkOption { + type = lib.types.attrsOf lib.types.path; + description = "Location of additional plugins."; + default = {}; + }; + }; + + config = lib.mkIf cfg.enable { + services.deluge = { + enable = true; + declarative = true; + openFirewall = true; + config = { + download_location = cfg.downloadLocation; + max_upload_speed = -1.0; + allow_remote = true; + daemon_port = cfg.daemonPort; + listen_ports = cfg.daemonListenPorts; + proxy = lib.optionalAttrs (cfg.proxyPort != null) { + force_proxy = true; + hostname = "127.0.0.1"; + port = cfg.proxyPort; + proxy_hostnames = true; + proxy_peer_connections = true; + proxy_tracker_connections = true; + type = 4; # HTTP + }; + }; + authFile = "/run/secrets/deluge/auth"; + + web.enable = true; + web.port = cfg.webPort; + }; + + + systemd.tmpfiles.rules = lib.attrsets.mapAttrsToList (name: path: + "L+ ${config.services.deluge.dataDir}/.config/deluge/plugins/${name} - - - - ${path}" + ) cfg.additionalPlugins; + + sops.secrets."deluge/auth" = { + inherit (cfg) sopsFile; + mode = "0440"; + owner = config.services.deluge.user; + group = config.services.deluge.group; + restartUnits = [ "deluged.service" "delugeweb.service" ]; + }; + + services.nginx = { + enable = true; + + virtualHosts.${fqdn} = { + forceSSL = true; + sslCertificate = "/var/lib/acme/${cfg.domain}/cert.pem"; + sslCertificateKey = "/var/lib/acme/${cfg.domain}/key.pem"; + + # Taken from https://github.com/authelia/authelia/issues/178 + locations."/".extraConfig = '' + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_cache_bypass $http_upgrade; + + auth_request /authelia; + auth_request_set $user $upstream_http_remote_user; + auth_request_set $groups $upstream_http_remote_groups; + proxy_set_header X-Forwarded-User $user; + proxy_set_header X-Forwarded-Groups $groups; + # TODO: Are those needed? + # auth_request_set $name $upstream_http_remote_name; + # auth_request_set $email $upstream_http_remote_email; + # proxy_set_header Remote-Name $name; + # proxy_set_header Remote-Email $email; + # TODO: Would be nice to have this working, I think. + # set $new_cookie $http_cookie; + # if ($http_cookie ~ "(.*)(?:^|;)\s*example\.com\.session\.id=[^;]+(.*)") { + # set $new_cookie $1$2; + # } + # proxy_set_header Cookie $new_cookie; + + auth_request_set $redirect $scheme://$http_host$request_uri; + error_page 401 =302 ${cfg.oidcEndpoint}?rd=$redirect; + error_page 403 = ${cfg.oidcEndpoint}/error/403; + + proxy_pass http://127.0.0.1:${toString config.services.deluge.web.port}; + ''; + + # Virtual endpoint created by nginx to forward auth requests. + locations."/authelia".extraConfig = '' + internal; + proxy_pass ${cfg.oidcEndpoint}/api/verify; + + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Original-URL $scheme://$host$request_uri; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Content-Length ""; + proxy_pass_request_body off; + # TODO: Would be nice to be able to enable this. + # proxy_ssl_verify on; + # proxy_ssl_trusted_certificate "/etc/ssl/certs/DST_Root_CA_X3.pem"; + proxy_ssl_protocols TLSv1.2; + proxy_ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + proxy_ssl_verify_depth 2; + proxy_ssl_server_name on; + ''; + + }; + }; + + shb.authelia.rules = [ + { + domain = fqdn; + policy = "two_factor"; + subject = ["group:deluge_user"]; + } + ]; + + users.groups.deluge = { + members = [ "backup" ]; + }; + + shb.backup.instances.deluge = { + sourceDirectories = [ + config.services.deluge.dataDir + ]; + }; + }; +} diff --git a/modules/tinyproxy.nix b/modules/tinyproxy.nix new file mode 100644 index 0000000..ca400f1 --- /dev/null +++ b/modules/tinyproxy.nix @@ -0,0 +1,147 @@ +# Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances. +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.tinyproxy; + + mkValueStringTinyproxy = with lib; v: + if true == v then "yes" + else if false == v then "no" + else generators.mkValueStringDefault {} v; + + mkKeyValueTinyproxy = { + mkValueString ? mkValueStringDefault {} + }: sep: k: v: + if null == v then "" + else "${lib.strings.escape [sep] k}${sep}${mkValueString v}"; + + settingsFormat = (pkgs.formats.keyValue { + mkKeyValue = mkKeyValueTinyproxy { + mkValueString = mkValueStringTinyproxy; + } " "; + listsAsDuplicateKeys= true; + }); + + configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings; + + someEnabled = any (mapAttrsToList (name: c: c.enable) cfg); +in +{ + options = + let + instanceOption = types.submodule { + options = { + enable = mkEnableOption (lib.mdDoc "Tinyproxy daemon"); + + package = mkPackageOptionMD pkgs "tinyproxy" {}; + + dynamicBindFile = mkOption { + description = lib.mdDoc '' + File holding the IP to bind to. + ''; + default = ""; + }; + + settings = mkOption { + description = lib.mdDoc '' + Configuration for [tinyproxy](https://tinyproxy.github.io/). + ''; + default = { }; + example = literalExpression ''{ + Port 8888; + Listen 127.0.0.1; + Timeout 600; + Allow 127.0.0.1; + Anonymous = ['"Host"' '"Authorization"']; + ReversePath = '"/example/" "http://www.example.com/"'; + }''; + type = types.submodule ({name, ...}: { + freeformType = settingsFormat.type; + options = { + Listen = mkOption { + type = types.str; + default = "127.0.0.1"; + description = lib.mdDoc '' + Specify which address to listen to. + ''; + }; + Port = mkOption { + type = types.int; + default = 8888; + description = lib.mdDoc '' + Specify which port to listen to. + ''; + }; + Anonymous = mkOption { + type = types.listOf types.str; + default = []; + description = lib.mdDoc '' + If an `Anonymous` keyword is present, then anonymous proxying is enabled. The + headers listed with `Anonymous` are allowed through, while all others are denied. + If no Anonymous keyword is present, then all headers are allowed through. You must + include quotes around the headers. + ''; + }; + Filter = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc '' + Tinyproxy supports filtering of web sites based on URLs or domains. This option + specifies the location of the file containing the filter rules, one rule per line. + ''; + }; + }; + }); + }; + }; + }; + in + { + services.tinyproxy = mkOption { + description = "Tinyproxy instances."; + default = {}; + type = types.attrsOf instanceOption; + }; + }; + + config = { + systemd.services = + let + instanceConfig = name: c: mkIf c.enable { + "tinyproxy-${name}" = { + description = "TinyProxy daemon - instance ${name}"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "tinyproxy"; + Group = "tinyproxy"; + Type = "simple"; + ExecStart = "${getExe c.package} -d -c /etc/tinyproxy/${name}.conf"; + ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID"; + KillSignal = "SIGINT"; + TimeoutStopSec = "30s"; + Restart = "on-failure"; + ConfigurationDirectory = "tinyproxy"; + }; + preStart = concatStringsSep "\n" ([ + "cat ${configFile name c} > /etc/tinyproxy/${name}.conf" + ] ++ optionals (c.dynamicBindFile != "") [ + "echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf" + "cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf" + ]); + }; + }; + in + mkMerge (mapAttrsToList instanceConfig cfg); + + users.users.tinyproxy = { + group = "tinyproxy"; + isSystemUser = true; + }; + users.groups.tinyproxy = {}; + }; + + meta.maintainers = with maintainers; [ tcheronneau ]; +} diff --git a/modules/vpn.nix b/modules/vpn.nix new file mode 100644 index 0000000..90ad379 --- /dev/null +++ b/modules/vpn.nix @@ -0,0 +1,268 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.shb.vpn; + + quoteEach = lib.concatMapStrings (x: ''"${x}"''); + + nordvpnConfig = + { name + , dev + , authFile + , remoteServerIP + , dependentServices ? [] + }: '' + client + dev ${dev} + proto tcp + remote ${remoteServerIP} 443 + resolv-retry infinite + remote-random + nobind + tun-mtu 1500 + tun-mtu-extra 32 + mssfix 1450 + persist-key + persist-tun + ping 15 + ping-restart 0 + ping-timer-rem + reneg-sec 0 + comp-lzo no + + remote-cert-tls server + + auth-user-pass ${authFile} + verb 3 + pull + fast-io + cipher AES-256-CBC + auth SHA512 + + script-security 2 + route-noexec + route-up ${routeUp name dependentServices}/bin/routeUp.sh + down ${routeDown name dependentServices}/bin/routeDown.sh + + + -----BEGIN CERTIFICATE----- + MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA5MQswCQYDVQQGEwJQQTEQ + MA4GA1UEChMHTm9yZFZQTjEYMBYGA1UEAxMPTm9yZFZQTiBSb290IENBMB4XDTE2 + MDEwMTAwMDAwMFoXDTM1MTIzMTIzNTk1OVowOTELMAkGA1UEBhMCUEExEDAOBgNV + BAoTB05vcmRWUE4xGDAWBgNVBAMTD05vcmRWUE4gUm9vdCBDQTCCAiIwDQYJKoZI + hvcNAQEBBQADggIPADCCAgoCggIBAMkr/BYhyo0F2upsIMXwC6QvkZps3NN2/eQF + kfQIS1gql0aejsKsEnmY0Kaon8uZCTXPsRH1gQNgg5D2gixdd1mJUvV3dE3y9FJr + XMoDkXdCGBodvKJyU6lcfEVF6/UxHcbBguZK9UtRHS9eJYm3rpL/5huQMCppX7kU + eQ8dpCwd3iKITqwd1ZudDqsWaU0vqzC2H55IyaZ/5/TnCk31Q1UP6BksbbuRcwOV + skEDsm6YoWDnn/IIzGOYnFJRzQH5jTz3j1QBvRIuQuBuvUkfhx1FEwhwZigrcxXu + MP+QgM54kezgziJUaZcOM2zF3lvrwMvXDMfNeIoJABv9ljw969xQ8czQCU5lMVmA + 37ltv5Ec9U5hZuwk/9QO1Z+d/r6Jx0mlurS8gnCAKJgwa3kyZw6e4FZ8mYL4vpRR + hPdvRTWCMJkeB4yBHyhxUmTRgJHm6YR3D6hcFAc9cQcTEl/I60tMdz33G6m0O42s + Qt/+AR3YCY/RusWVBJB/qNS94EtNtj8iaebCQW1jHAhvGmFILVR9lzD0EzWKHkvy + WEjmUVRgCDd6Ne3eFRNS73gdv/C3l5boYySeu4exkEYVxVRn8DhCxs0MnkMHWFK6 + MyzXCCn+JnWFDYPfDKHvpff/kLDobtPBf+Lbch5wQy9quY27xaj0XwLyjOltpiST + LWae/Q4vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG + SIb3DQEBDQUAA4ICAQC9fUL2sZPxIN2mD32VeNySTgZlCEdVmlq471o/bDMP4B8g + nQesFRtXY2ZCjs50Jm73B2LViL9qlREmI6vE5IC8IsRBJSV4ce1WYxyXro5rmVg/ + k6a10rlsbK/eg//GHoJxDdXDOokLUSnxt7gk3QKpX6eCdh67p0PuWm/7WUJQxH2S + DxsT9vB/iZriTIEe/ILoOQF0Aqp7AgNCcLcLAmbxXQkXYCCSB35Vp06u+eTWjG0/ + pyS5V14stGtw+fA0DJp5ZJV4eqJ5LqxMlYvEZ/qKTEdoCeaXv2QEmN6dVqjDoTAo + k0t5u4YRXzEVCfXAC3ocplNdtCA72wjFJcSbfif4BSC8bDACTXtnPC7nD0VndZLp + +RiNLeiENhk0oTC+UVdSc+n2nJOzkCK0vYu0Ads4JGIB7g8IB3z2t9ICmsWrgnhd + NdcOe15BincrGA8avQ1cWXsfIKEjbrnEuEk9b5jel6NfHtPKoHc9mDpRdNPISeVa + wDBM1mJChneHt59Nh8Gah74+TM1jBsw4fhJPvoc7Atcg740JErb904mZfkIEmojC + VPhBHVQ9LHBAdM8qFI2kRK0IynOmAZhexlP/aT/kpEsEPyaZQlnBn3An1CRz8h0S + PApL8PytggYKeQmRhl499+6jLxcZ2IegLfqq41dzIjwHwTMplg+1pKIOVojpWA== + -----END CERTIFICATE----- + + key-direction 1 + + # + # 2048 bit OpenVPN static key + # + -----BEGIN OpenVPN Static key V1----- + e685bdaf659a25a200e2b9e39e51ff03 + 0fc72cf1ce07232bd8b2be5e6c670143 + f51e937e670eee09d4f2ea5a6e4e6996 + 5db852c275351b86fc4ca892d78ae002 + d6f70d029bd79c4d1c26cf14e9588033 + cf639f8a74809f29f72b9d58f9b8f5fe + fc7938eade40e9fed6cb92184abb2cc1 + 0eb1a296df243b251df0643d53724cdb + 5a92a1d6cb817804c4a9319b57d53be5 + 80815bcfcb2df55018cc83fc43bc7ff8 + 2d51f9b88364776ee9d12fc85cc7ea5b + 9741c4f598c485316db066d52db4540e + 212e1518a9bd4828219e24b20d88f598 + a196c9de96012090e333519ae18d3509 + 9427e7b372d348d352dc4c85e18cd4b9 + 3f8a56ddb2e64eb67adfc9b337157ff4 + -----END OpenVPN Static key V1----- + + ''; + + routeUp = name: dependentServices: pkgs.writeShellApplication { + name = "routeUp.sh"; + + runtimeInputs = [ pkgs.iproute2 pkgs.systemd ]; + + text = '' + echo "dev=''${dev:?}" + echo "ifconfig_local=''${ifconfig_local:?}" + echo "route_vpn_gateway=''${route_vpn_gateway:?}" + + ip rule add from "''${ifconfig_local:?}/32" table ${name} + ip rule add to "''${route_vpn_gateway:?}/32" table ${name} + + ip route add default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} + ip route flush cache + + echo "''${ifconfig_local:?}" > /run/openvpn/${name}/ifconfig_local + + dependencies=(${quoteEach dependentServices}) + for i in "''${dependencies[@]}"; do + systemctl restart "$i" + done + ''; + }; + + routeDown = name: dependentServices: pkgs.writeShellApplication { + name = "routeDown.sh"; + + runtimeInputs = [ pkgs.iproute2 pkgs.systemd ]; + + text = '' + echo "dev=''${dev:?}" + echo "ifconfig_local=''${ifconfig_local:?}" + echo "route_vpn_gateway=''${route_vpn_gateway:?}" + + ip rule del from "''${ifconfig_local:?}/32" table ${name} + ip rule del to "''${route_vpn_gateway:?}/32" table ${name} + + ip route del default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} + ip route flush cache + + rm /run/openvpn/${name}/ifconfig_local + + dependencies=(${quoteEach dependentServices}) + for i in "''${dependencies[@]}"; do + systemctl stop "$i" + done + ''; + }; + + someEnabled = lib.any (lib.mapAttrsToList (name: c: c.enable) cfg); +in +{ + options = + let + instanceOption = lib.types.submodule { + options = { + enable = lib.mkEnableOption (lib.mdDoc "OpenVPN config"); + + package = lib.mkPackageOptionMD pkgs "openvpn" {}; + + provider = lib.mkOption { + description = lib.mdDoc "VPN provider, if given uses ready-made configuration."; + type = lib.types.nullOf (lib.types.enum [ "nordvpn" ]); + default = null; + }; + + dev = lib.mkOption { + description = lib.mdDoc "Name of the interface."; + type = lib.types.str; + example = "tun0"; + }; + + remoteServerIP = lib.mkOption { + description = lib.mdDoc "IP of the VPN server to connect to."; + type = lib.types.str; + }; + + sopsFile = lib.mkOption { + description = lib.mdDoc "Location of file holding authentication secrets for provider."; + type = lib.types.anything; + }; + + proxyPort = lib.mkOption { + description = lib.mdDoc "If not null, sets up a proxy that listens on the given port and sends traffic to the VPN."; + type = lib.types.nullOr lib.types.int; + default = null; + }; + }; + }; + in + { + shb.vpn = lib.mkOption { + description = "OpenVPN instances."; + default = {}; + type = lib.types.attrsOf instanceOption; + }; + }; + + config = { + services.openvpn.servers = + let + instanceConfig = name: c: lib.mkIf c.enable { + ${name} = { + autoStart = true; + + up = "mkdir -p /run/openvpn/${name}"; + + config = nordvpnConfig { + inherit name; + inherit (c) dev remoteServerIP; + authFile = config.sops.secrets."${name}/auth".path; + dependentServices = lib.optional (c.proxyPort != null) "tinyproxy-${name}.service"; + }; + }; + }; + in + lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); + + sops.secrets = + let + instanceConfig = name: c: lib.mkIf c.enable { + "${name}/auth" = { + sopsFile = c.sopsFile; + mode = "0440"; + restartUnits = [ "openvpn-${name}" ]; + }; + }; + in + lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); + + networking.iproute2.enable = true; + networking.iproute2.rttablesExtraConfig = + lib.concatStringsSep "\n" (lib.mapAttrsToList (name: c: "10 ${name}") cfg); + + services.tinyproxy = + let + instanceConfig = name: c: lib.mkIf (c.enable && c.proxyPort != null) { + ${name} = { + enable = true; + # package = pkgs.tinyproxy.overrideAttrs (old: { + # withDebug = false; + # patches = old.patches ++ [ + # (pkgs.fetchpatch { + # name = ""; + # url = "https://github.com/tinyproxy/tinyproxy/pull/494/commits/2532ba09896352b31f3538d7819daa1fc3f829f1.patch"; + # sha256 = "sha256-Q0MkHnttW8tH3+hoCt9ACjHjmmZQgF6pC/menIrU0Co="; + # }) + # ]; + # }); + dynamicBindFile = "/run/openvpn/${name}/ifconfig_local"; + settings = { + Port = c.proxyPort; + Listen = "127.0.0.1"; + Syslog = "On"; + LogLevel = "Info"; + Allow = [ "127.0.0.1" "::1" ]; + ViaProxyName = ''"tinyproxy"''; + }; + }; + }; + in + lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); + }; +}