diff --git a/modules/config/instances/config/torrent.nix b/modules/config/instances/config/torrent.nix index ac75a67..61ff528 100755 --- a/modules/config/instances/config/torrent.nix +++ b/modules/config/instances/config/torrent.nix @@ -7,10 +7,10 @@ let mntPath secretPath ; - label = "Rqbit"; - name = "rqbit"; - subdomain = "share"; - domain = "${subdomain}.${domain0}"; + label = "qBittorrent"; + name = "qbittorrent"; + short = "share"; + domain = "${short}.${domain0}"; secrets = "${secretPath}/${name}"; ssl = "${sslPath}/${domain}"; in @@ -21,10 +21,9 @@ in domains = { url0 = domain; }; - subdomain = name; + subdomain = short; tags = [ name - "rqbit" "torrent" "p2p" ]; @@ -32,9 +31,9 @@ in port0 = 3030; }; interface = { - id = "vm-${name}"; + id = "vm-${short}"; mac = "02:00:00:00:56:07"; - idUser = "vmuser-${name}"; + idUser = "vmuser-${short}"; macUser = "02:00:00:00:00:07"; ip = "192.168.50.117"; gate = "192.168.50.1"; @@ -46,7 +45,7 @@ in key = "${ssl}/key.pem"; }; varPaths = { - path0 = "${varPath}/${name}"; + path0 = "${varPath}/${label}"; }; mntPaths = { path0 = "${mntPath}/${name}"; diff --git a/modules/nixos/guests/jellyfin/default.nix b/modules/nixos/guests/jellyfin/default.nix index da9c2ae..5115be3 100755 --- a/modules/nixos/guests/jellyfin/default.nix +++ b/modules/nixos/guests/jellyfin/default.nix @@ -83,7 +83,7 @@ in microvm = { vcpu = 1; - mem = 1024; + mem = 1024 * 3; hypervisor = "qemu"; interfaces = [ { diff --git a/modules/nixos/guests/torrent/default.nix b/modules/nixos/guests/torrent/default.nix index 1663ecb..ffd1a4c 100755 --- a/modules/nixos/guests/torrent/default.nix +++ b/modules/nixos/guests/torrent/default.nix @@ -1,6 +1,7 @@ { config, flake, + pkgs, ... }: let @@ -10,30 +11,94 @@ let host = instances.torrent.domains.url0; dns0 = instances.web.dns.provider0; dns0Path = "dns/${dns0}"; - + torrentPort = 2049; in { - microvm.vms.${serviceCfg.name} = { autostart = true; config = { system.stateVersion = "25.05"; - networking.firewall.allowedTCPPorts = [ - 22 - serviceCfg.ports.port0 - ]; - imports = [ - ./rqbit.nix - ]; + networking = { + wg-quick.interfaces = { + wg0 = { + address = [ + "172.22.116.126/32" + "fd00:4956:504e:ffff::ac16:747e/128" + ]; + dns = [ "172.16.0.1" ]; + privateKeyFile = "/run/secrets/wireguard-pass"; + peers = [ + { + publicKey = "hku9gjamhoii8OvxZgx+TdUDIkOAQYFu39qbav2AyUQ="; + endpoint = "89.187.181.116:${builtins.toString torrentPort}"; + allowedIPs = [ + "0.0.0.0/0" + "::/0" + ]; + persistentKeepalive = 25; + } + ]; + }; + }; + firewall = { + enable = true; + allowedTCPPorts = [ + 22 + torrentPort + serviceCfg.ports.port0 + ]; + allowedUDPPorts = [ + torrentPort + ]; + }; + dhcpcd.enable = false; + useNetworkd = true; + }; + + # imports = [ + # ./rqbit.nix + # ]; services = { - rqbit = { + qbittorrent = { enable = true; - dataDir = "/var/lib/rqbit/downloads"; - listenAddress = "0.0.0.0"; + webuiPort = serviceCfg.ports.port0; + torrentingPort = torrentPort; openFirewall = true; - extraArgs = [ ]; + serverConfig = { + LegalNotice.Accepted = true; + + BitTorrent = { + Session = { + Interface = "wg0"; + InterfaceName = "wg0"; + MaxConnections = -1; + Port = torrentPort; + MaxConnectionsPerTorrent = -1; + MaxUploads = -1; + MaxUploadsPerTorrent = -1; + }; + }; + + Preferences = { + WebUI = { + Username = "user"; + # generate new passwords with this: + # https://codeberg.org/feathecutie/qbittorrent_password + Password_PBKDF2 = "@ByteArray(1bJKXLVSLU6kgCHbCS2lDg==:BmyrMaod6dbJqEe7Ud/JgKAxRMqzsAuEjHcTvLzIBgc5rc5Z7J2X9mbH0cDEAhXqc+O3gQxrckt8S2Gf+zlO9w==)"; + }; + + General = { + Locale = "en"; + }; + Downloads = { + SavePath = "${serviceCfg.varPaths.path0}/downloads"; + TempPathEnabled = false; + PreAllocation = false; + }; + }; + }; }; openssh = { @@ -48,21 +113,28 @@ in network = { enable = true; networks."10-enp" = { - matchConfig.Name = "enp0s4"; + matchConfig.Name = "enp0s5"; addresses = [ { Address = "${serviceCfg.interface.ip}/24"; } ]; gateway = [ serviceCfg.interface.gate ]; }; }; + tmpfiles.rules = [ "d ${serviceCfg.varPaths.path0} 755 ${serviceCfg.name} ${serviceCfg.name} -" "d ${serviceCfg.varPaths.path0}/downloads 755 ${serviceCfg.name} ${serviceCfg.name} -" ]; + + services.qbittorrent = { + after = [ "wg-quick-wg0.service" ]; + requires = [ "wg-quick-wg0.service" ]; + }; }; microvm = { - vcpu = 4; - mem = 1024 * 4; + vcpu = 1; + mem = 1024 * 1; hypervisor = "qemu"; + interfaces = [ { type = "tap"; @@ -75,6 +147,7 @@ in mac = serviceCfg.interface.macUser; } ]; + forwardPorts = [ { from = "host"; @@ -91,20 +164,24 @@ in proto = "virtiofs"; } { - mountPoint = "/var/lib/${serviceCfg.name}"; + mountPoint = serviceCfg.varPaths.path0; proto = "virtiofs"; source = serviceCfg.mntPaths.path0; tag = "${serviceCfg.name}_data"; } - # { - # mountPoint = "/run/secrets"; - # proto = "virtiofs"; - # source = "/run/secrets/${serviceCfg.name}"; - # tag = "host_secrets"; - # } + { + mountPoint = "/run/secrets"; + proto = "virtiofs"; + source = "/run/secrets/proton"; + tag = "host_secrets"; + } ]; }; - environment.systemPackages = [ ]; + + environment.systemPackages = with pkgs; [ + wireguard-tools + speedtest-go + ]; }; }; @@ -116,7 +193,6 @@ in basic_auth { {$CADDY_AUTH_USER} {$CADDY_AUTH_PASSWORD_HASH} } - reverse_proxy ${serviceCfg.interface.ip}:${toString serviceCfg.ports.port0} tls ${serviceCfg.ssl.cert} ${serviceCfg.ssl.key} @@ -129,11 +205,15 @@ in }; sops.secrets = { - "caddy/${serviceCfg.name}-auth" = { + "caddy/share-auth" = { owner = "caddy"; group = "caddy"; mode = "0400"; }; + "proton/wireguard-pass" = { + owner = "root"; + mode = "0400"; + }; }; security.acme.certs."${host}" = { @@ -147,15 +227,13 @@ in systemd = { services.caddy = { serviceConfig = { - EnvironmentFile = config.sops.secrets."caddy/${serviceCfg.name}-auth".path; + EnvironmentFile = config.sops.secrets."caddy/share-auth".path; }; }; - tmpfiles.rules = [ "d ${serviceCfg.mntPaths.path0} 0755 microvm wheel - -" "d ${serviceCfg.secretPaths.path0}/caddy 755 caddy caddy -" "d /var/log/caddy 755 caddy caddy -" - ]; }; } diff --git a/modules/nixos/guests/torrent/rqbit.nix b/modules/nixos/guests/torrent/rqbit.nix index d1fbd8b..897e8bb 100755 --- a/modules/nixos/guests/torrent/rqbit.nix +++ b/modules/nixos/guests/torrent/rqbit.nix @@ -4,9 +4,7 @@ pkgs, ... }: - with lib; - let cfg = config.services.rqbit; in @@ -27,24 +25,221 @@ in description = "Directory to store downloaded torrents."; }; - listenAddress = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "IP address to listen on for the web UI and API."; + # HTTP API Configuration + httpApi = { + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "IP address to listen on for the web UI and API."; + }; + + listenPort = mkOption { + type = types.port; + default = 3030; + description = "Port for the web UI and API."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open the firewall for the web UI port."; + }; }; - listenPort = mkOption { - type = types.port; - default = 3030; - description = "Port for the web UI and API."; + # BitTorrent TCP Configuration + tcp = { + minPort = mkOption { + type = types.port; + default = 4240; + description = "Minimum port for incoming BitTorrent connections."; + }; + + maxPort = mkOption { + type = types.port; + default = 4260; + description = "Maximum port for incoming BitTorrent connections."; + }; + + disable = mkOption { + type = types.bool; + default = false; + description = "Disable listening for incoming TCP connections."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open firewall ports for incoming BitTorrent connections."; + }; }; - openFirewall = mkOption { - type = types.bool; - default = false; - description = "Open the firewall for the web UI port."; + # DHT Configuration + dht = { + disable = mkOption { + type = types.bool; + default = false; + description = "Disable DHT (Distributed Hash Table) for peer discovery."; + }; + + disablePersistence = mkOption { + type = types.bool; + default = false; + description = "Disable DHT state persistence (useful for multiple instances)."; + }; }; + # UPnP Configuration + upnp = { + disablePortForward = mkOption { + type = types.bool; + default = false; + description = "Disable UPnP port forwarding."; + }; + + enableServer = mkOption { + type = types.bool; + default = false; + description = "Enable UPnP Media Server to stream torrents."; + }; + + serverFriendlyName = mkOption { + type = types.nullOr types.str; + default = null; + description = "Friendly name for the UPnP server."; + example = "rqbit Media Server"; + }; + }; + + # Rate Limiting + rateLimit = { + download = mkOption { + type = types.nullOr types.int; + default = null; + description = "Download rate limit in bytes per second."; + example = 1048576; # 1 MB/s + }; + + upload = mkOption { + type = types.nullOr types.int; + default = null; + description = "Upload rate limit in bytes per second."; + example = 524288; # 512 KB/s + }; + }; + + # Logging Configuration + logging = { + level = mkOption { + type = types.nullOr ( + types.enum [ + "trace" + "debug" + "info" + "warn" + "error" + ] + ); + default = null; + description = "Console log level."; + }; + + file = mkOption { + type = types.nullOr types.path; + default = null; + description = "Log file path (in addition to console logging)."; + example = "/var/log/rqbit/rqbit.log"; + }; + + fileRustLog = mkOption { + type = types.str; + default = "librqbit=debug,info"; + description = "RUST_LOG value for the log file."; + }; + }; + + # Performance Configuration + performance = { + workerThreads = mkOption { + type = types.nullOr types.ints.positive; + default = null; + description = "Number of worker threads for the executor."; + }; + + maxBlockingThreads = mkOption { + type = types.ints.positive; + default = 8; + description = "Maximum blocking threads for disk I/O operations."; + }; + + singleThreadRuntime = mkOption { + type = types.bool; + default = false; + description = "Use tokio's single-threaded runtime (for debugging)."; + }; + + concurrentInitLimit = mkOption { + type = types.ints.positive; + default = 5; + description = "Maximum number of torrents that can initialize simultaneously."; + }; + }; + + # Peer Configuration + peer = { + connectTimeout = mkOption { + type = types.str; + default = "2s"; + description = "Peer connection timeout."; + example = "1.5s"; + }; + + readWriteTimeout = mkOption { + type = types.str; + default = "10s"; + description = "Peer read/write timeout."; + example = "5s"; + }; + }; + + # Tracker Configuration + tracker = { + refreshInterval = mkOption { + type = types.nullOr types.str; + default = null; + description = "Force a specific tracker refresh interval."; + example = "30s"; + }; + + trackersFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "File with tracker URLs to use for all torrents."; + }; + }; + + # Advanced Options + socksProxy = mkOption { + type = types.nullOr types.str; + default = null; + description = "SOCKS5 proxy URL."; + example = "socks5://user:pass@localhost:1080"; + }; + + blocklistUrl = mkOption { + type = types.nullOr types.str; + default = null; + description = "URL to download a P2P blocklist from."; + example = "https://example.com/blocklist.txt"; + }; + + umask = mkOption { + type = types.nullOr types.str; + default = null; + description = "Set the process umask for file creation permissions."; + example = "022"; + }; + + # User/Group Configuration user = mkOption { type = types.str; default = "rqbit"; @@ -61,7 +256,7 @@ in type = types.listOf types.str; default = [ ]; description = "Extra command-line arguments to pass to rqbit."; - example = literalExpression ''[ "--upnp" "--enable-upnp-server" ]''; + example = literalExpression ''[ "--experimental-mmap-storage" ]''; }; }; @@ -71,29 +266,73 @@ in after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; + preStart = mkIf (cfg.logging.file != null) '' + mkdir -p $(dirname ${cfg.logging.file}) + chown ${cfg.user}:${cfg.group} $(dirname ${cfg.logging.file}) + ''; + serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; - Environment = [ - "XDG_CACHE_HOME=/var/lib/rqbit/.cache" - "XDG_DATA_HOME=/var/lib/rqbit/.local/share" + "XDG_CACHE_HOME=${cfg.dataDir}/.cache" + "XDG_DATA_HOME=${cfg.dataDir}/.local/share" ]; - - ExecStart = '' - ${cfg.package}/bin/rqbit \ - --http-api-listen-addr ${cfg.listenAddress}:${toString cfg.listenPort} \ - ${concatStringsSep " " cfg.extraArgs} \ - server start ${cfg.dataDir} - ''; + ExecStart = + let + args = [ + "${cfg.package}/bin/rqbit" + "--http-api-listen-addr ${cfg.httpApi.listenAddress}:${toString cfg.httpApi.listenPort}" + ] + ++ optional (cfg.logging.level != null) "-v ${cfg.logging.level}" + ++ optional (cfg.logging.file != null) "--log-file ${cfg.logging.file}" + ++ optional (cfg.logging.file != null) "--log-file-rust-log ${cfg.logging.fileRustLog}" + ++ optional (cfg.tracker.refreshInterval != null) "-i ${cfg.tracker.refreshInterval}" + ++ optional cfg.performance.singleThreadRuntime "-s" + ++ optional cfg.dht.disable "--disable-dht" + ++ optional cfg.dht.disablePersistence "--disable-dht-persistence" + ++ optional (cfg.peer.connectTimeout != "2s") "--peer-connect-timeout ${cfg.peer.connectTimeout}" + ++ optional ( + cfg.peer.readWriteTimeout != "10s" + ) "--peer-read-write-timeout ${cfg.peer.readWriteTimeout}" + ++ optional (cfg.performance.workerThreads != null) "-t ${toString cfg.performance.workerThreads}" + ++ optional cfg.tcp.disable "--disable-tcp-listen" + ++ optional (cfg.tcp.minPort != 4240) "--tcp-min-port ${toString cfg.tcp.minPort}" + ++ optional (cfg.tcp.maxPort != 4260) "--tcp-max-port ${toString cfg.tcp.maxPort}" + ++ optional cfg.upnp.disablePortForward "--disable-upnp-port-forward" + ++ optional cfg.upnp.enableServer "--enable-upnp-server" + ++ optional ( + cfg.upnp.serverFriendlyName != null + ) "--upnp-server-friendly-name '${cfg.upnp.serverFriendlyName}'" + ++ optional ( + cfg.performance.maxBlockingThreads != 8 + ) "--max-blocking-threads ${toString cfg.performance.maxBlockingThreads}" + ++ optional (cfg.socksProxy != null) "--socks-url ${cfg.socksProxy}" + ++ optional ( + cfg.performance.concurrentInitLimit != 5 + ) "--concurrent-init-limit ${toString cfg.performance.concurrentInitLimit}" + ++ optional (cfg.umask != null) "--umask ${cfg.umask}" + ++ optional ( + cfg.rateLimit.download != null + ) "--ratelimit-download ${toString cfg.rateLimit.download}" + ++ optional (cfg.rateLimit.upload != null) "--ratelimit-upload ${toString cfg.rateLimit.upload}" + ++ optional (cfg.blocklistUrl != null) "--blocklist-url ${cfg.blocklistUrl}" + ++ optional (cfg.tracker.trackersFile != null) "--trackers-filename ${cfg.tracker.trackersFile}" + ++ cfg.extraArgs + ++ [ + "server" + "start" + cfg.dataDir + ]; + in + concatStringsSep " " args; Restart = "on-failure"; - StateDirectory = "rqbit"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; - ReadWritePaths = [ cfg.dataDir ]; + ReadWritePaths = [ cfg.dataDir ] ++ optional (cfg.logging.file != null) (dirOf cfg.logging.file); }; }; @@ -109,8 +348,12 @@ in rqbit = { }; }; - networking.firewall = mkIf cfg.openFirewall { - allowedTCPPorts = [ cfg.listenPort ]; + networking.firewall = mkIf (cfg.httpApi.openFirewall || cfg.tcp.openFirewall) { + allowedTCPPorts = optional cfg.httpApi.openFirewall cfg.httpApi.listenPort; + allowedTCPPortRanges = optional cfg.tcp.openFirewall { + from = cfg.tcp.minPort; + to = cfg.tcp.maxPort; + }; }; }; } diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index 9ca0160..dc90627 100755 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -47,13 +47,15 @@ glance-jellyfin: ENC[AES256_GCM,data:ozdDKgAWkA88J2j8RtiOP/aQPAt/neUOSlAZF20g510 opencloud: projectenv: ENC[AES256_GCM,data:+XCd3xScfxCN1Zl5L+4RAOjpmMPhVLSBtqH2nkEUpXhssy5EU82qAanNmqwiIJ1VrYXYovuu3XOwRKY3Ub1nsR5h1S0KUCwav2zmFKVopxF/5jVNIk6qR8Ggz/fAa1YQSW+SAnrtRGvP0Q1SERlCgnH4isVxNvWPyWCZKIgiX2Enu7hVwsJXKLYDomRWt47zzXNUzw50aFn7xPtXE/AYbMPBa+FweCrCfkaQ6i6jPvkdc6VBYTqIanD0908wB2SJA+1xvY7bYgRVB17/4a/9DuUN5J4xU84TOW7EFkvC/hWhlhC58GqQrOFyAgTP4YJHKGbLVKPlc4fcNMh5+pENpPG2fRDElCaLoJcYe6sYhaCDSegpDR/U9bgzKirnCu/hmdG+NQ3sGK/C89JL2kZT+tVT1u5JWnKGOGvLGQm73QUmnssDZVd8ubNsnd57W7siqAXY3+DN46yLrGgmTfHTRi4x2DKF8VCD9jXOxWsyoLvKYDyz09H9dI72xlCtSmcrFAt7bY7uEAWutrPCf3Kh/gq6oFUAPBEwfqhgnpgGA1vyA6o4zhxl4Rqye5YZMx2uNkxdA4wmk9KB/e7BVR/P04TSXoAV931OX7bnlw3XjSw5NTPEPnpmwZ3VPRGGkz171RiQQp+CkwUr35+DdwFrGazuv3wlwAhM19h9SRn8jikrw6PPGVehYp8mB/FhpNgqV0nM2DfjaBqE3yMfDzXH5b92t4Q=,iv:6mlHq6yh03x/FbZNu+A9QBoV6ALX1rRWuL13ItJWriI=,tag:tK6Ek2fzgPPWT8WCeU1Frw==,type:str] caddy: - rqbit-auth: ENC[AES256_GCM,data:4fLeeykasmPDtUC5wFRqWEVxRa4ntaiDU2v3AUBdUWxiRQ9EDEk3S1lWpwBtMlan3bU/QsIGVIh/hhWmWD2WdkcaKJxvPGtb9OSJ3wtxCTlI2EsWS1h3BQY4Z1Jf5VVDLR+/dj3mZNv7cu1D,iv:+fIzvbXsE7EoSrfsD1ZzKMmF1BazHre7yXQbjyKdDks=,tag:d/wFZf8szahaU9LqM7lr/g==,type:str] + share-auth: ENC[AES256_GCM,data:3jY2B2GOdz5EPJeAyVsk4XCs5NMft3VquIBep7SxYtEZ9H7IDroq1U1Sch6YVQ7VcL85L4Ix/OVPm4jVDEA0sZiGkltbYXRXZ8CR34ifsHtHR35lgjXyj8ZhJLydw7LgmZCEztWO8GjLdvSY,iv:MT5sA32Djx81HGc36rqV2xS5KUHLAeTyZiOdSu8oqQY=,tag:V1dv4yS2RXf4Xqrl5+tEuA==,type:str] comfyui-auth: ENC[AES256_GCM,data:7VTXoRxnD0NyVCFRAjHaZswEUsFuQd/ZIwVfqGPmNNV87hn6CBYWvxvcPPFwe+uw7BmKMt+I66DyKx5ydYENTWxPocyT/rFdgdtWwNoenj+JwsUzegmMbEiH2HCZdiwKj0h1lo142mtA6zkc,iv:xT5XHCj8D4dyvglstE2oqo92fLdscCkaNMux43hJ7nQ=,tag:HgU9wAmjPvfoDXgnorB5yA==,type:str] wifi-home: ENC[AES256_GCM,data:5NYSCUyalDf7gZF7WaRQJCo=,iv:RkVZKsmVEBg5M28DSkBD41673iLM+dqDAAhSwjqejck=,tag:QQ17VSWOnU0bGglZq6455Q==,type:str] firefly-iii: pass: ENC[AES256_GCM,data:WjHcoTuEzEq9pfw4QoqRjI4jhu5VPEMOXlHL0olg9dqUj4EGa1Shv5T/kIxdRFuao0y3zQ==,iv:4/fmFOxxDLzplsNGpSJMQOeoNviZw2c2pFlB1ZkRu+o=,tag:7TQ2q/kEFDU4tZxPx53ebw==,type:str] data: ENC[AES256_GCM,data:921LhcRTWVk24eEAQoDMV+RllSP3PbSXCCIDXlQA80Mq,iv:YXEgas77DgdyPTnBZa/ySjcERBIwmdDZJbijeNKNF24=,tag:Wj25wA7tLJ2bZ/faG9DUhg==,type:str] smtp: ENC[AES256_GCM,data:+e4MiRZ2WOZyWYpMf+By1Eb45ih4TA+svLI2+00yQk82,iv:+52+kJouMwkOSDEaOCA8V80+wT/VzNxgtCkOO68SCdk=,tag:YrtrJAXIhQpsUTEeYvrVwQ==,type:str] +proton: + wireguard-pass: ENC[AES256_GCM,data:2SuLDMkJsOa/EjoYq0ZYp8A6yAO9KSaMJ35ofMXQDWhPcpL7/2eld83by021,iv:g5zGOdl2pGu3rsQcnwgRCHqDdzYNVkNhhfl3YMieBUY=,tag:FzbkDA4FyHrcC09EFZDoKw==,type:str] backblaze: env: ENC[AES256_GCM,data:cdOYt77KocuGB3aqYz13oBokoLkEIgI1AW+cYC5uutgZYujG3PqoLEh6Gvbpzn3O+0OWg1/4UAYr4f2v7oCsgwFzPWS3HrhqC5+kIBjrPCyAnxDxlu2xaQ9hR+ogFh5UTDo=,iv:6+jx4Dj5CNV72DAss6NNYm44f9gSHco/EUBvL2o2CNI=,tag:6/cx84MgTDqQJxu/zINEeA==,type:str] repo: ENC[AES256_GCM,data:sRae9XELIfkWPaXelCdgEXIDbLTHVqGcRO0o+WA9aBfB8MUw92JjRCYgMgGXT0Apy38eszyuEHFB3XPpRmtQ7g==,iv:EilVA9zdHm6B9pTIhNxyj6Th1248nXvh0kpnEqZJ5HI=,tag:q9ASAgx5vgY0IePws4rT5Q==,type:str] @@ -70,7 +72,7 @@ sops: bXBOa1VSakoyaWxpODJEOU11QUZCaUEK8Ch9Ten3DdrPHF1DTH2qei85AlHUOaLD aNfzakake7ej+MxJYdKEU0bcWofNMKzIlZa2uM10KZSENDP8d8qlig== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-11-25T10:47:46Z" - mac: ENC[AES256_GCM,data:JF264gi1QgOcPk9GVzFeWrPa9T5AuHxuYQTVLbpSoGnqJEwQCvmsYKe1guqlNRlxm11VDGSa3DB1++0fpd4Vn7ngTxQuLCkCbnBo3fU+SDxe3gYt4c21pT3qabbcIuCGTqf6fy32ZWi8BabcFN1hN+LWuSox40PugnuU5SgZdXs=,iv:dbQb4OITPZozKyprRUV9df22zu+XwAr2S28K4A4Ki5I=,tag:alTMCTyKq/Cxn21h06kkGQ==,type:str] + lastmodified: "2025-11-26T08:19:12Z" + mac: ENC[AES256_GCM,data:eqsiEBdTva+rjC64+n/RhVqWT7q4QUfARAvqZCezVCAvoTFJ+qJO2PWrJ39H/axLcLTwq7xC36UYkN8ckvAUSLx8pZyyuHVeI+4irL16AIpJM8z14kYG6dyw0ujVI+JZpM57FHWXkwX6JtzbqN7sv+u/YnM6ubfmporykvlQOmQ=,iv:eYhCL11iv3B6jsGieJ7nosKcd6MUH1QBWjJptgjBhiQ=,tag:7W+bhwOHM7e0zCP7eP0jxQ==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0