Replace caddy with nginx

Before this commit, we used Caddy. Here's what was needed to make that setup go:
- As we needed the layer4 plugin, we had to compile caddy with layer4 using an
  unsupported technique in nix
- As the layer4 plugin did not support caddyfile, we had to use caddy.json
  - We wrote some nix code to setup reverse proxy routes easily however this was
    a challenge to reasonably maintain, and caddy.json is far
    less-well-documented than caddyfile leading to a significant amount of pain
    if we ever needed to break out
- Several modules in NixOS use nginx by default (privatebin, nextcloud etc.), we
  need to disable and then replicate their setup. This is often nontrivial

Nginx has some distinct advantages for Clicks specifically on NixOS:
- "Streams" are supported Out-Of-The box, meaning no layer4 plugin
- Nginx has a standard nixified interface for virtual hosts, meaning there's no
  breaking out into nginx config for http
  - Note how you may still have to break out for TCP/UDP streams
- Nginx configurations, including relatively complex ones (e.g. nextcloud) are
  often included with modules by default. Choosing nginx will avoid us rewriting
  these, as well as all the pain and debugging that comes with that

Additionally:
- Nginx has excellent integration with the builtin ACME module. I am satisfied
  with its ability to replace caddy on that front
- A major point in the favor of caddy is usability. Unfortunately, this really
  only seems to apply to caddyfile with caddy.json being significantly more
  cumbersome to write, even with nix helper functions
- It is trivial to write some simple but decent helper functions for nginx

Alternatives considered:
- Stick with caddy
  - Too much maintenance burden
  - We struggle to update it frequently, due to the plugin install bits
- Use traefik
  - From a short look, it's too complex for our needs
  - It doesn't have configs built into nix in the same way as nginx
- Use apache2
  - Apache has no analogue to nginx streams
  - It doesn't have configs built into nix in the same way as nginx

This commit will need a followup in future to improve the typing of the Service
type. We have a very basic implementation but it would be nice to be able to
check for types that are inside it rather than just the top level.

Change-Id: I25e7ba48cec6b9308e6aa9a14f57a8c192918c92
diff --git a/.editorconfig b/.editorconfig
index 56c2916..0f112c3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,5 +5,5 @@
 insert_final_newline = true
 charset = utf-8
 indent_style = space
-indent_size = 4
+indent_size = 2
 max_line_length = 80
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..9b836ce
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "helpers"]
+	path = helpers
+	url = https://git.clicks.codes/Clicks/NixHelpers
diff --git a/flake.lock b/flake.lock
index cf8678c..6dd1f0b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -54,6 +54,24 @@
         "type": "github"
       }
     },
+    "helpers": {
+      "inputs": {
+        "nixpkgs": "nixpkgs_2"
+      },
+      "locked": {
+        "lastModified": 1697846472,
+        "narHash": "sha256-OWxoAM79X6fssw6CnlhPvxfmuoC4Aq4PX+0aYv/ONBQ=",
+        "ref": "refs/heads/main",
+        "rev": "5c7ee827fd35a9b2e489e919796f73536788c483",
+        "revCount": 11,
+        "type": "git",
+        "url": "https://git.clicks.codes/Clicks/NixHelpers"
+      },
+      "original": {
+        "type": "git",
+        "url": "https://git.clicks.codes/Clicks/NixHelpers"
+      }
+    },
     "home-manager": {
       "inputs": {
         "nixpkgs": [
@@ -141,6 +159,20 @@
     },
     "nixpkgs_2": {
       "locked": {
+        "lastModified": 1697688401,
+        "narHash": "sha256-61QlajY7R9PbK25uFl55zh968CVNspwXX1zzimic4Uo=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "458ef9126aa380996d77d44f53f886c2d8485f53",
+        "type": "github"
+      },
+      "original": {
+        "id": "nixpkgs",
+        "type": "indirect"
+      }
+    },
+    "nixpkgs_3": {
+      "locked": {
         "lastModified": 1693428224,
         "narHash": "sha256-FWUUlhYqkGEySUD0blTADRiDQ7fw+H1ikivfu88uy+w=",
         "owner": "nixos",
@@ -159,8 +191,9 @@
       "inputs": {
         "deploy-rs": "deploy-rs",
         "flake-utils": "flake-utils",
+        "helpers": "helpers",
         "home-manager": "home-manager",
-        "nixpkgs": "nixpkgs_2",
+        "nixpkgs": "nixpkgs_3",
         "nixpkgs-clicksforms": "nixpkgs-clicksforms",
         "nixpkgs-privatebin": "nixpkgs-privatebin",
         "scalpel": "scalpel",
diff --git a/flake.nix b/flake.nix
index 8c3be8b..3eb97d4 100644
--- a/flake.nix
+++ b/flake.nix
@@ -18,6 +18,8 @@
 
   inputs.nixpkgs-privatebin.url = "github:e1mo/nixpkgs/privatebin";
 
+  inputs.helpers.url = "git+https://git.clicks.codes/Clicks/NixHelpers";
+
   outputs =
     { self
     , nixpkgs
@@ -26,6 +28,7 @@
     , sops-nix
     , scalpel
     , nixpkgs-privatebin
+    , helpers
     , ...
     }@inputs:
     let
@@ -47,7 +50,6 @@
               ./default/configuration.nix
               ./default/hardware-configuration.nix
               ./modules/cache.nix
-              ./modules/caddy.nix
               ./modules/clamav.nix
               ./modules/cloudflare-ddns.nix
               ./modules/dmarc.nix
@@ -57,7 +59,6 @@
               ./modules/drivePaths.nix
               ./modules/ecryptfs.nix
               ./modules/fail2ban.nix
-              ./modules/fuck.nix
               ./modules/gerrit.nix
               ./modules/git.nix
               ./modules/grafana.nix
@@ -67,7 +68,10 @@
               ./modules/loginctl-linger.nix
               ./modules/matrix.nix
               ./modules/mongodb.nix
+              ./modules/networking.nix
               ./modules/nextcloud.nix
+              ./modules/nginx-routes.nix
+              ./modules/nginx.nix
               ./modules/node.nix
               ./modules/postgres.nix
               ./modules/privatebin.nix
@@ -84,7 +88,12 @@
                 users.mutableUsers = false;
               }
             ];
-            specialArgs = { base = null; drive_paths = import ./variables/drive_paths.nix; inherit system; };
+            specialArgs = {
+              base = null;
+              drive_paths = import ./variables/drive_paths.nix;
+              inherit system;
+              helpers = helpers.helpers { inherit pkgs; };
+            };
           };
         in
         base.extendModules {
@@ -184,6 +193,6 @@
         packages = [ pkgs.deploy-rs ];
       };
 
-      formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt;
+      formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt;
     };
 }
diff --git a/helpers b/helpers
new file mode 160000
index 0000000..5c7ee82
--- /dev/null
+++ b/helpers
@@ -0,0 +1 @@
+Subproject commit 5c7ee827fd35a9b2e489e919796f73536788c483
diff --git a/modules/caddy.nix b/modules/caddy.nix
deleted file mode 100644
index 26e0a58..0000000
--- a/modules/caddy.nix
+++ /dev/null
@@ -1,47 +0,0 @@
-{ base, config, pkgs, lib, ... }: lib.recursiveUpdate
-{
-  services.nginx.enable = false; # PrivateBin, nextcloud etc. attempts to enable nginx but we already use caddy
-  services.caddy.enable = true;
-  services.caddy.configFile = lib.pipe ./caddy/caddyfile.nix [
-    import
-    (f: f { inherit pkgs lib config; })
-    builtins.toJSON
-    (pkgs.writeText "caddy.json")
-  ];
-  services.caddy.package = pkgs.callPackage ../packages/caddy.nix { };
-  services.caddy.user = "root";
-  systemd.services.caddy.serviceConfig.ProtectHome = lib.mkForce false;
-
-  sops.secrets.cloudflare_token = {
-    mode = "0600";
-    owner = config.users.users.root.name;
-    group = config.users.users.nobody.group;
-    sopsFile = ../secrets/caddy.json;
-    format = "json";
-  };
-}
-  (
-    let
-      isDerived = base != null;
-    in
-    if isDerived
-    then
-      let
-        caddy_json = base.config.services.caddy.configFile;
-      in
-      {
-        scalpel.trafos."caddy.json" = {
-          source = toString caddy_json;
-          matchers."cloudflare_token".secret =
-            config.sops.secrets.cloudflare_token.path;
-          owner = config.users.users.root.name;
-          group = config.users.users.nobody.group;
-          mode = "0400";
-        };
-
-        services.caddy.configFile = lib.mkForce config.scalpel.trafos."caddy.json".destination;
-
-        systemd.services.caddy.reloadTriggers = [ caddy_json ];
-      }
-    else { }
-  )
diff --git a/modules/caddy/caddyfile.nix b/modules/caddy/caddyfile.nix
deleted file mode 100644
index 9f6bf49..0000000
--- a/modules/caddy/caddyfile.nix
+++ /dev/null
@@ -1,441 +0,0 @@
-let
-  HTTPReverseProxyRoute = hosts: upstreams: {
-    handle = [
-      {
-        handler = "subroute";
-        routes = [
-          {
-            handle = [
-              {
-                handler = "reverse_proxy";
-                upstreams = map (upstream: { dial = upstream; }) upstreams;
-              }
-            ];
-          }
-        ];
-      }
-    ];
-    match = [{ host = hosts; }];
-    terminal = true;
-  };
-  PHPRoute = hosts: root: socket: {
-    handle = [
-      {
-        handler = "subroute";
-        routes = [
-          {
-            handle = [
-              {
-                handler = "vars";
-                inherit root;
-              }
-            ];
-          }
-          {
-            handle = [
-              {
-                handler = "static_response";
-                headers.Location = [ "{http.request.orig_uri.path}/" ];
-                status_code = 307;
-              }
-            ];
-            match = [
-              {
-                file.try_files = [ "{http.request.uri.path}/index.php" ];
-                not = [ { path = ["*/"]; } ];
-              }
-            ];
-          }
-          {
-            handle = [
-              {
-                handler = "rewrite";
-                uri = "{http.matchers.file.relative}";
-              }
-            ];
-            match = [
-              {
-                file = {
-                  split_path = [ ".php" ];
-                  try_files = [
-                    "{http.request.uri.path}"
-                    "{http.request.uri.path}/index.php"
-                    "index.php"
-                  ];
-                };
-              }
-            ];
-          }
-          {
-            handle = [
-              {
-                handler = "reverse_proxy";
-                transport = {
-                  protocol = "fastcgi";
-                  split_path = [".php"];
-                };
-                upstreams = [{ dial = socket; }];
-              }
-            ];
-            match = [{ path = ["*.php"]; }];
-          }
-          {
-            handle = [
-              {
-                handler = "file_server";
-              }
-            ];
-          }
-        ];
-      }
-    ];
-    match = [{ host = hosts; }];
-    terminal = true;
-  };
-  HTTPRedirectRoute = hosts: goto: {
-    handle = [
-      {
-        handler = "subroute";
-        routes = [
-          {
-            handle = [
-              {
-                handler = "static_response";
-                headers = { Location = [ goto ]; };
-                status_code = 302;
-              }
-            ];
-          }
-        ];
-      }
-    ];
-    match = [{ host = hosts; }];
-    terminal = true;
-  };
-  HTTPFileServerRoute = hosts: root: {
-    handle = [
-      {
-        handler = "subroute";
-        routes = [
-          {
-            handle = [
-              {
-                handler = "file_server";
-                inherit root;
-              }
-            ];
-          }
-        ];
-      }
-    ];
-    match = [{ host = hosts; }];
-    terminal = true;
-  };
-
-  TCPReverseProxyRoute = ports: upstreams: {
-    listen = map (port: "0.0.0.0:${toString port}") ports;
-    routes = [
-      {
-        handle = [
-          {
-            handler = "proxy";
-            proxy_protocol = "v2";
-            upstreams = [{ dial = upstreams; }];
-          }
-        ];
-      }
-    ];
-  };
-in
-{ pkgs, lib, config }: {
-  apps = {
-    http.servers = {
-      srv0 = {
-        listen = [ ":443" ];
-        routes = [
-          (HTTPReverseProxyRoute [ "signup.hopescaramels.com" ] [ "192.168.0.4:3035" ])
-          (HTTPReverseProxyRoute [ "homebridge.coded.codes" ] [ "localhost:8581" ])
-          {
-            handle = [
-              {
-                handler = "subroute";
-                routes = [
-                  {
-                    handle = [
-                      {
-                        error = "You can't access admin routes from outside the server. Please use SSH tunneling, cURL on the host or similar";
-                        handler = "error";
-                        status_code = "403";
-                      }
-                    ];
-                    match = [{ path = [ "/_dendrite/admin/*" "/_synapse/admin/*" ]; }];
-                    terminal = true;
-                  }
-                  {
-                    handle = [
-                      {
-                        handler = "reverse_proxy";
-                        transport = { protocol = "http"; };
-                        upstreams = [{ dial = "localhost:4527"; }];
-                      }
-                    ];
-                  }
-                ];
-              }
-            ];
-            match = [{ host = [ "matrix-backend.coded.codes" ]; }];
-            terminal = true;
-          }
-          (HTTPReverseProxyRoute
-            [
-              "mail.coded.codes"
-              "mail.clicks.codes"
-              "mail.hopescaramels.com"
-            ]
-            [ "localhost:1080" ]
-          )
-          (HTTPReverseProxyRoute [ "logs.clicks.codes" ] [ "localhost:9052" ])
-          (HTTPRedirectRoute
-            [
-              "hopescaramels.com"
-              "www.hopescaramels.com"
-            ]
-            "https://etsy.com/shop/HopesCaramels"
-          )
-          # (HTTPReverseProxyRoute [ "omv.coded.codes" ] [ "localhost:6773" ])
-          # (HTTPReverseProxyRoute [ "jellyfin.coded.codes" ] [ "localhost:8096" ])
-          (HTTPReverseProxyRoute [ "codedpc.coded.codes" ] [ "192.168.0.2:3389" ])
-          (HTTPReverseProxyRoute [ "testing.coded.codes" ] [ "192.168.0.2:3030" ])
-          (HTTPReverseProxyRoute [ "kavita.coded.codes" ] [ "localhost:5000" ])
-          {
-            handle = [
-              {
-                handler = "subroute";
-                routes = [
-                  {
-                    handle = [
-                      {
-                        handler = "subroute";
-                        routes = [
-                          {
-                            handle = [
-                              {
-                                handler = "rewrite";
-                                strip_path_prefix = "/nucleus";
-                              }
-                            ];
-                          }
-                          {
-                            handle = [
-                              {
-                                handler = "reverse_proxy";
-                                upstreams = [{ dial = "127.0.0.1:10000"; }];
-                              }
-                            ];
-                          }
-                        ];
-                      }
-                    ];
-                    match = [{ path = [ "/nucleus/*" ]; }];
-                  }
-                  {
-                    handle = [
-                      {
-                        handler = "error";
-                        error = "This API route does not exist";
-                        status_code = 404;
-                      }
-                    ];
-                  }
-                ];
-              }
-            ];
-            match = [{ host = [ "api.clicks.codes" ]; }];
-            terminal = true;
-          }
-          {
-            handle = [
-              {
-                handler = "subroute";
-                routes = [
-                  {
-                    handle = [
-                      {
-                        handler = "subroute";
-                        routes = [
-                          {
-                            handle = [
-                              {
-                                handler = "rewrite";
-                                strip_path_prefix = "/nucleus";
-                              }
-                            ];
-                          }
-                          {
-                            handle = [
-                              {
-                                handler = "reverse_proxy";
-                                upstreams = [{ dial = "192.168.0.2:10000"; }];
-                              }
-                            ];
-                          }
-                        ];
-                      }
-                    ];
-                    match = [{ path = [ "/nucleus/*" ]; }];
-                  }
-                  {
-                    handle = [
-                      {
-                        handler = "error";
-                        error = "This API route does not exist";
-                        status_code = 404;
-                      }
-                    ];
-                  }
-                ];
-              }
-            ];
-            match = [{ host = [ "api.coded.codes" ]; }];
-            terminal = true;
-          }
-          (HTTPRedirectRoute
-            [
-              "www.clicks.codes"
-            ]
-            "https://clicks.codes{http.request.uri}"
-          )
-          (HTTPReverseProxyRoute [ "clicks.codes" ] [ "127.0.0.1:3000" ])
-          {
-            handle = [
-              {
-                handler = "subroute";
-                routes = [
-                  {
-                    handle = [
-                      {
-                        handler = "static_response";
-                        status_code = 200;
-                        body = builtins.readFile ./coded.codes/.well-known/matrix;
-                        headers = { Access-Control-Allow-Origin = [ "*" ]; };
-                      }
-                    ];
-                    match = [{
-                      path = [
-                        "/.well-known/matrix/server"
-                        "/.well-known/matrix/client"
-                      ];
-                    }];
-                    terminal = true;
-                  }
-                  {
-                    handle = [
-                      {
-                        handler = "static_response";
-                        headers = { Location = [ "https://clicks.codes{http.request.uri}" ]; };
-                        status_code = 302;
-                      }
-                    ];
-                  }
-                ];
-              }
-            ];
-            match = [{ host = [ "coded.codes" ]; }];
-            terminal = true;
-          }
-          (HTTPFileServerRoute [ "matrix.coded.codes" ] (
-            pkgs.schildichat-web.override {
-              conf = {
-                default_server_config = lib.pipe ./coded.codes/.well-known/matrix [
-                  builtins.readFile
-                  builtins.fromJSON
-                ];
-                features = {
-                  feature_report_to_moderators = true;
-                  feature_latex_maths = true;
-                  feature_pinning = true;
-                  feature_mjolnir = true;
-                  feature_presence_in_room_list = true;
-                  feature_custom_themes = true;
-                  feature_dehydration = true;
-                };
-                setting_defaults = {
-                  "fallbackICEServerAllowed" = true;
-                };
-                default_theme = "dark";
-                permalink_prefix = "https://matrix.coded.codes";
-                disable_guests = true;
-                disable_3pid_login = true;
-              };
-            }
-          ))
-          (HTTPReverseProxyRoute [ "passwords.clicks.codes" ] [ "localhost:8452" ])
-          (HTTPReverseProxyRoute [ "login.clicks.codes" ] [ "localhost:9083" ])
-          (HTTPReverseProxyRoute [
-            "syncthing.clicks.codes"
-            "syncthing.coded.codes"
-            "syncthing.thecoded.prof"
-            "syncthing.hopescaramels.com"
-          ] [ "localhost:8384" ])
-          (HTTPReverseProxyRoute [
-            "git.clicks.codes"
-            "gerrit.clicks.codes"
-          ] [ "127.0.0.255:1000" ])
-          (PHPRoute
-            [ "paste.clicks.codes" "paste.coded.codes" ]
-            "${pkgs.privatebin}/share/privatebin"
-            "unix/${config.services.phpfpm.pools.privatebin.socket}"
-          )
-          (PHPRoute
-            [ "cloud.clicks.codes" "nextcloud.clicks.codes" "docs.clicks.codes" ]
-            "${config.services.nextcloud.package}"
-            "unix/${config.services.phpfpm.pools.nextcloud.socket}"
-          )
-        ];
-      };
-      srv1 = {
-        listen = [ ":80" ];
-        routes = [
-          (HTTPReverseProxyRoute
-            [
-              "mail.clicks.codes"
-              "mail.coded.codes"
-              "mail.hopescaramels.com"
-              "autoconfig.coded.codes"
-              "autoconfig.clicks.codes"
-              "autoconfig.hopescaramels.com"
-              "imap.coded.codes"
-              "imap.clicks.codes"
-              "imap.hopescaramels.com"
-              "pop.coded.codes"
-              "pop.clicks.codes"
-              "pop.hopescaramels.com"
-              "smtp.coded.codes"
-              "smtp.clicks.codes"
-              "smtp.hopescaramels.com"
-            ]
-            [ "localhost:1080" ]
-          )
-        ];
-      };
-    };
-    layer4.servers = {
-      imap-143 = (TCPReverseProxyRoute [ 143 ] [ "localhost:1143" ]);
-      imap-993 = (TCPReverseProxyRoute [ 993 ] [ "localhost:1993" ]);
-      pop-110 = (TCPReverseProxyRoute [ 110 ] [ "localhost:1110" ]);
-      pop-995 = (TCPReverseProxyRoute [ 995 ] [ "localhost:1995" ]);
-      smtp-25 = (TCPReverseProxyRoute [ 25 ] [ "localhost:1025" ]);
-      smtp-465 = (TCPReverseProxyRoute [ 465 ] [ "localhost:1465" ]);
-      smtp-587 = (TCPReverseProxyRoute [ 587 ] [ "localhost:1587" ]);
-    };
-    tls.automation.policies = [{
-      issuers = [{
-        module = "acme";
-        challenges.dns.provider = {
-          name = "cloudflare";
-          api_token = "!!cloudflare_token!!";
-        };
-      }];
-    }];
-  };
-}
diff --git a/modules/cloudflare-ddns.nix b/modules/cloudflare-ddns.nix
index 3abd8a2..35cbf13 100644
--- a/modules/cloudflare-ddns.nix
+++ b/modules/cloudflare-ddns.nix
@@ -12,7 +12,7 @@
     mode = "0600";
     owner = config.users.users.root.name;
     group = config.users.users.root.group;
-    sopsFile = ../secrets/cloudflare.env.bin;
+    sopsFile = ../secrets/cloudflare-ddns.env.bin;
     format = "binary";
   };
 }
diff --git a/modules/fuck.nix b/modules/fuck.nix
deleted file mode 100644
index 572f926..0000000
--- a/modules/fuck.nix
+++ /dev/null
@@ -1,5 +0,0 @@
-{ config, pkgs, ... }: {
-  programs.thefuck.enable = true;
-  programs.thefuck.alias = "fuck";
-}
-
diff --git a/modules/grafana.nix b/modules/grafana.nix
index 7c3b175..9a97342 100644
--- a/modules/grafana.nix
+++ b/modules/grafana.nix
@@ -1,4 +1,4 @@
-{ lib, config, base, pkgs, ... }:
+{ lib, config, base, pkgs, helpers, ... }:
 lib.recursiveUpdate
 {
   services.grafana = {
diff --git a/modules/networking.nix b/modules/networking.nix
new file mode 100644
index 0000000..1940ced
--- /dev/null
+++ b/modules/networking.nix
@@ -0,0 +1,8 @@
+{
+    networking.hosts = {
+        "127.0.0.1" = [ "standard" ];
+        "127.0.0.2" = [ "clicks" ];
+        "127.0.0.3" = [ "caramels" ];
+        "127.0.0.255" = [ "generic" ];
+    };
+}
diff --git a/modules/nextcloud.nix b/modules/nextcloud.nix
index 8574407..52ab029 100644
--- a/modules/nextcloud.nix
+++ b/modules/nextcloud.nix
@@ -16,7 +16,7 @@
 
     services.nextcloud.enable = true;
     services.nextcloud.config.adminpassFile = config.sops.secrets.nextcloud_admin_password.path;
-    services.nextcloud.hostName = "cloud.clicks.codes";
+    services.nextcloud.hostName = "nextcloud.clicks.codes";
     services.nextcloud.package = pkgs.nextcloud27;
     services.nextcloud.poolSettings = {
         pm = "dynamic";
@@ -47,6 +47,14 @@
             url = "https://github.com/zorn-v/nextcloud-social-login/releases/download/v5.5.3/release.tar.gz";
             sha256 = "sha256-96/wtK7t23fXVRcntDONjgb5bYtZuaNZzbvQCa5Gsj4=";
         };
+        richdocumentscode = pkgs.fetchNextcloudApp {
+            url = "redacted";
+            sha256 = "sha256-XYtjBZCIQ6+PL3BNLSZfJTgLLpOyphzR5HOAwI7bWx0=";
+        };
+        richdocuments = pkgs.fetchNextcloudApp {
+            url = "https://github.com/nextcloud-releases/richdocuments/releases/download/v8.2.0/richdocuments-v8.2.0.tar.gz";
+            sha256 = "sha256-PKw7FXSWvden2+6XjnUDOvbTF71slgeTF/ktS/l2+Dk=";
+        };
     };
 
     sops.secrets.nextcloud_admin_password = {
diff --git a/modules/nginx-routes.nix b/modules/nginx-routes.nix
new file mode 100644
index 0000000..918d6dd
--- /dev/null
+++ b/modules/nginx-routes.nix
@@ -0,0 +1,99 @@
+{ pkgs, helpers, config, lib, ... }: {
+  clicks.nginx.services = with helpers.nginx; [
+    (Host "signup.hopescaramels.com" (ReverseProxy "CodedPi.local:3035"))
+    (Host "homebridge.coded.codes" (ReverseProxy "CodedPi.local:8581"))
+    (Host "codedpc.coded.codes" (ReverseProxy "SamuelDesktop.local:3389"))
+    (Host "testing.coded.codes" (ReverseProxy "SamuelDesktop.local:3000"))
+    (Hosts [ "kavita.coded.codes" "reading.coded.codes" ]
+      (ReverseProxy "localhost:5000"))
+    (Host "www.clicks.codes" (RedirectPermanent "https://clicks.codes$request_uri"))
+    (Host "clicks.codes" (ReverseProxy "127.0.0.1:3000"))
+    (Host "passwords.clicks.codes" (ReverseProxy "localhost:8452"))
+    (Host "login.clicks.codes" (ReverseProxy "localhost:9083"))
+    (Hosts [
+      "syncthing.clicks.codes"
+      "syncthing.coded.codes"
+      "syncthing.thecoded.prof"
+      "syncthing.hopescaramels.com"
+    ] (ReverseProxy "localhost:8384"))
+    (Hosts [ "gerrit.clicks.codes" "git.clicks.codes" ]
+      (ReverseProxy "127.0.0.255:1000"))
+    (Hosts [ "grafana.clicks.codes" "logs.clicks.codes" ]
+      (ReverseProxy "localhost:9052"))
+    (InsecureHosts [
+      "mail.clicks.codes"
+      "mail.coded.codes"
+      "mail.hopescaramels.com"
+      "autoconfig.coded.codes"
+      "autoconfig.clicks.codes"
+      "autoconfig.hopescaramels.com"
+      "imap.coded.codes"
+      "imap.clicks.codes"
+      "imap.hopescaramels.com"
+      "pop.coded.codes"
+      "pop.clicks.codes"
+      "pop.hopescaramels.com"
+      "smtp.coded.codes"
+      "smtp.clicks.codes"
+      "smtp.hopescaramels.com"
+    ] (ReverseProxy "localhost:1080"))
+    (Hosts [
+      "mail.clicks.codes"
+      "mail.coded.codes"
+      "mail.hopescaramels.com"
+    ] (ReverseProxy "localhost:1080"))
+    (Host "matrix.coded.codes" (Directory "${builtins.toString (pkgs.schildichat-web.override {
+      conf = {
+        default_server_config = lib.pipe ./nginx/coded.codes/.well-known/matrix [
+          builtins.readFile
+          builtins.fromJSON
+        ];
+        features = {
+          feature_report_to_moderators = true;
+          feature_latex_maths = true;
+          feature_pinning = true;
+          feature_mjolnir = true;
+          feature_presence_in_room_list = true;
+          feature_custom_themes = true;
+          feature_dehydration = true;
+        };
+        setting_defaults = { "fallbackICEServerAllowed" = true; };
+        default_theme = "dark";
+        permalink_prefix = "https://matrix.coded.codes";
+        disable_guests = true;
+        disable_3pid_login = true;
+      };
+    })}"))
+    (Host "api.clicks.codes" (Path "/nucleus/" (ReverseProxy "localhost:10000")))
+    (Host "api.coded.codes" (Path "/nucleus/" (ReverseProxy "SamuelDesktop.local:10000")))
+    (Host "coded.codes" (Compose [
+      (Path "/.well-known/matrix/" (File ./nginx/coded.codes/.well-known/matrix))
+      (Redirect "https://clicks.codes$request_uri")
+    ]))
+    (Host "matrix-backend.coded.codes" (Compose [
+      (Path "/_synapse/admin/" (Status 403))
+      (ReverseProxy "localhost:4527")
+    ]))
+  ];
+  clicks.nginx.serviceAliases = with helpers.nginx; [
+    (Aliases "nextcloud.clicks.codes" [
+      "cloud.clicks.codes"
+      "docs.clicks.codes"
+    ])
+    (Aliases "privatebin" [
+      "paste.clicks.codes"
+      "paste.coded.codes"
+      "paste.thecoded.prof"
+      "paste.hopescaramels.com"
+    ])
+  ];
+  clicks.nginx.streams = with helpers.nginx; [
+    (Stream 143 "localhost:1143" "tcp") #imap
+    (Stream 993 "localhost:1993" "tcp") #imap
+    (Stream 110 "localhost:1110" "tcp") #pop3
+    (Stream 995 "localhost:1995" "tcp") #pop3
+    (Stream  25 "localhost:1025" "tcp") #smtp
+    (Stream 465 "localhost:1465" "tcp") #smtp
+    (Stream 587 "localhost:1587" "tcp") #smtp
+  ];
+}
diff --git a/modules/nginx.nix b/modules/nginx.nix
new file mode 100644
index 0000000..64ad1ce
--- /dev/null
+++ b/modules/nginx.nix
@@ -0,0 +1,187 @@
+{ config, lib, pkgs, helpers, base, ... }:
+lib.recursiveUpdate
+{
+  options.clicks = {
+    nginx = {
+      services = lib.mkOption {
+        type = with lib.types;
+          listOf (submodule {
+            options = {
+              host = lib.mkOption { type = str; };
+              extraHosts = lib.mkOption { type = listOf str; };
+              secure = lib.mkOption { type = bool; };
+              service = lib.mkOption {
+                type = let
+                  validServiceTypes = {
+                    "redirect" = {
+                      to = ["string" str];
+                      permanent = ["bool" bool];
+                    };
+                    "reverseproxy" = {
+                      to = ["string" str];
+                    };
+                    "php" = {
+                      root = ["string" str];
+                      socket = ["string" str];
+                    };
+                    "directory" = {
+                      private = ["bool" bool];
+                      root = ["string" str];
+                    };
+                    "file" = {
+                      path = ["string" str];
+                    };
+                    "path" = {
+                      path = ["string" str];
+                      service = ["set" serviceType];
+                    };
+                    "compose" = {
+                      services = ["list" (listOf serviceType)];
+                    };
+                    "status" = {
+                      statusCode = ["int" int];
+                    };
+                  };
+
+                  serviceType = mkOptionType {
+                    name = "Service";
+
+                    description = "clicks Nginx service";
+                    descriptionClass = "noun";
+
+                    check = (x:
+                      if (builtins.typeOf x) != "set"
+                      then lib.warn "clicks nginx services must be sets but ${x} is not a set" false
+                      else if !(builtins.hasAttr "type" x)
+                      then lib.warn "clicks nginx services must have a type attribute but ${x} does not" false
+                      else if !(builtins.hasAttr x.type validServiceTypes)
+                      then lib.warn "clicks nginx services must have a valid type, but ${x.type} is not one" false
+                      else (let
+                        optionTypes =
+                          (builtins.mapAttrs (n: o: builtins.elemAt o 0) validServiceTypes.${x.type})
+                          // { type = "string"; };
+                      in (lib.pipe x [
+                        (builtins.mapAttrs (n: o: (builtins.hasAttr n optionTypes) && optionTypes.${n} == (builtins.typeOf o)))
+                        lib.attrValues
+                        (builtins.all (x: x))
+                      ]) && (lib.pipe optionTypes [
+                        (builtins.mapAttrs (n: _: builtins.hasAttr n x))
+                        lib.attrValues
+                        (builtins.all (x: x))
+                      ] )));
+                  };
+                in serviceType;
+              };
+              type = lib.mkOption { type = strMatching "hosts"; };
+            };
+          });
+        example = lib.literalExpression ''
+          with helpers.nginx; [
+            (Host "example.clicks.codes" (ReverseProxy "generic:1001"))
+          ]'';
+        description = lib.mdDoc ''
+          Connects hostnames to services for your nginx server. We recommend using the Clicks helper to generate these
+        '';
+        default = [ ];
+      };
+      serviceAliases = lib.mkOption {
+        type = with lib.types;
+          listOf (submodule {
+            options = {
+              host = lib.mkOption {
+                type = str;
+                example = "example.clicks.codes";
+                description = ''
+                  The ServerName of the server. If you override this in the nginx server block, you still need to put in the name of the attribute
+                '';
+              };
+              aliases = lib.mkOption {
+                type = listOf str;
+                example = [ "example2.clicks.codes" "example.coded.codes" ];
+                description = ''
+                  A list of servers to add as aliases
+                '';
+              };
+              type = lib.mkOption { type = strMatching "aliases"; };
+            };
+          });
+        example = lib.literalExpression ''
+          with helpers.nginx; [
+            (Host "example.clicks.codes" (ReverseProxy "generic:1001"))
+          ]'';
+        description = lib.mdDoc ''
+          Adds additional host names to your nginx server. If you're using `clicks.nginx.services`
+          you should generally use a Hosts block instead
+        '';
+        default = [ ];
+      };
+      streams = lib.mkOption {
+        type = with lib.types; listOf (submodule {
+          options = {
+            internal = lib.mkOption { type = str; };
+            external = lib.mkOption { type = port; };
+            protocol = lib.mkOption { type = strMatching "^(tcp|udp)$"; };
+          };
+        });
+        example = lib.literalExpression ''
+          with helpers.nginx; [
+            (Stream 1001 "generic:1002" "tcp")
+          ]'';
+        description = lib.mdDoc ''
+          A list of servers to be placed in the nginx streams block. We recommend using the Clicks helper to generate these
+        '';
+        default = [ ];
+      };
+    };
+  };
+  config = {
+    services.nginx = {
+      enable = true;
+      enableReload = true;
+
+      virtualHosts = lib.recursiveUpdate (helpers.nginx.Merge
+        config.clicks.nginx.services) # clicks.nginx.services
+        (lib.pipe config.clicks.nginx.serviceAliases [
+          (map (alias: {
+            name = alias.host;
+            value.serverAliases = alias.aliases;
+          }))
+          builtins.listToAttrs
+        ]); # clicks.nginx.serviceAliases
+
+      streamConfig = builtins.concatStringsSep "\n" (map (stream: ''
+        server {
+            listen ${builtins.toString stream.external}${lib.optionalString (stream.protocol == "udp") " udp"};
+            proxy_pass ${builtins.toString stream.internal};
+        }
+      '') config.clicks.nginx.streams);
+    };
+
+    networking.firewall.allowedTCPPorts = lib.pipe config.clicks.nginx.streams [
+      (builtins.filter (stream: stream.protocol == "tcp"))
+      (map (stream: stream.external))
+    ];
+    networking.firewall.allowedUDPPorts = lib.pipe config.clicks.nginx.streams [
+      (builtins.filter (stream: stream.protocol == "udp"))
+      (map (stream: stream.external))
+    ];
+
+    security.acme.defaults = {
+      email = "admin@clicks.codes";
+      credentialsFile = config.sops.secrets.cloudflare_cert__api_token.path;
+    };
+    security.acme.acceptTerms = true;
+
+    sops.secrets.cloudflare_cert__api_token = {
+      mode = "0660";
+      owner = config.users.users.nginx.name;
+      group = config.users.users.acme.group;
+      sopsFile = ../secrets/cloudflare-cert.env.bin;
+      format = "binary";
+    };
+  };
+} (
+  if base != null
+  then {
+    config.security.acme.certs = builtins.mapAttrs (_: v: { webroot = null; dnsProvider = "cloudflare"; }) base.config.security.acme.certs;
+  } else {})
diff --git a/modules/caddy/coded.codes/.well-known/matrix b/modules/nginx/coded.codes/.well-known/matrix
similarity index 100%
rename from modules/caddy/coded.codes/.well-known/matrix
rename to modules/nginx/coded.codes/.well-known/matrix
diff --git a/modules/privatebin.nix b/modules/privatebin.nix
index e82c389..17a1556 100644
--- a/modules/privatebin.nix
+++ b/modules/privatebin.nix
@@ -6,7 +6,7 @@
     settings = {
       main = {
         name = "Clicks Minute Paste";
-        basepath = "https://paste.clicks.codes/";
+        basepath = "https://privatebin.clicks.codes/";
         opendiscussion = true;
         fileupload = true;
 
@@ -20,6 +20,11 @@
         langaugeselection = true;
       };
 
+      nginx = {
+        serverName = "privatebin.clicks.codes";
+        enableACME = true;
+      };
+
       expire.default = "1month";
 
       expire_options = {
diff --git a/secrets/caddy.json b/secrets/caddy.json
deleted file mode 100644
index 1e60b3f..0000000
--- a/secrets/caddy.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-	"cloudflare_token": "ENC[AES256_GCM,data:ZrVPHcTEED9TnK23wvtZQDEwcWC5vyh5HJkhBlC4VjLsJkVUURsNmw==,iv:oFincLWd0ESOuNQoFRoioFPupdCl76sVpZLHZL3kV38=,tag:11wCHPq3BktPPH/g77Lg/Q==,type:str]",
-	"sops": {
-		"kms": null,
-		"gcp_kms": null,
-		"azure_kv": null,
-		"hc_vault": null,
-		"age": [
-			{
-				"recipient": "age15mv77dpnh5762gk5rsw2u79uza4tg8cu6r3nlwjudlzmdqqck3ss6mg9dy",
-				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBacDQxSnFyRHppcTZ5RGtx\nTktlU3lxV3ZxS3ZRMG1kUEpBVWxlTFdwM0h3CjRRWkJGMzZqaUNJRmhDU2d4eXd1\nVkFrL09tZk1WMlJpaTRTb2RNZk9RajgKLS0tIHY2Mkovd1VjY1JEVHErZGxYRno2\nWnplTTdNaGpmWUxMZUJpbkIxMFdoek0KfOooGbcME83hOWy2KRA+qkOiIQccdeT/\nb7VthIvwE301M7gN2AtOgxNMri9zDCx+ZQ9F4dglVYwUXfMaHrSG1A==\n-----END AGE ENCRYPTED FILE-----\n"
-			},
-			{
-				"recipient": "age1m7k864feyuezllp2hj4edkccn36rthrvfw969j6f0l3c0mhh5emsnfx6pd",
-				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxanhZKzQ2Qlh0YzlLRnor\nVTZ2S0xEdXVaTWYwc3pRVDd5UXhqVloxWHpnCjJUUG5QZGlJR0tDeDFzTnd6bTM0\nUWxlY1N3anJUMDh4Y1YxYlpzWnRsWG8KLS0tIHI1ZGtROFVxY3l6eFFIMWRpN3Jt\nSmNpWHAyeFNvYllwUlFKbkI3WVFCMkUKUnEeqqClufvhkCrwzKdon7PYMagJupQf\n89WTPHVyAgsua1cUn/ZiG042d9VavijrAHruUEj2j2c0kKFAwf6FFQ==\n-----END AGE ENCRYPTED FILE-----\n"
-			},
-			{
-				"recipient": "age1fxxnmkeuqhhct93c43pwkzhuzzq8857s5hye6pgfpku70kjn4ecqtamfqr",
-				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQRWZid2QwSzBRbDR6eEVE\nS2cwRDF5NGNyVVhlMmRVanhjRHhGWmtwVnhBCitQcnk4T0hNR3RkWUh4WUN6RzhV\nSGwyWlMxdFkzditXemZzeWpFN1pPTmMKLS0tIG8zaU5QMmpBRDdvNXhwNTBFTERO\naXFZSS80amFPeHIxeDMveWxvd09VV28KSNJAKsWCOTLJsb0britOs+EunO3kdXyX\nS1fs2bpaa2tH92Fn13LZOVoiLatR8ppD2Sd8hkZ3kQp2dG3s4TiJjw==\n-----END AGE ENCRYPTED FILE-----\n"
-			}
-		],
-		"lastmodified": "2023-05-24T15:18:01Z",
-		"mac": "ENC[AES256_GCM,data:ZETpBaOW1TCYmDYflRBz4Gz8bHvXCuZS+kN54MZ59gt3xh31n+2MBYWavh6H1EPppL6WVt1gynAF3GWPuBqfo+OeOWGaL6c3VCnnnWgbTWDb2K3Qn7JYpsAMUnSXT9DF+oAtrtvuFX+K3ubR4UL+3PYIVFmwr5esCQKI0ngz34U=,iv:CNIh86JUlMvzCfJfsqRWirNtCuUKvlcwOj/8xPR/J8M=,tag:yFRV0TB8ay86U1Z1HbpCnQ==,type:str]",
-		"pgp": null,
-		"unencrypted_suffix": "_unencrypted",
-		"version": "3.7.3"
-	}
-}
\ No newline at end of file
diff --git a/secrets/cloudflare-cert.env.bin b/secrets/cloudflare-cert.env.bin
new file mode 100644
index 0000000..2178003
--- /dev/null
+++ b/secrets/cloudflare-cert.env.bin
@@ -0,0 +1,28 @@
+{
+	"data": "ENC[AES256_GCM,data:w2kDT51mb1a+4n53zv8PNA+MaPgL7k49bq9bjxk+wdp1zi8lDOTekUT07I6HbUXL5m2a+7vdrb1LH1QSAZ6FIZuG,iv:ljL8ghvNMtnLkPV80q0zWnZ+lNFbFWlFS0PpuEVWXVM=,tag:6gceTFG5hy3v5PR4PC8GHg==,type:str]",
+	"sops": {
+		"kms": null,
+		"gcp_kms": null,
+		"azure_kv": null,
+		"hc_vault": null,
+		"age": [
+			{
+				"recipient": "age15mv77dpnh5762gk5rsw2u79uza4tg8cu6r3nlwjudlzmdqqck3ss6mg9dy",
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXT2JQd3JFVjdvd0ptUk8v\nSGp4c3ZHVWQzR2VzSUI0TjFTV01vV1UyVmd3Clg5Z2FDTWRObGdlaWMxL2NuWnA1\nZXM4ajNOZDdJbDJpWGdBU3NqS3BVa2sKLS0tIHJFa0RzOFNKMFBvSFBGQjBzT0Vo\nOUFYd2tTSTBldGpic0wrVDdEMlR2bjgKM9/KNI2zpiH3HGajHYi1e2WUf9zLcJAa\nBswooM1RqbWjSFqGYSF7Lv6F2x7C+7jgya/+M1UoXiB3ZuC5CzSgTg==\n-----END AGE ENCRYPTED FILE-----\n"
+			},
+			{
+				"recipient": "age1m7k864feyuezllp2hj4edkccn36rthrvfw969j6f0l3c0mhh5emsnfx6pd",
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2cDJ1WTVPV2R1K2M5YTFJ\nMDZwcjQxN1pqOE5ONkZuTFJUM2lkVDc4d0h3Cmt3dngyRVNIdlJMTHJ2WklLM0o1\nRTMyMVBRZmlDN3FMVUZDOVlPUGlsdzAKLS0tIFMzTVhTaXNyT0kxalY3ajNHZEtW\nMnhyZXppZVhCUlhVRVlLb2tyVi9SR28K2atV+UZN39jsnoQoUKxDwCuV6tO8c1nH\nMVR/p2w+D3Q1lj2YrXjQInoYTLfEbmXHxpwTGonkHXo3fjuyFPlrlg==\n-----END AGE ENCRYPTED FILE-----\n"
+			},
+			{
+				"recipient": "age1fxxnmkeuqhhct93c43pwkzhuzzq8857s5hye6pgfpku70kjn4ecqtamfqr",
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNlRjUWtYVDYwN3dFcE9B\nVFZxNmxHekZFOWNSb1dxZFJhQnVyYmNjSVFRCmcyT05nR2RtK3VZajRSbGN3NFRy\neDlUZmZLcXkxUmlXZzNoeFRndnFsc00KLS0tIGpEcGZvZ0VBWEVGemN2M3BSV3ha\nWUZIS1AzNEhNT2dKQUN6TjhzcEh4VDgKvEYNaxMaWkWmbaxK0gIe+VUyfW59IRfy\nsdVpld6fTSnZYrhWM33h9RQt4A+ZLkQQ0Kiq4+AmWu+r6BIbP7cFEw==\n-----END AGE ENCRYPTED FILE-----\n"
+			}
+		],
+		"lastmodified": "2023-10-21T00:40:17Z",
+		"mac": "ENC[AES256_GCM,data:bATqqFEZzXwFCvjJDI1+sFxDmFmH807YQh/agB5NPLmilWj7XKcAUXfPHfHlbFgfIGaQzsmzZaqrO9IT0wNZb2+Rbo2J8IFwWE0KcZRPpdbrxDM8Ad9xBcBfgfHUr9gXr1hkqqf6kwVHvADxEtvsfR6jYdB0sey6rQS8naWp1KM=,iv:DSxQV/94EsFlqvj/k9qJ2z6IeShByHCSdgpQDhtjt80=,tag:ToXwkkZ7nzxkkF+FMBztaA==,type:str]",
+		"pgp": null,
+		"unencrypted_suffix": "_unencrypted",
+		"version": "3.7.3"
+	}
+}
\ No newline at end of file
diff --git a/secrets/cloudflare.env.bin b/secrets/cloudflare-ddns.env.bin
similarity index 76%
rename from secrets/cloudflare.env.bin
rename to secrets/cloudflare-ddns.env.bin
index 9c98cce..bf60c21 100644
--- a/secrets/cloudflare.env.bin
+++ b/secrets/cloudflare-ddns.env.bin
@@ -1,5 +1,5 @@
 {
-	"data": "ENC[AES256_GCM,data:yAvKBvjHqUyF10o5QqI4u7TJfbNicHQ7gc5bz+RNjL0cJr9Y3W1xOlzSVzU2VfSAsFUVP2Ug92uZrrgBeIs=,iv:ccB2mUP/3TnvsmaJP37VFSLPPDjw5DHZq2p1jQNIBN0=,tag:+AJBgtIR9x0WMFy1hEILHg==,type:str]",
+	"data": "ENC[AES256_GCM,data:jWAQGoHUKYiwC588KqaIJYfXagBqXqPcWfeEfkGF8PwPv3HqApxxcUONW88FxjxKBpt/dNRJSxddbmZCx5s=,iv:SYuJo36aFRZ6EWr5uaEHIuRFPK7cTqlAEjvo9/cET64=,tag:3fUGqxuBRtc47O/ORN/M4g==,type:str]",
 	"sops": {
 		"kms": null,
 		"gcp_kms": null,
@@ -19,8 +19,8 @@
 				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaSXU1TURlMVRVS0paMlZo\nbTJiWllRT0c1V1BzOTBCQ0JubzFtc2NFb1NBCkN5Qy8rS3hoNzQvbmNPQjdWd2NI\nYUZvdXdYYkVqLzlMbHFIR0xGNjhncVEKLS0tIGV4Q2pKZzRLWngyRVNBN2dZbmxi\nc1kyVG1sd1hrdUYxZkFWMTNVQ01vZU0K9GemfZkMEfz6Sa+FmAX8Dl8LsDKpNsJL\n/sD4ZHu/EwVIxqsi1Roctx6lSOd4ZQNs1o7dSjSEzzwBH5pSUMMVow==\n-----END AGE ENCRYPTED FILE-----\n"
 			}
 		],
-		"lastmodified": "2023-08-20T22:16:06Z",
-		"mac": "ENC[AES256_GCM,data:jyRs+sCKlBWYebAfarkQS/J4CB5unoTLcQNmdB3QORkoiX1+GNysJo8YSqAgIt9V3BkTEaCfyb83MndNchzSDdwR00pp9H7tL+r5wU/fqOc8efKm4K/WbMzr48eBy30ybyPDjTNvyxk2cr5KHaT53c/9VUtaF1dabKsFgfGwMd4=,iv:WS23Oo1+sjRXZmWT7yG7NfpkkcDseQS2W/U1LVqJudI=,tag:rXR6efgFW0IYCC9awOQvMA==,type:str]",
+		"lastmodified": "2023-10-21T00:39:36Z",
+		"mac": "ENC[AES256_GCM,data:rfYAEHHN12rNmEv2OoZTjBKxHz47T0fCr6/SONmu3fu+MdW4gH1S8S5KyFQugCGCxxsIvqzCnueuA3EUtgM/6UrnULP5r+0sXpLmR3GrG7T6Jzp+Q7oIH5LIU6p2VCI5sN3kB3Ghdspcev5FnSfWTjPRzmQxvw6lhCYb1x0bMRA=,iv:GwtwwW4D9cr3xvaV19auuOACmruFSR6wFE/V+Tqf2wo=,tag:kXdVIKiblvTclRaOfBchQQ==,type:str]",
 		"pgp": null,
 		"unencrypted_suffix": "_unencrypted",
 		"version": "3.7.3"