feat[server]: also proxy roundcube

This commit is contained in:
Leon Schwarzäugl 2025-12-23 01:52:26 +01:00 committed by Leon Schwarzäugl
parent 1502faf345
commit b06c19c52b
24 changed files with 436 additions and 216 deletions

View file

@ -46,6 +46,14 @@ creation_rules:
age: age:
- *pyramid - *pyramid
- path_regex: secrets/nginx/acme.json
key_groups:
- pgp:
- *swarsel
age:
- *twothreetunnel
- *eagleland
- path_regex: hosts/nixos/x86_64-linux/pyramid/secrets/[^/]+\.(yaml|json|env|ini|enc)$ - path_regex: hosts/nixos/x86_64-linux/pyramid/secrets/[^/]+\.(yaml|json|env|ini|enc)$
key_groups: key_groups:
- pgp: - pgp:

View file

@ -2858,37 +2858,36 @@ This is my main server that I run at home. It handles most tasks that require bi
swarselmodules.server = { swarselmodules.server = {
diskEncryption = lib.mkForce false; diskEncryption = lib.mkForce false;
wireguard = lib.mkDefault true; nginx = true; # for php stuff
nfs = lib.mkDefault true; acme = false; # cert handled by proxy
nginx = lib.mkDefault true; wireguard = true;
kavita = lib.mkDefault true;
restic = lib.mkDefault true; nfs = true;
jellyfin = lib.mkDefault true; kavita = true;
navidrome = lib.mkDefault true; restic = true;
spotifyd = lib.mkDefault true; jellyfin = true;
mpd = lib.mkDefault true; navidrome = true;
postgresql = lib.mkDefault true; spotifyd = true;
matrix = lib.mkDefault true; mpd = true;
nextcloud = lib.mkDefault true; postgresql = true;
immich = lib.mkDefault true; matrix = true;
paperless = lib.mkDefault true; nextcloud = true;
transmission = lib.mkDefault true; immich = true;
syncthing = lib.mkDefault true; paperless = true;
grafana = lib.mkDefault true; transmission = true;
emacs = lib.mkDefault true; syncthing = true;
freshrss = lib.mkDefault true; grafana = true;
jenkins = lib.mkDefault false; emacs = true;
kanidm = lib.mkDefault true; freshrss = true;
firefly-iii = lib.mkDefault true; kanidm = true;
koillection = lib.mkDefault true; firefly-iii = true;
radicale = lib.mkDefault true; koillection = true;
atuin = lib.mkDefault true; radicale = true;
forgejo = lib.mkDefault true; atuin = true;
ankisync = lib.mkDefault true; forgejo = true;
# snipeit = lib.mkDefault false; ankisync = true;
homebox = lib.mkDefault true; homebox = true;
opkssh = lib.mkDefault true; opkssh = true;
garage = lib.mkDefault false;
}; };
} }
@ -3001,10 +3000,6 @@ This is my main server that I run at home. It handles most tasks that require bi
server = true; server = true;
}; };
swarselmodules.server = {
nginx = lib.mkForce false;
};
microvm.vms = microvm.vms =
let let
mkMicrovm = guestName: { mkMicrovm = guestName: {
@ -3296,7 +3291,6 @@ This is my main server that I run at home. It handles most tasks that require bi
swarselmodules = { swarselmodules = {
server = { server = {
nginx = lib.mkForce false; # we get this from the server profile
wireguard = true; wireguard = true;
}; };
}; };
@ -3914,7 +3908,6 @@ This machine mainly acts as my proxy server to stand before my local machines.
topology.self = { topology.self = {
icon = "devices.cloud-server"; icon = "devices.cloud-server";
}; };
swarselmodules.server.nginx = false;
swarselsystems = { swarselsystems = {
flakePath = "/root/.dotfiles"; flakePath = "/root/.dotfiles";
@ -3963,7 +3956,7 @@ This machine mainly acts as my proxy server to stand before my local machines.
postgresql = true; postgresql = true;
attic = true; attic = true;
garage = true; garage = true;
hydra = true; hydra = false;
dns-hostrecord = true; dns-hostrecord = true;
}; };
@ -4144,8 +4137,6 @@ This machine mainly acts as my proxy server to stand before my local machines.
topology.self = { topology.self = {
icon = "devices.cloud-server"; icon = "devices.cloud-server";
}; };
swarselmodules.server.nginx = false;
swarselsystems = { swarselsystems = {
flakePath = "/root/.dotfiles"; flakePath = "/root/.dotfiles";
@ -4168,7 +4159,6 @@ This machine mainly acts as my proxy server to stand before my local machines.
swarselmodules.server = { swarselmodules.server = {
nsd = true; nsd = true;
nginx = false;
dns-hostrecord = true; dns-hostrecord = true;
}; };
} }
@ -4370,7 +4360,6 @@ This machine mainly acts as my proxy server to stand before my local machines.
}; };
swarselmodules.server = { swarselmodules.server = {
nginx = false;
bastion = true; bastion = true;
dns-hostrecord = true; dns-hostrecord = true;
# ssh = false; # ssh = false;
@ -4578,7 +4567,7 @@ This machine mainly acts as my proxy server to stand before my local machines.
"moonside" "moonside"
"winters" "winters"
"belchsfactory" "belchsfactory"
# "eagleland" "eagleland"
]; ];
}; };
}; };
@ -4590,8 +4579,8 @@ This machine mainly acts as my proxy server to stand before my local machines.
}; };
swarselmodules.server = { swarselmodules.server = {
nginx = true; # for now nginx = true;
oauth2-proxy = true; # for now oauth2-proxy = true;
dns-hostrecord = true; dns-hostrecord = true;
wireguard = true; wireguard = true;
}; };
@ -4756,6 +4745,7 @@ This machine mainly acts as my proxy server to stand before my local machines.
:END: :END:
***** Main Configuration ***** Main Configuration
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: h:96540b9c-1610-45f2-ba19-916051ab5e10 :CUSTOM_ID: h:96540b9c-1610-45f2-ba19-916051ab5e10
:END: :END:
@ -4789,7 +4779,15 @@ This machine mainly acts as my proxy server to stand before my local machines.
isBtrfs = true; isBtrfs = true;
isNixos = true; isNixos = true;
isLinux = true; isLinux = true;
proxyHost = "eagleland"; proxyHost = "twothreetunnel"; # mail shall not be proxied through twothreetunnel
server = {
wireguard.interfaces = {
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
}; };
} // lib.optionalAttrs (!minimal) { } // lib.optionalAttrs (!minimal) {
@ -4797,6 +4795,8 @@ This machine mainly acts as my proxy server to stand before my local machines.
mailserver = true; mailserver = true;
dns-hostrecord = true; dns-hostrecord = true;
postgresql = true; postgresql = true;
nginx = true;
wireguard = true;
}; };
swarselprofiles = { swarselprofiles = {
@ -6271,6 +6271,7 @@ A breakdown of the flags being set:
additions = final: _: import "${self}/pkgs/config" { additions = final: _: import "${self}/pkgs/config" {
inherit self config lib; inherit self config lib;
pkgs = final; pkgs = final;
nixosConfig = config;
homeConfig = config.home-manager.users.${config.swarselsystems.mainUser}; homeConfig = config.home-manager.users.${config.swarselsystems.mainUser};
}; };
in in
@ -8440,7 +8441,7 @@ Here we just define some aliases for rebuilding the system, and we allow some in
} }
#+end_src #+end_src
**** System Packages **** System Packages (Server Programs)
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: h:6f2967d9-7e32-4605-bb5c-5e27770bec0f :CUSTOM_ID: h:6f2967d9-7e32-4605-bb5c-5e27770bec0f
:END: :END:
@ -8462,6 +8463,9 @@ Here we just define some aliases for rebuilding the system, and we allow some in
swarsel-deploy swarsel-deploy
tmux tmux
busybox busybox
attic-client
swarsel-gens
swarsel-switch
]; ];
}; };
} }
@ -8530,16 +8534,64 @@ Here we just define some aliases for rebuilding the system, and we allow some in
} }
#+end_src #+end_src
**** acme
#+begin_src nix-ts :tangle modules/nixos/server/acme.nix
{ self, pkgs, lib, config, globals, ... }:
let
inherit (config.repo.secrets.common) dnsProvider dnsBase dnsMail;
sopsFile = self + "/secrets/nginx/acme.json";
in
{
options.swarselmodules.server.acme = lib.mkEnableOption "enable acme on server";
config = lib.mkIf config.swarselmodules.server.acme {
environment.systemPackages = with pkgs; [
lego
];
sops = {
secrets = {
acme-creds = { format = "json"; key = ""; group = "acme"; inherit sopsFile; mode = "0660"; };
};
templates."certs.secret".content = ''
ACME_DNS_API_BASE = ${dnsBase}
ACME_DNS_STORAGE_PATH=${config.sops.secrets.acme-creds.path}
'';
};
users.groups.acme.members = lib.mkIf config.swarselmodules.server.nginx [ "nginx" ];
security.acme = {
acceptTerms = true;
defaults = {
inherit dnsProvider;
email = dnsMail;
environmentFile = "${config.sops.templates."certs.secret".path}";
reloadServices = [ "nginx" ];
dnsPropagationCheck = true;
};
certs."${globals.domains.main}" = {
domain = "*.${globals.domains.main}";
};
};
environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence {
directories = [{ directory = "/var/lib/acme"; }];
};
};
}
#+end_src
**** NGINX **** NGINX
:PROPERTIES: :PROPERTIES:
:CUSTOM_ID: h:302468d2-106a-41c8-b2bc-9fdc40064a9c :CUSTOM_ID: h:302468d2-106a-41c8-b2bc-9fdc40064a9c
:END: :END:
#+begin_src nix-ts :tangle modules/nixos/server/nginx.nix #+begin_src nix-ts :tangle modules/nixos/server/nginx.nix
{ pkgs, lib, config, globals, ... }: { pkgs, lib, config, ... }:
let let
inherit (config.repo.secrets.common) dnsProvider dnsBase dnsMail;
serviceUser = "nginx"; serviceUser = "nginx";
serviceGroup = serviceUser; serviceGroup = serviceUser;
@ -8619,40 +8671,12 @@ Here we just define some aliases for rebuilding the system, and we allow some in
}; };
}; };
config = lib.mkIf config.swarselmodules.server.nginx { config = lib.mkIf config.swarselmodules.server.nginx {
environment.systemPackages = with pkgs; [
lego
];
sops = lib.mkIf (config.node.name == config.swarselsystems.proxyHost) { swarselmodules.server.acme = lib.mkDefault true;
secrets = {
acme-creds = { format = "json"; key = ""; group = "acme"; sopsFile = config.node.secretsDir + "/acme.json"; mode = "0660"; };
};
templates."certs.secret".content = ''
ACME_DNS_API_BASE = ${dnsBase}
ACME_DNS_STORAGE_PATH=${config.sops.secrets.acme-creds.path}
'';
};
users.groups.acme.members = [ "nginx" ];
security.acme = lib.mkIf (config.node.name == config.swarselsystems.proxyHost) {
acceptTerms = true;
defaults = {
inherit dnsProvider;
email = dnsMail;
environmentFile = "${config.sops.templates."certs.secret".path}";
reloadServices = [ "nginx" ];
dnsPropagationCheck = true;
};
certs."${globals.domains.main}" = {
domain = "*.${globals.domains.main}";
};
};
networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.firewall.allowedTCPPorts = [ 80 443 ];
environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence { environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence {
directories = [{ directory = "/var/lib/acme"; }];
files = [ dhParamsPathBase ]; files = [ dhParamsPathBase ];
}; };
@ -14041,7 +14065,7 @@ or 2) use classic path addressing =aws s3 cp <local file> s3://<bucket>/<path to
SOA = { SOA = {
nameServer = "soa"; nameServer = "soa";
adminEmail = "admin@${globals.domains.main}"; # this option is not parsed as domain (we cannot just write "admin") adminEmail = "admin@${globals.domains.main}"; # this option is not parsed as domain (we cannot just write "admin")
serial = 2025120506; # update this on changes for secondary dns serial = 2025122204; # update this on changes for secondary dns
}; };
useOrigin = false; useOrigin = false;
@ -14051,7 +14075,23 @@ or 2) use classic path addressing =aws s3 cp <local file> s3://<bucket>/<path to
"srv" "srv"
] ++ globals.domains.externalDns; ] ++ globals.domains.externalDns;
CAA = letsEncrypt config.repo.secrets.common.dnsMail; CAA = [
{
issuerCritical = false;
tag = "issue";
value = "letsencrypt.org";
}
{
issuerCritical = false;
tag = "issuewild";
value = "letsencrypt.org";
}
{
issuerCritical = false;
tag = "iodef";
value = "mailto:${config.repo.secrets.common.dnsMail}";
}
];
A = [ config.repo.secrets.local.dns.homepage-ip ]; A = [ config.repo.secrets.local.dns.homepage-ip ];
@ -14204,9 +14244,13 @@ or 2) use classic path addressing =aws s3 cp <local file> s3://<bucket>/<path to
{ lib, config, globals, dns, confLib, ... }: { lib, config, globals, dns, confLib, ... }:
let let
inherit (config.swarselsystems) sopsFile; inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "mailserver"; dir = "/var/lib/dovecot"; user = "virtualMail"; group = "virtualMail"; port = 443; }) serviceName serviceDir servicePort serviceUser serviceGroup serviceDomain serviceProxy proxyAddress4 proxyAddress6; inherit (confLib.gen { name = "mailserver"; dir = "/var/lib/dovecot"; user = "virtualMail"; group = "virtualMail"; port = 443; }) serviceName serviceDir servicePort serviceUser serviceGroup serviceAddress serviceDomain serviceProxy proxyAddress4 proxyAddress6;
inherit (config.repo.secrets.local.mailserver) user1 alias1_1 alias1_2 alias1_3 alias1_4 user2 alias2_1 alias2_2 user3; inherit (config.repo.secrets.local.mailserver) user1 alias1_1 alias1_2 alias1_3 alias1_4 user2 alias2_1 alias2_2 user3;
baseDomain = globals.domains.main; baseDomain = globals.domains.main;
roundcubeDomain = config.repo.secrets.common.services.domains.roundcube;
endpointAddress4 = globals.hosts.${config.node.name}.wanAddress4 or null;
endpointAddress6 = globals.hosts.${config.node.name}.wanAddress6 or null;
in in
{ {
options = { options = {
@ -14215,13 +14259,21 @@ or 2) use classic path addressing =aws s3 cp <local file> s3://<bucket>/<path to
config = lib.mkIf config.swarselmodules.server.${serviceName} { config = lib.mkIf config.swarselmodules.server.${serviceName} {
nodes.stoicclub.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = { nodes.stoicclub.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6; "${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host endpointAddress4 endpointAddress6;
"${globals.services.roundcube.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
}; };
globals.services.${serviceName} = { globals.services = {
${serviceName} = {
domain = serviceDomain; domain = serviceDomain;
proxyAddress4 = endpointAddress4;
proxyAddress6 = endpointAddress6;
};
roundcube = {
domain = roundcubeDomain;
inherit proxyAddress4 proxyAddress6; inherit proxyAddress4 proxyAddress6;
}; };
};
sops.secrets = { sops.secrets = {
user1-hashed-pw = { inherit sopsFile; owner = serviceUser; }; user1-hashed-pw = { inherit sopsFile; owner = serviceUser; };
@ -14286,7 +14338,7 @@ or 2) use classic path addressing =aws s3 cp <local file> s3://<bucket>/<path to
enable = true; enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of # this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver # the mailserver
hostName = serviceDomain; hostName = roundcubeDomain;
extraConfig = '' extraConfig = ''
$config['imap_host'] = "ssl://${config.mailserver.fqdn}"; $config['imap_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_host'] = "ssl://${config.mailserver.fqdn}"; $config['smtp_host'] = "ssl://${config.mailserver.fqdn}";
@ -14299,10 +14351,11 @@ or 2) use classic path addressing =aws s3 cp <local file> s3://<bucket>/<path to
# the rest of the ports are managed by snm # the rest of the ports are managed by snm
networking.firewall.allowedTCPPorts = [ 80 servicePort ]; networking.firewall.allowedTCPPorts = [ 80 servicePort ];
nodes.${serviceProxy}.services.nginx = { services.nginx = {
virtualHosts = { virtualHosts = {
"${serviceDomain}" = { "${roundcubeDomain}" = {
enableACME = true; useACMEHost = globals.domains.main;
enableACME = false;
forceSSL = true; forceSSL = true;
acmeRoot = null; acmeRoot = null;
locations = { locations = {
@ -14315,6 +14368,32 @@ or 2) use classic path addressing =aws s3 cp <local file> s3://<bucket>/<path to
}; };
}; };
nodes.${serviceProxy}.services.nginx = {
upstreams = {
${serviceName} = {
servers = {
"${serviceAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${roundcubeDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "https://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
}; };
} }
#+end_src #+end_src
@ -15929,6 +16008,8 @@ This is just a separate container for derivations defined in [[#h:64a5cc16-6b16-
endme endme
git-replace git-replace
prstatus prstatus
swarsel-gens
swarsel-switch
]; ];
}; };
} }
@ -24652,12 +24733,12 @@ This script allows for quick git replace of a string.
:END: :END:
#+begin_src nix-ts :tangle pkgs/config/default.nix #+begin_src nix-ts :tangle pkgs/config/default.nix
{ self, homeConfig, lib, pkgs, ... }: { self, homeConfig, lib, pkgs, nixosConfig ? null, ... }:
let let
mkPackages = names: pkgs: builtins.listToAttrs (map mkPackages = names: pkgs: builtins.listToAttrs (map
(name: { (name: {
inherit name; inherit name;
value = pkgs.callPackage "${self}/pkgs/config/${name}" { inherit self name homeConfig; }; value = pkgs.callPackage "${self}/pkgs/config/${name}" { inherit self name homeConfig nixosConfig; };
}) })
names); names);
packageNames = lib.swarselsystems.readNix "pkgs/config"; packageNames = lib.swarselsystems.readNix "pkgs/config";
@ -24687,6 +24768,42 @@ This script allows for quick git replace of a string.
} }
#+end_src
**** swarsel-gens
This script quickly lists all nix generatinos on the system
#+begin_src nix-ts :tangle pkgs/config/swarsel-gens/default.nix
{ name, writeShellApplication, nixosConfig, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ nixosConfig.nix.package ];
text = ''
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system
'';
}
#+end_src
**** swarsel-switch
This script quickly switches to another nix generation.
#+begin_src nix-ts :tangle pkgs/config/swarsel-switch/default.nix
{ name, writeShellApplication, nixosConfig, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ nixosConfig.nix.package ];
text = ''
sudo nix-env --switch-generation "$1" -p /nix/var/nix/profiles/system && sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch
'';
}
#+end_src #+end_src
** Profiles ** Profiles
@ -24911,7 +25028,6 @@ Modules that need to be loaded on the NixOS level. Note that these will not be a
diskEncryption = lib.mkDefault true; diskEncryption = lib.mkDefault true;
packages = lib.mkDefault true; packages = lib.mkDefault true;
ssh = lib.mkDefault true; ssh = lib.mkDefault true;
nginx = lib.mkDefault true;
}; };
}; };
}; };

View file

@ -13,7 +13,6 @@
topology.self = { topology.self = {
icon = "devices.cloud-server"; icon = "devices.cloud-server";
}; };
swarselmodules.server.nginx = false;
swarselsystems = { swarselsystems = {
flakePath = "/root/.dotfiles"; flakePath = "/root/.dotfiles";
@ -62,7 +61,7 @@
postgresql = true; postgresql = true;
attic = true; attic = true;
garage = true; garage = true;
hydra = true; hydra = false;
dns-hostrecord = true; dns-hostrecord = true;
}; };

View file

@ -32,7 +32,6 @@
}; };
swarselmodules.server = { swarselmodules.server = {
nginx = false;
bastion = true; bastion = true;
dns-hostrecord = true; dns-hostrecord = true;
# ssh = false; # ssh = false;

View file

@ -10,8 +10,6 @@
topology.self = { topology.self = {
icon = "devices.cloud-server"; icon = "devices.cloud-server";
}; };
swarselmodules.server.nginx = false;
swarselsystems = { swarselsystems = {
flakePath = "/root/.dotfiles"; flakePath = "/root/.dotfiles";
@ -34,7 +32,6 @@
swarselmodules.server = { swarselmodules.server = {
nsd = true; nsd = true;
nginx = false;
dns-hostrecord = true; dns-hostrecord = true;
}; };
} }

View file

@ -33,7 +33,7 @@
"moonside" "moonside"
"winters" "winters"
"belchsfactory" "belchsfactory"
# "eagleland" "eagleland"
]; ];
}; };
}; };
@ -45,8 +45,8 @@
}; };
swarselmodules.server = { swarselmodules.server = {
nginx = true; # for now nginx = true;
oauth2-proxy = true; # for now oauth2-proxy = true;
dns-hostrecord = true; dns-hostrecord = true;
wireguard = true; wireguard = true;
}; };

View file

@ -1,28 +0,0 @@
{
"swarsel.win": {
"fulldomain": "ENC[AES256_GCM,data:CVasUSMRn/KWzVRlcYfTO/RL+W5Cz2JpDj0JLAKITXrDZrl+Wsg46X8zv4hX6NLj/wAyvXQ=,iv:N3DL4JPX8vWTbllFWcpNulwtDJ57xpHrAwoUxWhTzxs=,tag:CYWoK9uT121rFXQ5h69CZA==,type:str]",
"subdomain": "ENC[AES256_GCM,data:uM457vEJa10IV4SovBDUzLLlW+mPwh1SiWr8thQisFoe6zAk,iv:Tdbd5a20Gv/thkPfsvNiAbI86JjcDs70MAfk4yCZLgs=,tag:MulJiRWPs215x0bc+1jBiA==,type:str]",
"username": "ENC[AES256_GCM,data:ePE2BEKL5uaXqzGngW9ArhwP3qwDzwULtfwUfb5Q56VGGURp,iv:/GZRbyXHorcq1PIYlhfOmUVwCg0I/N4ZraEzSrc8qmA=,tag:wM5B1U0BsRsBAJg3qNOXpA==,type:str]",
"password": "ENC[AES256_GCM,data:RGzdi8IMqm+rtiuU4RtWGQ4N/7FYBbp5Pir8/k2V1QEdM8z7SIn0FQ==,iv:ThFbY9eZuEZoyzcWV5DwtSi8ugNwM49JfRof560Qx/Y=,tag:sgMaLrPB8WgpXWPzaCwOBQ==,type:str]",
"server_url": "ENC[AES256_GCM,data:zJdXoO7ED7qeskYJ9Wu0Rdprbvj/uP+Z,iv:ce+QXocqCjNKCsZRyVt6koUyc2lsTwPNMcfQyqbktN0=,tag:bQSE4/6va+V0TORWANLdUA==,type:str]"
},
"sops": {
"age": [
{
"recipient": "age1g7atkxdlt4ymeh7v7aa2yzr2hq2qkvzrc4r49ugttm3n582ymv9qrmpk8d",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVZ0ErYjZTb2o1LzdZY2tz\nNUR0dy9DWkVyQlZBQU1WSmFja0pUN3NJSkNvClNLbTU5RFFwUkJQVUNML291eG5N\nZDlCK0JvMjVDL3lvMURMbFptQ1Z4ZWsKLS0tIFA3OEUrL2tXZGM3TFk4L2l6RUo0\nMVBZOFBYS2lablRuR0hneU02eURYQWMK1M9ng/GcFH+NEmknJ8SHOUxc8atX3p1E\nB/3+4dVWSwVdTEkG2VqQTdo/irjbTKpqZ0m5bg9zDhxZpyQ2lr2ePA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-12-04T19:04:02Z",
"mac": "ENC[AES256_GCM,data:nWV/knCo/MeWTBrfq1VlV6SPEQ2i2P+le82S2So0BIxPfz8tqan0MdaIaKLFlapsT9VRJOv8ZCCXSLWeGcbEvfmEz4MP1E4iHcU/4YaO+n895D1JrjeyP1cgGisnXqe01xMXCsDY178sqxHcnDDlXp9foCem+mGjIlKGPYGu5Oo=,iv:qbavbW3MF4fx+E3aybBYaz/T/Hb63ggWml4Oe9WFz+I=,tag:05vBbBGDGRNaXJWoZn1bVw==,type:str]",
"pgp": [
{
"created_at": "2025-12-04T17:59:06Z",
"enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMAwDh3VI7VctTAQ//bwwqP095CUku9qYMYLJToU9iuL7USF7UxfKQLgP7Lx3a\nilbrofOS508V2og32sZD8y8GGDCMc7HMQv8TcgIk/kq6jX5dHUYN68nVMQ8ZG0As\nW1kpo/cLZAPHoWWEG5E1INX+KSN3b/KhZgXohuVyrax3aTy0kcKeApAJlntr+gyV\nfjPjjvGxXrCXZHN6DzKZ+zqEIs18T0ByLtqLsYzTlD4FszISGCnf6Kr5jpj43BcA\ny1Hj6avzk1bQqPEFovf5JcB+O3DnkIwus+GlXihu/6gIiatbdshVKk/vDdR6TR1/\noDg2EV98uX1K+gEe1JvJdC1JrAPZkOtx4hiFcLVc5G6phdQ08hY4PZ8On4Yajkby\nj46FkPNLB4TwwSC2Ga03CadpaUK0twNGAH7oya3VXUiHqqu2rnVgUjrsZCr6yA4d\nJmumRiTHvnQjECQB5J837wXoDOivaaY0OszELM41p6UIhMTG4/SkkEvfgAI3goGN\nV5g4uBES/TGCedU5NS5EMtsjRoJSQDyvhfkzMUBDcUm8xQ3RKRtdqTZVkT75Ti6M\nmnZolAkqq3uWwmSTIXTgC7T2dnWLRVgfpj7hzZX43ucf5bXCn6QXoZscMUL9LKR5\nd3lyh66PoHghatrb0u3E1ub6XJQWkbDDkKDHRuYjU02Ai12oPd48nhyTuhnmeLCF\nAgwDC9FRLmchgYQBEACgklMklJy3J1U542h3ofmkH5otjNaWv14oVr2yNdOxhlIG\nDYTb9vuLL1lAwxOB7JW6sgPbS9TmiCU6ZBYDeQDmfth7yPWK3/Epmd5wmXDENqra\njoZcpNSvvMnescS0MJWsSF2BHJiwPJewuOCAiL0EXGYVNB7z54kAt342okScNDK2\ndS6/ddjVKFsSi73HmLqQk7wmYpZuqIGoJQXH+E2to8h19e35YxOEsnG2DcVyC7xZ\nqHeUfuM9BTVJmUvqFdovz3lYJ+xg2CjBf8u0jRKOhhufS8JAu9H2ye9dWPktslMF\nRjfRbTAwryVGYmajnlmfoge+OD0XsubSaT79BixZ6xwXgA8xrCvM8in8ZeYsug66\nrgA/I7sO2PPQBh+FNVfuxVVr4MC1Nehk3/JghYzF9Ip7uAvoB9bzi0Yx7L3wGY8i\nr5Rss81IIYvZY4NmPwsOkeX+v9k6GbrcBDa521nl9gz3Ll9Q59jicZBaNyuIvJ3f\nP/bmh1nZc9CM+uIP3A5e/5tUTS5E7judEmOeqlotOjZGdoqyGsG1VqJcrcyTzscY\n8LxCIJtQEeM4KoptKaIXt0Mu/puMzQxIpcx9eFDZ+SE7Cl1QXC6HRLW5N99AuD5f\nSmxquKsmc+xB+gNGkYuySeTqfklK3FLTvISXZmoAQKgqdgO0d+hpCpOQ9lkprtJc\nAbMyytjCe+RLnIWHXi1hjQyspcF8JvBgnRp0zWEZwn+C7QI7ChHlSIrudMohS76L\nN2rF646oaFcxr8mDHy9bebQDXlWahbDB/2jFm3/SuyARtKSg8/PaNcuh+c8=\n=LxIo\n-----END PGP MESSAGE-----",
"fp": "4BE7925262289B476DBBC17B76FD3810215AE097"
}
],
"unencrypted_suffix": "_unencrypted",
"version": "3.11.0"
}
}

View file

@ -26,7 +26,15 @@
isBtrfs = true; isBtrfs = true;
isNixos = true; isNixos = true;
isLinux = true; isLinux = true;
proxyHost = "eagleland"; proxyHost = "twothreetunnel"; # mail shall not be proxied through twothreetunnel
server = {
wireguard.interfaces = {
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
}; };
} // lib.optionalAttrs (!minimal) { } // lib.optionalAttrs (!minimal) {
@ -34,6 +42,8 @@
mailserver = true; mailserver = true;
dns-hostrecord = true; dns-hostrecord = true;
postgresql = true; postgresql = true;
nginx = true;
wireguard = true;
}; };
swarselprofiles = { swarselprofiles = {

View file

@ -52,7 +52,6 @@
swarselmodules = { swarselmodules = {
server = { server = {
nginx = lib.mkForce false; # we get this from the server profile
wireguard = true; wireguard = true;
}; };
}; };

View file

@ -43,10 +43,6 @@
server = true; server = true;
}; };
swarselmodules.server = {
nginx = lib.mkForce false;
};
microvm.vms = microvm.vms =
let let
mkMicrovm = guestName: { mkMicrovm = guestName: {

View file

@ -72,37 +72,36 @@
swarselmodules.server = { swarselmodules.server = {
diskEncryption = lib.mkForce false; diskEncryption = lib.mkForce false;
wireguard = lib.mkDefault true; nginx = true; # for php stuff
nfs = lib.mkDefault true; acme = false; # cert handled by proxy
nginx = lib.mkDefault true; wireguard = true;
kavita = lib.mkDefault true;
restic = lib.mkDefault true; nfs = true;
jellyfin = lib.mkDefault true; kavita = true;
navidrome = lib.mkDefault true; restic = true;
spotifyd = lib.mkDefault true; jellyfin = true;
mpd = lib.mkDefault true; navidrome = true;
postgresql = lib.mkDefault true; spotifyd = true;
matrix = lib.mkDefault true; mpd = true;
nextcloud = lib.mkDefault true; postgresql = true;
immich = lib.mkDefault true; matrix = true;
paperless = lib.mkDefault true; nextcloud = true;
transmission = lib.mkDefault true; immich = true;
syncthing = lib.mkDefault true; paperless = true;
grafana = lib.mkDefault true; transmission = true;
emacs = lib.mkDefault true; syncthing = true;
freshrss = lib.mkDefault true; grafana = true;
jenkins = lib.mkDefault false; emacs = true;
kanidm = lib.mkDefault true; freshrss = true;
firefly-iii = lib.mkDefault true; kanidm = true;
koillection = lib.mkDefault true; firefly-iii = true;
radicale = lib.mkDefault true; koillection = true;
atuin = lib.mkDefault true; radicale = true;
forgejo = lib.mkDefault true; atuin = true;
ankisync = lib.mkDefault true; forgejo = true;
# snipeit = lib.mkDefault false; ankisync = true;
homebox = lib.mkDefault true; homebox = true;
opkssh = lib.mkDefault true; opkssh = true;
garage = lib.mkDefault false;
}; };
} }

View file

@ -33,6 +33,8 @@
endme endme
git-replace git-replace
prstatus prstatus
swarsel-gens
swarsel-switch
]; ];
}; };
} }

View file

@ -127,6 +127,7 @@ in
additions = final: _: import "${self}/pkgs/config" { additions = final: _: import "${self}/pkgs/config" {
inherit self config lib; inherit self config lib;
pkgs = final; pkgs = final;
nixosConfig = config;
homeConfig = config.home-manager.users.${config.swarselsystems.mainUser}; homeConfig = config.home-manager.users.${config.swarselsystems.mainUser};
}; };
in in

View file

@ -0,0 +1,45 @@
{ self, pkgs, lib, config, globals, ... }:
let
inherit (config.repo.secrets.common) dnsProvider dnsBase dnsMail;
sopsFile = self + "/secrets/nginx/acme.json";
in
{
options.swarselmodules.server.acme = lib.mkEnableOption "enable acme on server";
config = lib.mkIf config.swarselmodules.server.acme {
environment.systemPackages = with pkgs; [
lego
];
sops = {
secrets = {
acme-creds = { format = "json"; key = ""; group = "acme"; inherit sopsFile; mode = "0660"; };
};
templates."certs.secret".content = ''
ACME_DNS_API_BASE = ${dnsBase}
ACME_DNS_STORAGE_PATH=${config.sops.secrets.acme-creds.path}
'';
};
users.groups.acme.members = lib.mkIf config.swarselmodules.server.nginx [ "nginx" ];
security.acme = {
acceptTerms = true;
defaults = {
inherit dnsProvider;
email = dnsMail;
environmentFile = "${config.sops.templates."certs.secret".path}";
reloadServices = [ "nginx" ];
dnsPropagationCheck = true;
};
certs."${globals.domains.main}" = {
domain = "*.${globals.domains.main}";
};
};
environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence {
directories = [{ directory = "/var/lib/acme"; }];
};
};
}

View file

@ -1,9 +1,13 @@
{ lib, config, globals, dns, confLib, ... }: { lib, config, globals, dns, confLib, ... }:
let let
inherit (config.swarselsystems) sopsFile; inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "mailserver"; dir = "/var/lib/dovecot"; user = "virtualMail"; group = "virtualMail"; port = 443; }) serviceName serviceDir servicePort serviceUser serviceGroup serviceDomain serviceProxy proxyAddress4 proxyAddress6; inherit (confLib.gen { name = "mailserver"; dir = "/var/lib/dovecot"; user = "virtualMail"; group = "virtualMail"; port = 443; }) serviceName serviceDir servicePort serviceUser serviceGroup serviceAddress serviceDomain serviceProxy proxyAddress4 proxyAddress6;
inherit (config.repo.secrets.local.mailserver) user1 alias1_1 alias1_2 alias1_3 alias1_4 user2 alias2_1 alias2_2 user3; inherit (config.repo.secrets.local.mailserver) user1 alias1_1 alias1_2 alias1_3 alias1_4 user2 alias2_1 alias2_2 user3;
baseDomain = globals.domains.main; baseDomain = globals.domains.main;
roundcubeDomain = config.repo.secrets.common.services.domains.roundcube;
endpointAddress4 = globals.hosts.${config.node.name}.wanAddress4 or null;
endpointAddress6 = globals.hosts.${config.node.name}.wanAddress6 or null;
in in
{ {
options = { options = {
@ -12,13 +16,21 @@ in
config = lib.mkIf config.swarselmodules.server.${serviceName} { config = lib.mkIf config.swarselmodules.server.${serviceName} {
nodes.stoicclub.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = { nodes.stoicclub.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6; "${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host endpointAddress4 endpointAddress6;
"${globals.services.roundcube.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
}; };
globals.services.${serviceName} = { globals.services = {
${serviceName} = {
domain = serviceDomain; domain = serviceDomain;
proxyAddress4 = endpointAddress4;
proxyAddress6 = endpointAddress6;
};
roundcube = {
domain = roundcubeDomain;
inherit proxyAddress4 proxyAddress6; inherit proxyAddress4 proxyAddress6;
}; };
};
sops.secrets = { sops.secrets = {
user1-hashed-pw = { inherit sopsFile; owner = serviceUser; }; user1-hashed-pw = { inherit sopsFile; owner = serviceUser; };
@ -83,7 +95,7 @@ in
enable = true; enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of # this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver # the mailserver
hostName = serviceDomain; hostName = roundcubeDomain;
extraConfig = '' extraConfig = ''
$config['imap_host'] = "ssl://${config.mailserver.fqdn}"; $config['imap_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_host'] = "ssl://${config.mailserver.fqdn}"; $config['smtp_host'] = "ssl://${config.mailserver.fqdn}";
@ -96,10 +108,11 @@ in
# the rest of the ports are managed by snm # the rest of the ports are managed by snm
networking.firewall.allowedTCPPorts = [ 80 servicePort ]; networking.firewall.allowedTCPPorts = [ 80 servicePort ];
nodes.${serviceProxy}.services.nginx = { services.nginx = {
virtualHosts = { virtualHosts = {
"${serviceDomain}" = { "${roundcubeDomain}" = {
enableACME = true; useACMEHost = globals.domains.main;
enableACME = false;
forceSSL = true; forceSSL = true;
acmeRoot = null; acmeRoot = null;
locations = { locations = {
@ -112,5 +125,31 @@ in
}; };
}; };
nodes.${serviceProxy}.services.nginx = {
upstreams = {
${serviceName} = {
servers = {
"${serviceAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${roundcubeDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "https://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
}; };
} }

View file

@ -1,7 +1,5 @@
{ pkgs, lib, config, globals, ... }: { pkgs, lib, config, ... }:
let let
inherit (config.repo.secrets.common) dnsProvider dnsBase dnsMail;
serviceUser = "nginx"; serviceUser = "nginx";
serviceGroup = serviceUser; serviceGroup = serviceUser;
@ -81,40 +79,12 @@ in
}; };
}; };
config = lib.mkIf config.swarselmodules.server.nginx { config = lib.mkIf config.swarselmodules.server.nginx {
environment.systemPackages = with pkgs; [
lego
];
sops = lib.mkIf (config.node.name == config.swarselsystems.proxyHost) { swarselmodules.server.acme = lib.mkDefault true;
secrets = {
acme-creds = { format = "json"; key = ""; group = "acme"; sopsFile = config.node.secretsDir + "/acme.json"; mode = "0660"; };
};
templates."certs.secret".content = ''
ACME_DNS_API_BASE = ${dnsBase}
ACME_DNS_STORAGE_PATH=${config.sops.secrets.acme-creds.path}
'';
};
users.groups.acme.members = [ "nginx" ];
security.acme = lib.mkIf (config.node.name == config.swarselsystems.proxyHost) {
acceptTerms = true;
defaults = {
inherit dnsProvider;
email = dnsMail;
environmentFile = "${config.sops.templates."certs.secret".path}";
reloadServices = [ "nginx" ];
dnsPropagationCheck = true;
};
certs."${globals.domains.main}" = {
domain = "*.${globals.domains.main}";
};
};
networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.firewall.allowedTCPPorts = [ 80 443 ];
environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence { environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence {
directories = [{ directory = "/var/lib/acme"; }];
files = [ dhParamsPathBase ]; files = [ dhParamsPathBase ];
}; };

View file

@ -3,7 +3,7 @@ with dns.lib.combinators; {
SOA = { SOA = {
nameServer = "soa"; nameServer = "soa";
adminEmail = "admin@${globals.domains.main}"; # this option is not parsed as domain (we cannot just write "admin") adminEmail = "admin@${globals.domains.main}"; # this option is not parsed as domain (we cannot just write "admin")
serial = 2025120506; # update this on changes for secondary dns serial = 2025122204; # update this on changes for secondary dns
}; };
useOrigin = false; useOrigin = false;
@ -13,7 +13,23 @@ with dns.lib.combinators; {
"srv" "srv"
] ++ globals.domains.externalDns; ] ++ globals.domains.externalDns;
CAA = letsEncrypt config.repo.secrets.common.dnsMail; CAA = [
{
issuerCritical = false;
tag = "issue";
value = "letsencrypt.org";
}
{
issuerCritical = false;
tag = "issuewild";
value = "letsencrypt.org";
}
{
issuerCritical = false;
tag = "iodef";
value = "mailto:${config.repo.secrets.common.dnsMail}";
}
];
A = [ config.repo.secrets.local.dns.homepage-ip ]; A = [ config.repo.secrets.local.dns.homepage-ip ];

View file

@ -14,6 +14,9 @@
swarsel-deploy swarsel-deploy
tmux tmux
busybox busybox
attic-client
swarsel-gens
swarsel-switch
]; ];
}; };
} }

View file

@ -1,9 +1,9 @@
{ self, homeConfig, lib, pkgs, ... }: { self, homeConfig, lib, pkgs, nixosConfig ? null, ... }:
let let
mkPackages = names: pkgs: builtins.listToAttrs (map mkPackages = names: pkgs: builtins.listToAttrs (map
(name: { (name: {
inherit name; inherit name;
value = pkgs.callPackage "${self}/pkgs/config/${name}" { inherit self name homeConfig; }; value = pkgs.callPackage "${self}/pkgs/config/${name}" { inherit self name homeConfig nixosConfig; };
}) })
names); names);
packageNames = lib.swarselsystems.readNix "pkgs/config"; packageNames = lib.swarselsystems.readNix "pkgs/config";

View file

@ -0,0 +1,9 @@
{ name, writeShellApplication, nixosConfig, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ nixosConfig.nix.package ];
text = ''
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system
'';
}

View file

@ -0,0 +1,9 @@
{ name, writeShellApplication, nixosConfig, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ nixosConfig.nix.package ];
text = ''
sudo nix-env --switch-generation "$1" -p /nix/var/nix/profiles/system && sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch
'';
}

View file

@ -20,7 +20,6 @@
diskEncryption = lib.mkDefault true; diskEncryption = lib.mkDefault true;
packages = lib.mkDefault true; packages = lib.mkDefault true;
ssh = lib.mkDefault true; ssh = lib.mkDefault true;
nginx = lib.mkDefault true;
}; };
}; };
}; };

32
secrets/nginx/acme.json Normal file
View file

@ -0,0 +1,32 @@
{
"swarsel.win": {
"fulldomain": "ENC[AES256_GCM,data:CVasUSMRn/KWzVRlcYfTO/RL+W5Cz2JpDj0JLAKITXrDZrl+Wsg46X8zv4hX6NLj/wAyvXQ=,iv:N3DL4JPX8vWTbllFWcpNulwtDJ57xpHrAwoUxWhTzxs=,tag:CYWoK9uT121rFXQ5h69CZA==,type:str]",
"subdomain": "ENC[AES256_GCM,data:uM457vEJa10IV4SovBDUzLLlW+mPwh1SiWr8thQisFoe6zAk,iv:Tdbd5a20Gv/thkPfsvNiAbI86JjcDs70MAfk4yCZLgs=,tag:MulJiRWPs215x0bc+1jBiA==,type:str]",
"username": "ENC[AES256_GCM,data:ePE2BEKL5uaXqzGngW9ArhwP3qwDzwULtfwUfb5Q56VGGURp,iv:/GZRbyXHorcq1PIYlhfOmUVwCg0I/N4ZraEzSrc8qmA=,tag:wM5B1U0BsRsBAJg3qNOXpA==,type:str]",
"password": "ENC[AES256_GCM,data:RGzdi8IMqm+rtiuU4RtWGQ4N/7FYBbp5Pir8/k2V1QEdM8z7SIn0FQ==,iv:ThFbY9eZuEZoyzcWV5DwtSi8ugNwM49JfRof560Qx/Y=,tag:sgMaLrPB8WgpXWPzaCwOBQ==,type:str]",
"server_url": "ENC[AES256_GCM,data:zJdXoO7ED7qeskYJ9Wu0Rdprbvj/uP+Z,iv:ce+QXocqCjNKCsZRyVt6koUyc2lsTwPNMcfQyqbktN0=,tag:bQSE4/6va+V0TORWANLdUA==,type:str]"
},
"sops": {
"age": [
{
"recipient": "age1g7atkxdlt4ymeh7v7aa2yzr2hq2qkvzrc4r49ugttm3n582ymv9qrmpk8d",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBWmFOUGZrbkNlQ2ppbnFw\nTVd3RVYrK1RocjU3Z0pPOUNpc05YZ0FKK2hJClAvdkZ0aFRtaE5PRWFNbWdWSjN3\nbWc1WUpuTTVDTlNldTh0ejBGakZvbDgKLS0tIHJaRmwwSEtqQXprcFZ3TlllWmJa\nczN6eE8wWUppc3o1VTRpcjF0VUM1azQKdWhbP34mx6yR6TXUMpP/npA9TjAayjqz\nHC7ztK5/zu4ML7BRCsoLNLDDVtMsmhEVKO8VV9sbMvusSq0WNu0I7g==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1cmzh82q8k59yzceuuy2epmqu22g7m84gqvq056mhgehwpmvjadfsc3glc8",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxVUM5eTMrZ3lCeDgxVHlj\naWx4QUZ5c0NGV2xEaXliNHFyVktkTnBYR0d3CnRMcUgyUU1hNzhLcWhaYzJCMndR\nQWJyNEZJV2xNVVBSc2xnbzRqdEx1eXMKLS0tIDJhTGhVYkxJcGZ6N3dHZmV3QlBo\nNzBORDJXb1pRbVNiWHZiVjBKQjNwWDAK1z/XA6gRor2fNX70UooBqHjCFfIIP34j\n+vHx10dtoue5tSPgM3cA8QZzqM6Ht+U282JFMztrlDtiz/j/JofTNg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-12-04T19:04:02Z",
"mac": "ENC[AES256_GCM,data:nWV/knCo/MeWTBrfq1VlV6SPEQ2i2P+le82S2So0BIxPfz8tqan0MdaIaKLFlapsT9VRJOv8ZCCXSLWeGcbEvfmEz4MP1E4iHcU/4YaO+n895D1JrjeyP1cgGisnXqe01xMXCsDY178sqxHcnDDlXp9foCem+mGjIlKGPYGu5Oo=,iv:qbavbW3MF4fx+E3aybBYaz/T/Hb63ggWml4Oe9WFz+I=,tag:05vBbBGDGRNaXJWoZn1bVw==,type:str]",
"pgp": [
{
"created_at": "2025-12-22T14:07:12Z",
"enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMAwDh3VI7VctTAQ//XZNxhn6vCEDo6SAtlIcA5OxNuj3326ITbjjv+XwiTHk/\n28cErtXOF+4bnjIXLwXGbbewWgvrnK0Lx5nBqpidMrhANHVEWDp3iBrW79QPdRl9\ntIcofQUK+6Dwh+XqJK4eorEAeAKlu6l5y3pc19L4j7BiDOxcXBYqTx4ScnZhFFoc\nRVC6ngRjck+2e6bL/+lopntHML41yrXpAHMBlobTbzYP8cnfMKroUZzLoNareVIS\nNlcE7oaCwAvCXYBbQN5qQCIFbLBFB+vD4ArOrL/zNxWVjnEeM4RhBdIjd0bdq1FQ\nLBsO1crEniz4Lgp5N4cWOzuOCz5fHlLJEKtNIIFt8rcocBEdyQf1OmZ2VrY57nN+\nSeCYGN+fPA87UoH+iZcmLNjDvMy+MHNIiGCEuY9jWLzgcDDvtFwtQdl2CpI8a/Ax\nKG9RgMFyjLtPCgNzDkn6LrvHKtnFkcsx22f8PHB4RZIgP43loH//xieOVnA9vo/k\nV3pRHZuZkTp3qmAED3KhbjE7aKP3bjUkJdrTATsal5YyRd76pjPvtXkX6MchgTgh\n6emt6DssvWJy5vhgitM0Ucq1IeeaLwIDTXqP5GShcQrWJ2P5ilzLGp2OKYzOqSBf\n//LfA3EHq4aFaTGuOVxjf/VS1vEeIAMaAo4eo86unIIhcQ6OBVAKzKfrSgzV9iiF\nAgwDC9FRLmchgYQBEACFwFYn12YCzYIwpSfJ7Apo5nEiLYbcD1wXR7bx7MFjJFxc\ncrS6yB/44y6a+OJfTLEVpDTPahKiOFAlmMAb8B05bDd/jqairwJPS+Sw2t/Tb6Ry\nToHi8ucg0+P8b0j+VeXoE0DlnuSMKbKQGvIq9fd8G6FymHppA4TpuKFl0QPUAbX7\nGLQbApk7gRoZK1XTQ/HKjyXdhfRogo8/07oRt6iO3nxC5NkLS/uxk/0ODqiFTGVg\nESkGQK3YQcr8hhhM467S49nzh+AQODgCTm7jSFO8kJwDP6eb5vF63qrIBZNUGc6j\nIoNHzQxqDoaFUd2GZcKg2Zsyizb91qYEMZdHUh51h1XH+jhg8G+L7I+hnBbzIj/o\naZfXALcmYNiI630SsiqA4iSufWy1bONnP0kHPSxVBqilLXpmfzIuNUwrZSQB+ZbR\nkIIA5lRqu1jWIcG/3CGQQMDfjqxus4Wt8bvGfOrbQk2qNs+l3uPb0+OBDNKQfVHD\nenXkTGrKIHt4YqnDc3ziWNAULwvRPviPQZPkJwPCDHdfA2bcQqLJOIYiBF1TdN9o\nv2eMQoTalEXfHOI5ndULjlyy5DaW2ZBKAgtH/WXg/O7ey5Q9e9QcnmwbOezEWx6x\nNNY3eoQs5LYxoWO7cEUv2MiEqY7ZWkrwmwcWzRV/7UmspvPHKULQV/fiNWb1yNJc\nATKv+sla0n0DzD+7BSiFcYC0ZvGAErYAwNIYhlgMEN7okuCscB+tz1d0yLhNt8DK\nZAn0NkIVnTn6esHqhp233ZmCrCmUezccMMINs8xQbqJR8bkns5hTDYLY48s=\n=v2/q\n-----END PGP MESSAGE-----",
"fp": "4BE7925262289B476DBBC17B76FD3810215AE097"
}
],
"unencrypted_suffix": "_unencrypted",
"version": "3.11.0"
}
}

File diff suppressed because one or more lines are too long