feat: Add nginx module

Change-Id: I34fbb926c4b7eab344c1c14de4e4b5f82c6c30eb
Reviewed-on: https://git.clicks.codes/c/Infra/NixFiles/+/785
Reviewed-by: Samuel Shuert <coded@clicks.codes>
Tested-by: Skyler Grey <minion@clicks.codes>
diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000..ea890af
--- /dev/null
+++ b/LICENSES/BSD-3-Clause.txt
@@ -0,0 +1,11 @@
+Copyright (c) <year> <owner>. 
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/flake.lock b/flake.lock
index e54a23e..cc4f48b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -9,11 +9,11 @@
         "utils": "utils"
       },
       "locked": {
-        "lastModified": 1711973905,
-        "narHash": "sha256-UFKME/N1pbUtn+2Aqnk+agUt8CekbpuqwzljivfIme8=",
+        "lastModified": 1718194053,
+        "narHash": "sha256-FaGrf7qwZ99ehPJCAwgvNY5sLCqQ3GDiE/6uLhxxwSY=",
         "owner": "serokell",
         "repo": "deploy-rs",
-        "rev": "88b3059b020da69cbe16526b8d639bd5e0b51c8b",
+        "rev": "3867348fa92bc892eba5d9ddb2d7a97b9e127a8a",
         "type": "github"
       },
       "original": {
@@ -59,6 +59,43 @@
         "systems": "systems_2"
       },
       "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils-plus": {
+      "inputs": {
+        "flake-utils": "flake-utils_2"
+      },
+      "locked": {
+        "lastModified": 1715533576,
+        "narHash": "sha256-fT4ppWeCJ0uR300EH3i7kmgRZnAVxrH+XtK09jQWihk=",
+        "owner": "gytis-ivaskevicius",
+        "repo": "flake-utils-plus",
+        "rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f",
+        "type": "github"
+      },
+      "original": {
+        "owner": "gytis-ivaskevicius",
+        "repo": "flake-utils-plus",
+        "rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f",
+        "type": "github"
+      }
+    },
+    "flake-utils_2": {
+      "inputs": {
+        "systems": "systems_3"
+      },
+      "locked": {
         "lastModified": 1694529238,
         "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
         "owner": "numtide",
@@ -72,31 +109,33 @@
         "type": "github"
       }
     },
-    "flake-utils-plus": {
+    "home-manager": {
       "inputs": {
-        "flake-utils": "flake-utils"
+        "nixpkgs": [
+          "nixpkgs"
+        ]
       },
       "locked": {
-        "lastModified": 1696331477,
-        "narHash": "sha256-YkbRa/1wQWdWkVJ01JvV+75KIdM37UErqKgTf0L54Fk=",
-        "owner": "gytis-ivaskevicius",
-        "repo": "flake-utils-plus",
-        "rev": "bfc53579db89de750b25b0c5e7af299e0c06d7d3",
+        "lastModified": 1718983978,
+        "narHash": "sha256-lp6stESwTLBZUQ5GBivxwNehShmBp4jqeX/1xahM61w=",
+        "owner": "nix-community",
+        "repo": "home-manager",
+        "rev": "c559542f0aa87971a7f4c1b3478fe33cc904b902",
         "type": "github"
       },
       "original": {
-        "owner": "gytis-ivaskevicius",
-        "repo": "flake-utils-plus",
+        "owner": "nix-community",
+        "repo": "home-manager",
         "type": "github"
       }
     },
     "impermanence": {
       "locked": {
-        "lastModified": 1708968331,
-        "narHash": "sha256-VUXLaPusCBvwM3zhGbRIJVeYluh2uWuqtj4WirQ1L9Y=",
+        "lastModified": 1717932370,
+        "narHash": "sha256-7C5lCpiWiyPoIACOcu2mukn/1JRtz6HC/1aEMhUdcw0=",
         "owner": "nix-community",
         "repo": "impermanence",
-        "rev": "a33ef102a02ce77d3e39c25197664b7a636f9c30",
+        "rev": "27979f1c3a0d3b9617a3563e2839114ba7d48d3f",
         "type": "github"
       },
       "original": {
@@ -124,6 +163,8 @@
     "root": {
       "inputs": {
         "deploy-rs": "deploy-rs",
+        "flake-utils": "flake-utils",
+        "home-manager": "home-manager",
         "impermanence": "impermanence",
         "nixpkgs": "nixpkgs",
         "snowfall-lib": "snowfall-lib",
@@ -206,6 +247,21 @@
         "type": "github"
       }
     },
+    "systems_3": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
     "unstable": {
       "locked": {
         "lastModified": 1714906307,
diff --git a/flake.nix b/flake.nix
index 00c6592..b22ff26 100644
--- a/flake.nix
+++ b/flake.nix
@@ -7,15 +7,22 @@
   description = "Clicks Infrastructure";
 
   inputs = {
-    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
-    unstable.url = "github:nixos/nixpkgs/nixos-unstable";
-
-
     deploy-rs = {
       url = "github:serokell/deploy-rs";
       inputs.nixpkgs.follows = "nixpkgs";
     };
 
+    home-manager = {
+      url = "github:nix-community/home-manager";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+
+    flake-utils.url = "github:numtide/flake-utils";
+
+    impermanence.url = "github:nix-community/impermanence";
+
+    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
+
     snowfall-lib = {
       url = "github:snowfallorg/lib";
       inputs.nixpkgs.follows = "nixpkgs";
@@ -29,7 +36,7 @@
       };
     };
 
-    impermanence.url = "github:nix-community/impermanence";
+    unstable.url = "github:nixos/nixpkgs/nixos-unstable";
   };
 
   outputs =
diff --git a/lib/modules/default.nix b/lib/modules/default.nix
new file mode 100644
index 0000000..f0b6092
--- /dev/null
+++ b/lib/modules/default.nix
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: {
+  modules = {
+    unset = lib.modules.mkIf false null;
+  };
+}
diff --git a/lib/nginx/http/default.nix b/lib/nginx/http/default.nix
new file mode 100644
index 0000000..ad0e822
--- /dev/null
+++ b/lib/nginx/http/default.nix
@@ -0,0 +1,53 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, inputs, ... }:
+{
+  nginx.http = let
+    _directory = listContents: root: {
+      inherit root listContents;
+      _type = "directory";
+      headers = null;
+    };
+    _redirect = permanent: to: {
+      inherit to permanent;
+      _type = "redirect";
+      headers = null;
+    };
+    _reverseProxy = protocol: host: port: {
+      inherit protocol host port;
+      _type = "reverseProxy";
+      headers = null;
+    };
+  in {
+    # Header Manipulation
+    addHeader = name: value: content: {
+      inherit name value content;
+      _type = "header";
+    };
+    unsafeAddCrossOriginHeader = lib.clicks.nginx.http.addHeader "Access-Control-Allow-Origin" "*";
+
+    # Location translatable directives
+
+    directory = _directory true;
+    privateDirectory = _directory false;
+
+    file = path: {
+      inherit path;
+      _type = "file";
+    };
+
+    redirect = _redirect false;
+    redirectPermanent = _redirect true;
+
+    reverseProxySecure = _reverseProxy "https";
+    reverseProxy = _reverseProxy "http";
+
+    status = code: {
+      inherit code;
+      _type = "status";
+      headers = null;
+    };
+  };
+}
diff --git a/lib/nginx/http/internal/README.md b/lib/nginx/http/internal/README.md
new file mode 100644
index 0000000..7b0813c
--- /dev/null
+++ b/lib/nginx/http/internal/README.md
@@ -0,0 +1,7 @@
+<!--
+SPDX-FileCopyrightText: 2024 Clicks Codes
+
+SPDX-License-Identifier: GPL-3.0-only
+-->
+
+dag ordering should start at 100 and should be spaced 50 apart
diff --git a/lib/nginx/http/internal/default.nix b/lib/nginx/http/internal/default.nix
new file mode 100644
index 0000000..767bc6f
--- /dev/null
+++ b/lib/nginx/http/internal/default.nix
@@ -0,0 +1,173 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, inputs, ... }: {
+  nginx.http.internal = {
+    generateWwwRedirects = hosts: let
+      withWww = name: host: if host.www
+        then {
+          "${name}" = lib.attrsets.removeAttrs host [ "www" ];
+          "www.${name}" = {
+            routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere (lib.clicks.nginx.http.redirectPermanent "https://${name}/$1");
+            service = null;
+            enableHttp = host.enableHttp;
+            authWith = null;
+          };
+        }
+        else {
+          "${name}" = lib.attrsets.removeAttrs host [ "www" ];
+        };
+    in lib.pipe hosts [
+      (lib.attrsets.mapAttrsToList withWww)
+      (lib.lists.foldr (host: merged: merged // host) {})
+    ];
+
+    assertExactlyOneOfServiceOrRoutes = hosts: let
+      validate = name: host: let
+        routesProvided = host.routes != null && host.routes != {};
+        serviceProvided = host.service != null;
+      in
+        if !serviceProvided && !routesProvided
+        then builtins.throw "${name}: You must provide one of `service` or `routes`"
+        else if serviceProvided && routesProvided
+          then builtins.throw "${name}: You may only provide one of `service` or `routes`"
+          else host;
+    in lib.attrsets.mapAttrs validate hosts;
+
+    assertTailscaleAuthServiceOnly = hosts: let
+      validate = name: host:
+        if host.authWith != "tailscale" || host.routes == null || host.routes == {}
+        then host
+        else builtins.throw "${name}: You may not use tailscale auth when manually specifying `routes`. Please use `service` instead";
+    in lib.attrsets.mapAttrs validate hosts;
+
+    translateServiceToRoutes = hosts: let
+      validate = name: value:
+        lib.trivial.throwIf (value.service != null && value.routes != null && value.routes != {}) "Both `service` and `routes` cannot be set at the same time" value;
+      translate = name: value:
+        lib.attrsets.removeAttrs (
+          if (value.service != null)
+          then value // {
+            routes."/" = lib.home-manager.hm.dag.entryAnywhere value.service;
+          }
+          else value
+        ) [ "service" ];
+    in lib.attrsets.mapAttrs translate
+      (lib.attrsets.mapAttrs validate hosts);
+
+    translateRoutesDagToList = hosts: let
+      hostTranslateRoutesDagToList = name: host: host // {
+        routes = let
+          HMDagRoutes = lib.home-manager.hm.dag.topoSort host.routes;
+        in map ({name, data}: { inherit name; value = data; }) HMDagRoutes.result;
+      };
+    in lib.attrsets.mapAttrs hostTranslateRoutesDagToList hosts;
+
+    unwrapHeaders = hosts: let
+      unwrapHeader = route:
+        if lib.types.isType "header" route.value
+          then unwrapHeader {
+            name = route.name;
+            value = lib.attrsets.recursiveUpdate route.value.content {
+              headers = (route.value.headers or {}) // {
+                "${route.value.name}" = "${route.value.value}";
+              };
+            };
+          }
+          else route;
+
+      hostUnwrapHeaders = name: host: host // {
+        routes = lib.lists.forEach host.routes unwrapHeader;
+      };
+    in lib.attrsets.mapAttrs hostUnwrapHeaders hosts;
+
+    translateRoutesToLocations = hosts: let
+      addHeaderToLocation = header: location:
+        location // {
+          extraConfig = (location.extraConfig or "") + "\nadd_header \"${header.name}\" \"${header.value}\";";
+        };
+      addHeadersToLocation = headers: location:
+        if headers == null
+          then location
+          else lib.trivial.pipe headers [
+            lib.attrsets.attrsToList
+            (lib.lists.foldr addHeaderToLocation location)
+          ];
+      serviceTypeHandlers = {
+        directory = service: {
+          root = service.root;
+          index = "index.html index.htm";
+          extraConfig = lib.strings.optionalString
+            service.listContents
+            "autoindex on;";
+        };
+        file = service: {
+          root = "/";
+          tryFiles = "${service.path} =404";
+        };
+        redirect = service: let
+          statusCode =
+            if service.permanent
+            then "307"
+            else "308";
+        in {
+          return = "${statusCode} ${service.to}";
+        };
+        reverseProxy = service: {
+          proxyPass = "${service.protocol}://${service.host}:${builtins.toString service.port}";
+          proxyWebsockets = true;
+          recommendedProxySettings = true;
+        };
+        status = service: {
+          return = builtins.toString service.statusCode;
+        };
+      };
+      translateServiceToLocation = service: lib.trivial.pipe service.value [
+        serviceTypeHandlers.${service.value._type}
+        (addHeadersToLocation service.value.headers)
+        (location: { name = service.name; value = location; })
+      ];
+      hostTranslateRoutesToLocations = name: host: lib.attrsets.removeAttrs (host // {
+        locations = lib.lists.forEach host.routes translateServiceToLocation;
+      }) [ "routes" ];
+    in lib.attrsets.mapAttrs hostTranslateRoutesToLocations hosts;
+
+    setLocationPriorities = hosts: let
+      priorityByIndex = index: 100 + index * 50;
+      locationWithPriority = index: location: {
+        name = location.name;
+        value = location.value // {
+          priority = location.value.priority or priorityByIndex index;
+        };
+      };
+      setHostLocationPriority = name: host: host // {
+        locations = lib.pipe host.locations [
+          (lib.lists.imap0 locationWithPriority)
+          lib.attrsets.listToAttrs
+        ];
+      };
+    in lib.attrsets.mapAttrs setHostLocationPriority hosts;
+
+    addListenDefaults = hosts: let
+      setDefaults = name: host: lib.attrsets.removeAttrs (host // {
+        onlySSL = !host.enableHttp;
+        addSSL = host.enableHttp;
+
+        useACMEHost = name;
+      }) [ "enableHttp" ];
+    in lib.attrsets.mapAttrs setDefaults hosts;
+
+    serviceTranslation = hosts: lib.trivial.pipe hosts [
+      lib.clicks.nginx.http.internal.assertExactlyOneOfServiceOrRoutes
+      lib.clicks.nginx.http.internal.assertTailscaleAuthServiceOnly
+      lib.clicks.nginx.http.internal.generateWwwRedirects
+      lib.clicks.nginx.http.internal.translateServiceToRoutes
+      lib.clicks.nginx.http.internal.translateRoutesDagToList
+      lib.clicks.nginx.http.internal.unwrapHeaders
+      lib.clicks.nginx.http.internal.translateRoutesToLocations
+      lib.clicks.nginx.http.internal.setLocationPriorities
+      lib.clicks.nginx.http.internal.addListenDefaults
+    ];
+  };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.assertion.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.assertion.spec.nix
new file mode 100644
index 0000000..946ef61
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.assertion.spec.nix
@@ -0,0 +1,71 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+  tailscaleAuthWithRoute = {
+    "calibre.clicks.codes" = {
+      routes."/somePath" = lib.home-manager.hm.dag.entryAnywhere {
+        host = "generic";
+        port = 1024;
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      service = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+  };
+  serviceAndRoutes = {
+    "calibre.clicks.codes" = {
+      routes."/somePath" = lib.home-manager.hm.dag.entryAnywhere {
+        host = "generic";
+        port = 1024;
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      service = {
+        host = "generic";
+        port = 1024;
+        _type = "reverseProxy";
+        headers = null;
+      };
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+  };
+  neitherServiceOrRoutes = {
+    "calibre.clicks.codes" = {
+      routes = null;
+      dnsProvider = "cloudflare";
+      service = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+  };
+in {
+  testAssertTailscaleAuthServiceOnlyFail = {
+    /* This is NOT considered a step, here we test that it passes through input */
+    expr = lib.clicks.nginx.http.internal.assertTailscaleAuthServiceOnly tailscaleAuthWithRoute;
+    expectedError = {
+      type = "ThrownError";
+      msg = "calibre.clicks.codes: You may not use tailscale auth when manually specifying `routes`. Please use `service` instead";
+    };
+  };
+  testAssertExactlyOneOServiceOrRoutesBothProvidedFail = {
+    expr = lib.clicks.nginx.http.internal.assertExactlyOneOfServiceOrRoutes serviceAndRoutes;
+    expectedError = {
+      type = "ThrownError";
+      msg = "calibre.clicks.codes: You may only provide one of `service` or `routes`";
+    };
+  };
+  testAssertExactlyOneOfServiceOrRoutesNeitherProvidedFail = {
+    expr = lib.clicks.nginx.http.internal.assertExactlyOneOfServiceOrRoutes neitherServiceOrRoutes;
+    expectedError = {
+      type = "ThrownError";
+      msg = "calibre.clicks.codes: You must provide one of `service` or `routes`";
+    };
+  };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.design.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.design.spec.nix
new file mode 100644
index 0000000..6710523
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.design.spec.nix
@@ -0,0 +1,238 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+  step0 = {
+    "calibre.clicks.codes" = {
+      service = lib.clicks.nginx.http.reverseProxy "generic" 1024;
+      www = true;
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+  };
+  step1 = {
+    "calibre.clicks.codes" = {
+      service = {
+        host = "generic";
+        port = 1024;
+        protocol = "http";
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      service = null;
+      enableHttp = false;
+    };
+  };
+  step2 = {
+    "calibre.clicks.codes" = {
+      routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+        host = "generic";
+        port = 1024;
+        protocol = "http";
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step3 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step4 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step5 = {
+    "calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "/";
+          value = {
+            proxyPass = "http://generic:1024";
+            proxyWebsockets = true;
+            recommendedProxySettings = true;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            return = "307 https://calibre.clicks.codes/$1";
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step6 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step7 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "calibre.clicks.codes";
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "www.calibre.clicks.codes";
+      authWith = null;
+    };
+  };
+in {
+  testGenerateWwwRedirects = {
+    expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+    expected = step1;
+  };
+  testTranslateServiceToRoutes = {
+    expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+    expected = step2;
+  };
+  testTranslateRoutesDagToList = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+    expected = step3;
+  };
+  testUnwrapHeaderPassthrough = {
+    expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+    expected = step4;
+  };
+  testTranslateRoutesToLocations = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+    expected = step5;
+  };
+  testSetLocationPriorities = {
+    expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+    expected = step6;
+  };
+  testAddListenDefaults = {
+    expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+    expected = step7;
+  };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.header.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.header.spec.nix
new file mode 100644
index 0000000..97756f4
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.header.spec.nix
@@ -0,0 +1,258 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+  step0 = {
+    "calibre.clicks.codes" = {
+      service = lib.clicks.nginx.http.unsafeAddCrossOriginHeader (lib.clicks.nginx.http.reverseProxy "generic" 1024);
+      www = true;
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+  };
+  step1 = {
+    "calibre.clicks.codes" = {
+      service = {
+        _type = "header";
+        name = "Access-Control-Allow-Origin";
+        value = "*";
+        content = {
+          host = "generic";
+          port = 1024;
+          protocol = "http";
+          _type = "reverseProxy";
+          headers = null;
+        };
+      };
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      service = null;
+      enableHttp = false;
+    };
+  };
+  step2 = {
+    "calibre.clicks.codes" = {
+      routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+        _type = "header";
+        name = "Access-Control-Allow-Origin";
+        value = "*";
+        content = {
+          host = "generic";
+          port = 1024;
+          protocol = "http";
+          _type = "reverseProxy";
+          headers = null;
+        };
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step3 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            _type = "header";
+            name = "Access-Control-Allow-Origin";
+            value = "*";
+            content = {
+              host = "generic";
+              port = 1024;
+              protocol = "http";
+              _type = "reverseProxy";
+              headers = null;
+            };
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step4 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = {
+              "Access-Control-Allow-Origin" = "*";
+            };
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step5 = {
+    "calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "/";
+          value = {
+            proxyPass = "http://generic:1024";
+            proxyWebsockets = true;
+            recommendedProxySettings = true;
+            extraConfig = "\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            return = "307 https://calibre.clicks.codes/$1";
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step6 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+        extraConfig = "\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step7 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+        extraConfig = "\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "calibre.clicks.codes";
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "www.calibre.clicks.codes";
+      authWith = null;
+    };
+  };
+in {
+  testGenerateWwwRedirects = {
+    expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+    expected = step1;
+  };
+  testTranslateServiceToRoutes = {
+    expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+    expected = step2;
+  };
+  testTranslateRoutesDagToList = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+    expected = step3;
+  };
+  testUnwrapHeader = {
+    expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+    expected = step4;
+  };
+  testTranslateRoutesToLocations = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+    expected = step5;
+  };
+  testSetLocationPriorities = {
+    expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+    expected = step6;
+  };
+  testAddListenDefaults = {
+    expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+    expected = step7;
+  };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.headers.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.headers.spec.nix
new file mode 100644
index 0000000..66ced1d
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.headers.spec.nix
@@ -0,0 +1,277 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+  step0 = {
+    "calibre.clicks.codes" = {
+      service = lib.pipe (lib.clicks.nginx.http.reverseProxy "generic" 1024) [
+        lib.clicks.nginx.http.unsafeAddCrossOriginHeader
+        (lib.clicks.nginx.http.addHeader "X-Clacks-Overhead" "GNU Terry Pratchett")
+      ];
+      www = true;
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+  };
+  step1 = {
+    "calibre.clicks.codes" = {
+      service = {
+        _type = "header";
+        name = "X-Clacks-Overhead";
+        value = "GNU Terry Pratchett";
+        content = {
+          _type = "header";
+          name = "Access-Control-Allow-Origin";
+          value = "*";
+          content = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        };
+      };
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      service = null;
+      enableHttp = false;
+    };
+  };
+  step2 = {
+    "calibre.clicks.codes" = {
+      routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+        _type = "header";
+        name = "X-Clacks-Overhead";
+        value = "GNU Terry Pratchett";
+        content = {
+          _type = "header";
+          name = "Access-Control-Allow-Origin";
+          value = "*";
+          content = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        };
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step3 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            _type = "header";
+            name = "X-Clacks-Overhead";
+            value = "GNU Terry Pratchett";
+            content = {
+              _type = "header";
+              name = "Access-Control-Allow-Origin";
+              value = "*";
+              content = {
+                host = "generic";
+                port = 1024;
+                protocol = "http";
+                _type = "reverseProxy";
+                headers = null;
+              };
+            };
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step4 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = {
+              "Access-Control-Allow-Origin" = "*";
+              "X-Clacks-Overhead" = "GNU Terry Pratchett";
+            };
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step5 = {
+    "calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "/";
+          value = {
+            proxyPass = "http://generic:1024";
+            proxyWebsockets = true;
+            recommendedProxySettings = true;
+            extraConfig = "\nadd_header \"X-Clacks-Overhead\" \"GNU Terry Pratchett\";\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            return = "307 https://calibre.clicks.codes/$1";
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step6 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+        extraConfig = "\nadd_header \"X-Clacks-Overhead\" \"GNU Terry Pratchett\";\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step7 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+        extraConfig = "\nadd_header \"X-Clacks-Overhead\" \"GNU Terry Pratchett\";\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "calibre.clicks.codes";
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "www.calibre.clicks.codes";
+      authWith = null;
+    };
+  };
+in {
+  testGenerateWwwRedirects = {
+    expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+    expected = step1;
+  };
+  testTranslateServiceToRoutes = {
+    expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+    expected = step2;
+  };
+  testTranslateRoutesDagToList = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+    expected = step3;
+  };
+  testUnwrapHeader = {
+    expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+    expected = step4;
+  };
+  testTranslateRoutesToLocations = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+    expected = step5;
+  };
+  testSetLocationPriorities = {
+    expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+    expected = step6;
+  };
+  testAddListenDefaults = {
+    expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+    expected = step7;
+  };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.http.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.http.spec.nix
new file mode 100644
index 0000000..4f56d96
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.http.spec.nix
@@ -0,0 +1,238 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+  step0 = {
+    "calibre.clicks.codes" = {
+      service = lib.clicks.nginx.http.reverseProxy "generic" 1024;
+      www = true;
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = true;
+    };
+  };
+  step1 = {
+    "calibre.clicks.codes" = {
+      service = {
+        host = "generic";
+        port = 1024;
+        protocol = "http";
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = true;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      service = null;
+      enableHttp = true;
+    };
+  };
+  step2 = {
+    "calibre.clicks.codes" = {
+      routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+        host = "generic";
+        port = 1024;
+        protocol = "http";
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = true;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      enableHttp = true;
+    };
+  };
+  step3 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = true;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = true;
+    };
+  };
+  step4 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "http";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = true;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = true;
+    };
+  };
+  step5 = {
+    "calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "/";
+          value = {
+            proxyPass = "http://generic:1024";
+            proxyWebsockets = true;
+            recommendedProxySettings = true;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = true;
+    };
+    "www.calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            return = "307 https://calibre.clicks.codes/$1";
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = true;
+    };
+  };
+  step6 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = true;
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      authWith = null;
+      enableHttp = true;
+    };
+  };
+  step7 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "http://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+      };
+      onlySSL = false;
+      addSSL = true;
+      useACMEHost = "calibre.clicks.codes";
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      onlySSL = false;
+      addSSL = true;
+      useACMEHost = "www.calibre.clicks.codes";
+      authWith = null;
+    };
+  };
+in {
+  testGenerateWwwRedirects = {
+    expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+    expected = step1;
+  };
+  testTranslateServiceToRoutes = {
+    expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+    expected = step2;
+  };
+  testTranslateRoutesDagToList = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+    expected = step3;
+  };
+  testUnwrapHeaderPassthrough = {
+    expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+    expected = step4;
+  };
+  testTranslateRoutesToLocations = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+    expected = step5;
+  };
+  testSetLocationPriorities = {
+    expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+    expected = step6;
+  };
+  testAddListenDefaults = {
+    expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+    expected = step7;
+  };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.https-proxy.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.https-proxy.spec.nix
new file mode 100644
index 0000000..77ad490
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.https-proxy.spec.nix
@@ -0,0 +1,238 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+  step0 = {
+    "calibre.clicks.codes" = {
+      service = lib.clicks.nginx.http.reverseProxySecure "generic" 1024;
+      www = true;
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+  };
+  step1 = {
+    "calibre.clicks.codes" = {
+      service = {
+        host = "generic";
+        port = 1024;
+        protocol = "https";
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      routes = null;
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      service = null;
+      enableHttp = false;
+    };
+  };
+  step2 = {
+    "calibre.clicks.codes" = {
+      routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+        host = "generic";
+        port = 1024;
+        protocol = "https";
+        _type = "reverseProxy";
+        headers = null;
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+        to = "https://calibre.clicks.codes/$1";
+        permanent = true;
+        _type = "redirect";
+        headers = null;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step3 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "https";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step4 = {
+    "calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "/";
+          value = {
+            host = "generic";
+            port = 1024;
+            protocol = "https";
+            _type = "reverseProxy";
+            headers = null;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      routes = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            to = "https://calibre.clicks.codes/$1";
+            permanent = true;
+            _type = "redirect";
+            headers = null;
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step5 = {
+    "calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "/";
+          value = {
+            proxyPass = "https://generic:1024";
+            proxyWebsockets = true;
+            recommendedProxySettings = true;
+          };
+        }
+      ];
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations = [
+        {
+          name = "~ ^/?([^\r\n]*)$";
+          value = {
+            return = "307 https://calibre.clicks.codes/$1";
+          };
+        }
+      ];
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step6 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "https://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+      };
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+      enableHttp = false;
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      authWith = null;
+      enableHttp = false;
+    };
+  };
+  step7 = {
+    "calibre.clicks.codes" = {
+      locations."/" = {
+        priority = 100;
+        proxyPass = "https://generic:1024";
+        proxyWebsockets = true;
+        recommendedProxySettings = true;
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "calibre.clicks.codes";
+      dnsProvider = "cloudflare";
+      authWith = "tailscale";
+    };
+    "www.calibre.clicks.codes" = {
+      locations."~ ^/?([^\r\n]*)$" = {
+        return = "307 https://calibre.clicks.codes/$1";
+        priority = 100;
+      };
+      onlySSL = true;
+      addSSL = false;
+      useACMEHost = "www.calibre.clicks.codes";
+      authWith = null;
+    };
+  };
+in {
+  testGenerateWwwRedirects = {
+    expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+    expected = step1;
+  };
+  testTranslateServiceToRoutes = {
+    expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+    expected = step2;
+  };
+  testTranslateRoutesDagToList = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+    expected = step3;
+  };
+  testUnwrapHeaderPassthrough = {
+    expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+    expected = step4;
+  };
+  testTranslateRoutesToLocations = {
+    expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+    expected = step5;
+  };
+  testSetLocationPriorities = {
+    expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+    expected = step6;
+  };
+  testAddListenDefaults = {
+    expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+    expected = step7;
+  };
+}
diff --git a/lib/nginx/http/lib.nginx.http.status-code.spec.nix b/lib/nginx/http/lib.nginx.http.status-code.spec.nix
new file mode 100644
index 0000000..67b4d86
--- /dev/null
+++ b/lib/nginx/http/lib.nginx.http.status-code.spec.nix
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: {
+  testSuccess = {
+    expr = lib.clicks.nginx.http.status 200;
+    expected = {
+      code = 200;
+      _type = "status";
+      headers = null;
+    };
+  };
+  testNotFound = {
+    expr = lib.clicks.nginx.http.status 404;
+    expected = {
+      code = 404;
+      _type = "status";
+      headers = null;
+    };
+  };
+}
diff --git a/lib/nginx/stream/default.nix b/lib/nginx/stream/default.nix
new file mode 100644
index 0000000..d6a3183
--- /dev/null
+++ b/lib/nginx/stream/default.nix
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, inputs, ... }:
+{
+  nginx.stream = {
+    proxyStream = external: internal: protocol: {
+      inherit external internal protocol;
+      haproxy = true;
+    };
+    stream = external: internal: protocol: {
+      inherit external internal protocol;
+      haproxy = false;
+    };
+  };
+}
diff --git a/lib/types/modules/README.md b/lib/types/modules/README.md
new file mode 100644
index 0000000..b13a8af
--- /dev/null
+++ b/lib/types/modules/README.md
@@ -0,0 +1,60 @@
+<!--
+Copyright (c) 2003-2016 Eelco Dolstra and the Nixpkgs/NixOS contributors
+SPDX-FileCopyrightText: 2024 Clicks Codes
+
+SPDX-License-Identifier: MIT
+-->
+
+# lib.clicks.types.modules.taggedSubmodule
+
+This is a cleaned-up version of the type proposed in https://github.com/NixOS/nixpkgs/pull/254790, and as-such is under the same license as nixpkgs
+
+{ *`types`*, *`specialArgs`* ? {} }
+
+This is like `types.oneOf`, but takes an attrsSet of submodule in types. Those need to have a type option
+    which is used to find the correct submodule.
+
+::: {#ex-tagged-submodule .example}
+### Tagged submodules
+```nix
+let
+  submoduleA = submodule {
+    options = {
+      _type = mkOption {
+        type = str;
+      };
+      foo = mkOption {
+        type = int;
+      };
+    };
+  };
+  submoduleB = submodule {
+    options = {
+      _type = mkOption {
+        type = str;
+      };
+      bar = mkOption {
+        type = int;
+      };
+    };
+  };
+in
+options.mod = mkOption {
+  type = attrsOf (taggedSubmodule {
+    types = {
+      a = submoduleA;
+      b = submoduleB;
+    };
+  });
+};
+config.mod = {
+  someA = {
+    _type = "a";
+    foo = 123;
+  };
+  someB = {
+    _type = "b";
+    bar = 456;
+  };
+};
+```
diff --git a/lib/types/modules/default.nix b/lib/types/modules/default.nix
new file mode 100644
index 0000000..d9d11d3
--- /dev/null
+++ b/lib/types/modules/default.nix
@@ -0,0 +1,26 @@
+# Copyright (c) 2003-2016 Eelco Dolstra and the Nixpkgs/NixOS contributors
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: MIT
+
+{
+  lib,
+  ...
+}: {
+  types.modules = {
+    taggedSubmodule =
+      { types
+      , specialArgs ? {}
+      }: lib.types.mkOptionType rec {
+      name = "taggedSubmodule";
+      description = "submodules tagged by ._type";
+      check = x: if x ? _type then types.${x._type}.check x else throw "No ._type option set in:\n${lib.generators.toPretty {} x}";
+      merge = loc: lib.lists.foldl'
+        (res: def: types.${def.value._type}.merge loc [
+          (lib.attrsets.recursiveUpdate { value._module.args = specialArgs; } def)
+        ])
+        { };
+      nestedTypes = types;
+    };
+  };
+}
diff --git a/lib/types/nginx/README.md b/lib/types/nginx/README.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/types/nginx/README.md
diff --git a/lib/types/nginx/default.nix b/lib/types/nginx/default.nix
new file mode 100644
index 0000000..3b48b7a
--- /dev/null
+++ b/lib/types/nginx/default.nix
@@ -0,0 +1,157 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{
+  lib,
+  ...
+}: {
+  types.nginx = {
+    host = config: lib.types.submodule (
+      { ... }@submodule: {
+        options = {
+          dnsProvider = lib.mkOption {
+            type = lib.types.nullOr lib.types.str;
+            description = "DNS Provider for this host's / these hosts certificate";
+            default = config.security.acme.defaults.dnsProvider;
+          };
+          www = lib.mkOption {
+            type = lib.types.bool;
+            description = "Automatically create www.<address>";
+            default = true;
+          };
+          service = lib.mkOption {
+            type = lib.types.nullOr (lib.clicks.types.modules.taggedSubmodule {
+              types = lib.clicks.types.nginx.services;
+            });
+            description = "Service which sits at \"/\"";
+            default = null;
+          };
+          routes = lib.mkOption {
+            type = lib.types.nullOr (lib.home-manager.hm.types.dagOf (lib.clicks.types.modules.taggedSubmodule {
+              types = lib.clicks.types.nginx.services;
+            }));
+            description = "An attrset of paths to the service wanted";
+            default = null;
+          };
+          authWith = lib.mkOption {
+            type = lib.types.nullOr (lib.types.enum [ "tailscale" ]);
+            description = "Authenticate with this service on the nginx level";
+            default = null;
+          };
+          enableHttp = lib.mkEnableOption "Listen additionally on port 80";
+        };
+      }
+    );
+    services = {
+      reverseProxy = lib.types.submodule {
+        options = {
+          host = lib.mkOption {
+            type = lib.types.str;
+            description = "Local host of the service";
+            default = "127.0.0.1";
+          };
+          port = lib.mkOption {
+            type = lib.types.port;
+            description = "Port the service is running on";
+          };
+          protocol = lib.mkOption {
+            type = lib.types.str;
+            description = "Protocol to connect with, normally 'http'";
+            default = "http";
+          };
+          _type = lib.mkOption {
+            type = lib.types.str;
+            internal = true;
+            description = "The type of service";
+            default = "reverseProxy";
+          };
+          headers = lib.mkOption {
+            type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+            description = "Headers and their values";
+            default = null;
+          };
+        };
+      };
+      redirect = lib.types.submodule {
+        options = {
+          to = lib.mkOption {
+            type = lib.types.str;
+            description = "Where to redirect to";
+          };
+          permanent = lib.mkEnableOption "Make this redirect with a HTTP 308 Permanent Redirect rather than 307 Temporary Redirect";
+          _type = lib.mkOption {
+            type = lib.types.str;
+            internal = true;
+            description = "The type of service";
+            default = "redirect";
+          };
+          headers = lib.mkOption {
+            type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+            description = "Headers and their values";
+            default = null;
+          };
+        };
+      };
+      directory = lib.types.submodule {
+        options = {
+          root = lib.mkOption {
+            type = lib.types.str;
+            description = "The root path of the directory";
+          };
+          listContents = lib.mkEnableOption "Make this directory and subdirectories give a listing when there's no index file";
+          _type = lib.mkOption {
+            type = lib.types.str;
+            internal = true;
+            description = "The type of service";
+            default = "directory";
+          };
+          headers = lib.mkOption {
+            type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+            description = "Headers and their values";
+            default = null;
+          };
+        };
+      };
+      file = lib.types.submodule {
+        options = {
+          path = lib.mkOption {
+            type = lib.types.str;
+            description = "Path to the file";
+          };
+          _type = lib.mkOption {
+            type = lib.types.str;
+            internal = true;
+            description = "The type of service";
+            default = "file";
+          };
+          headers = lib.mkOption {
+            type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+            description = "Headers and their values";
+            default = null;
+          };
+        };
+      };
+      status = lib.types.submodule {
+        options = {
+          code = lib.mkOption {
+            type = lib.types.number;
+            description = "HTTP status code to return";
+            default = 200;
+          };
+          _type = lib.mkOption {
+            type = lib.types.str;
+            internal = true;
+            description = "The type of service";
+            default = "status";
+          };
+          headers = lib.mkOption {
+            type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+            description = "Headers and their values";
+            default = null;
+          };
+        };
+      };
+    };
+  };
+}
diff --git a/modules/nixos/clicks/security/acme/default.nix b/modules/nixos/clicks/security/acme/default.nix
index 655e39f..7acb887 100644
--- a/modules/nixos/clicks/security/acme/default.nix
+++ b/modules/nixos/clicks/security/acme/default.nix
@@ -11,16 +11,33 @@
   options.clicks.security.acme = {
     enable = lib.mkEnableOption "Acme defaults";
 
-    email = lib.mkOption {
-      type = lib.types.str;
-      default = "";
-      description = "Email address to use for Let's Encrypt registration.";
-    };
-
     staging = lib.mkOption {
       type = lib.types.bool;
       default = false;
-      description = "Use the Let's Encrypt staging server.";
+      description = "Use the Let's Encrypt staging server";
+    };
+
+    defaults = {
+      email = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = "Email address to use for Let's Encrypt registration";
+      };
+
+      dnsProvider = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        description = "Default provider for getting web certificates";
+        default = config.clicks.services.nginx.defaultDnsProvider;
+      };
+
+      environmentFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default =
+          if config.clicks.security.acme.defaults.dnsProvider == null
+          then null
+          else throw "config.clicks.security.acme: You should provide an environment file default (or explicitly set to null) if you are using a DNS provider";
+        description = "Environment file containing DNS provider credentials";
+      };
     };
   };
 
@@ -29,7 +46,7 @@
       acceptTerms = true;
 
       defaults = {
-        inherit (cfg) email;
+        inherit (cfg.defaults) email dnsProvider environmentFile;
 
         group = lib.mkIf config.services.nginx.enable "nginx";
         server = lib.mkIf cfg.staging "https://acme-staging-v02.api.letsencrypt.org/directory";
diff --git a/modules/nixos/clicks/services/nginx/default.nix b/modules/nixos/clicks/services/nginx/default.nix
new file mode 100644
index 0000000..90d6405
--- /dev/null
+++ b/modules/nixos/clicks/services/nginx/default.nix
@@ -0,0 +1,90 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, config, ... }:
+let
+  cfg = config.clicks.services.nginx;
+in
+{
+  options.clicks.services.nginx = {
+    enable = lib.mkEnableOption "Enable Nginx routing";
+    hosts = lib.options.mkOption {
+      type = lib.types.attrsOf (lib.clicks.types.nginx.host config);
+      description = "Attrset of web domain to host data";
+      default = {};
+    };
+    defaultDnsProvider = lib.options.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      description = "Default provider for getting web certificates";
+      default = null;
+    };
+    xClacksOverhead.enable = lib.options.mkOption {
+      type = lib.types.bool;
+      description = "Write the header `X-Clacks-Overhead: GNU Terry Pratchett` on all virtual host locations";
+      default = true;
+    };
+  };
+
+  config = lib.modules.mkIf cfg.enable (let
+    processedHosts = lib.clicks.nginx.http.internal.serviceTranslation cfg.hosts;
+    hostsList = lib.attrsets.attrsToList processedHosts;
+    nginxHosts = lib.attrsets.mapAttrs (_: host: lib.attrsets.removeAttrs host [ "authWith" "dnsProvider" ]) processedHosts;
+    acmeCerts = lib.attrsets.mapAttrs (_: host: {
+      inherit (host) dnsProvider;
+      webroot = if host.dnsProvider == null
+                then config.security.acme.defaults.webroot
+                else null;
+    }) processedHosts;
+    tailscaleAuthHosts = lib.pipe hostsList [
+      (lib.lists.filter (host: host.value.authWith == "tailscale"))
+      (map (host: host.name))
+    ];
+  in {
+    services.nginx = {
+      enable = true;
+      enableReload = true;
+      virtualHosts = {
+        "default_server_ssl" = {
+          listen = [
+            {
+              ssl = true;
+              port = 443;
+              addr = "0.0.0.0";
+              extraParameters = [
+                "default_server"
+              ];
+            }
+          ];
+
+          rejectSSL = true;
+        };
+        "default_server" = {
+          listen = [
+            {
+              port = 80;
+              addr = "0.0.0.0";
+              extraParameters = [
+                "default_server"
+              ];
+            }
+          ];
+
+          locations."/" = {
+            return = 444;
+          };
+        };
+      } // nginxHosts;
+    };
+
+    security.acme.certs = acmeCerts;
+
+    clicks.services.tailscaleAuth = lib.mkIf (lib.lists.length tailscaleAuthHosts > 0) {
+      enable = true;
+
+      hosts = tailscaleAuthHosts;
+    };
+
+    networking.firewall.allowedTCPPorts = [ 80 443 ];
+  });
+}
diff --git a/modules/nixos/clicks/services/nginx/tailscaleAuth/default.nix b/modules/nixos/clicks/services/nginx/tailscaleAuth/default.nix
new file mode 100644
index 0000000..af9e07f
--- /dev/null
+++ b/modules/nixos/clicks/services/nginx/tailscaleAuth/default.nix
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, config, ... }:
+let
+  cfg = config.clicks.services.tailscaleAuth;
+in
+{
+  options.clicks.services.tailscaleAuth = {
+    enable = lib.mkEnableOption "Enable tailscaleAuth for Nginx";
+    expectedTailnet = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      description = "The tailnet to expect when authenticating";
+      default =  null;
+    };
+    hosts = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      description = "A list of hosts to put behind tailscale auth";
+      default = [];
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.expectedTailnet == null || lib.clicks.strings.endsWith ".ts.net" cfg.expectedTailnet;
+        message = "Your expected tailnet must be an official *.ts.net tailnet, headscale is not supported";
+      }
+    ];
+
+    services.nginx.tailscaleAuth = {
+      enable = true;
+      expectedTailnet = lib.modules.mkIf (cfg.expectedTailnet != null) cfg.expectedTailnet;
+
+      virtualHosts = cfg.hosts;
+    };
+  };
+}
diff --git a/overlays/tailscale-nginx-auth/default.nix b/overlays/tailscale-nginx-auth/default.nix
new file mode 100644
index 0000000..320c936
--- /dev/null
+++ b/overlays/tailscale-nginx-auth/default.nix
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{...}: final: prev: {
+  tailscale-nginx-auth = prev.tailscale-nginx-auth.overrideAttrs (prevAttrs: {
+    patches = (prevAttrs.patches or []) ++ [
+      ./fix-headscale-computedname.patch
+    ];
+  });
+}
diff --git a/overlays/tailscale-nginx-auth/fix-headscale-computedname.patch b/overlays/tailscale-nginx-auth/fix-headscale-computedname.patch
new file mode 100644
index 0000000..5fa3c44
--- /dev/null
+++ b/overlays/tailscale-nginx-auth/fix-headscale-computedname.patch
@@ -0,0 +1,30 @@
+From 74a12f91542ee1323b69675b0480168a22ad5b17 Mon Sep 17 00:00:00 2001
+From: Skyler Grey <minion@clicks.codes>
+Date: Sat, 29 Jun 2024 14:16:46 +0000
+Subject: [PATCH] fix(nginx): Continue if node Name is ComputedName
+
+In the headscale case, the node Name ends up the same as the
+ComputedName. This causes tailscale-nginx-auth to fail extracting the
+tailnet name and 403 all devices.
+
+This patch skips tailnet extraction in this case.
+
+Signed-off-by: Skyler Grey <minion@clicks.codes>
+---
+ cmd/nginx-auth/nginx-auth.go | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/cmd/nginx-auth/nginx-auth.go b/cmd/nginx-auth/nginx-auth.go
+index d2e9468e558ea..c6a8ff30fbb8a 100644
+--- a/cmd/nginx-auth/nginx-auth.go
++++ b/cmd/nginx-auth/nginx-auth.go
+@@ -66,7 +66,7 @@ func main() {
+ 		// will be empty because the tailnet of the sharee is not exposed.
+ 		var tailnet string
+ 
+-		if !info.Node.Hostinfo.ShareeNode() {
++		if !info.Node.Hostinfo.ShareeNode() && info.Node.Name != info.Node.ComputedName {
+ 			var ok bool
+ 			_, tailnet, ok = strings.Cut(info.Node.Name, info.Node.ComputedName+".")
+ 			if !ok {
+
diff --git a/overlays/tailscale-nginx-auth/fix-headscale-computedname.patch.license b/overlays/tailscale-nginx-auth/fix-headscale-computedname.patch.license
new file mode 100644
index 0000000..5ca0412
--- /dev/null
+++ b/overlays/tailscale-nginx-auth/fix-headscale-computedname.patch.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Clicks Codes
+
+SPDX-License-Identifier: BSD-3-Clause
diff --git a/systems/x86_64-linux/teal/acme.sops.env.bin b/systems/x86_64-linux/teal/acme.sops.env.bin
new file mode 100644
index 0000000..c66e26e
--- /dev/null
+++ b/systems/x86_64-linux/teal/acme.sops.env.bin
@@ -0,0 +1,36 @@
+{
+	"data": "ENC[AES256_GCM,data:HgebCH+Hrzbu3pvXbWa66OMKEEy8uzkutqO0oSrj1ZgDuZnU/GHT/AZhd8NptUKIOIerSjWFxD4tZSMyYqOwj2c2,iv:7G1mmGkYDX24wlKqdGLTxBQvkRcPpSlA/J8IHJsyJZE=,tag:ah199Tfk3E60v2wBlb+sOg==,type:str]",
+	"sops": {
+		"kms": null,
+		"gcp_kms": null,
+		"azure_kv": null,
+		"hc_vault": null,
+		"age": null,
+		"lastmodified": "2024-06-22T23:34:29Z",
+		"mac": "ENC[AES256_GCM,data:jTCygJEQDbIpPBwU7xmlkqfntkautpQDEnvVchWzFq8QnzWCPV1/P/qeSayPjkwPAnB24x/wbFkuHCnNVamQ/QxNiuEVk8c977DYzdl+Hg/7MED4O/kExMzdU6cHQGtkKn3cXWatJNpZQVe5lko3xbhJN/JQwRFYYnzZKSN906Q=,iv:Mo0vwHkvFxvOQRUPnorLhJ476l8ZMQvgZ4wSyss4j3c=,tag:QOpuNQRe1+ZtoAVVAO3kyQ==,type:str]",
+		"pgp": [
+			{
+				"created_at": "2024-06-22T23:23:10Z",
+				"enc": "-----BEGIN PGP MESSAGE-----\n\nhF4D6MHlIv4I/7ASAQdAygO+vRQVedxDSif6TnM4dI3OyMqTGqMaI2iBBIouKSkw\naxAi2caNG2Kkelgj1JMmlxV31wbtIMGWp3N2LhTAxcFX+N0idIDLrdF6aVjwMZaJ\n0l4BPkHzwA/jjIgMD5PurgGmarGiZkaXv0cOikEXhBaK52Kn849JjHt3hk0QZcIJ\n1PpLoatM8kwdJpJKrxePXWgmLGFlrv9Bza4Ephzfq2RzaUkS6eE6q7tKzSo2gFuj\n=UIIi\n-----END PGP MESSAGE-----",
+				"fp": "BC82DF237610AE9113EB075900E944BFBE99ADB5"
+			},
+			{
+				"created_at": "2024-06-22T23:23:10Z",
+				"enc": "-----BEGIN PGP MESSAGE-----\n\nhF4DxpBiwsu2o5wSAQdAw1CzYtLdBt3Wyn5VSl6WdCwrabGrFuFn1YoyTk2kUHkw\nrBYHoAFbhtSk0Kh5sEq4MbelLD8U8Vc4sWQ+uCBIP+IB3JqdFainNA3BgIX0xmuZ\n0l4Bp/Tim//p65+OYdtNXygpoK0QlM+jrcloND/fpbJ5DWEyKkPSHuDXTNXAa268\n9xLW2H3LhRimN/5y6hoh7QIT3WxAQoKkGRLruqWAvFq2fjyHAfepsu9xE1S80Jae\n=B3Aw\n-----END PGP MESSAGE-----",
+				"fp": "76E0B09A741C4089522111E5F27E3E5922772E7A"
+			},
+			{
+				"created_at": "2024-06-22T23:23:10Z",
+				"enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMA9bzf+GUl7kkARAAt+5GEtj4hbrZPD1kRaoFBjdtZDESlGHZgug88YHjyXTq\nDjlTheDs65ZfHBRhE+3OzsqLU8QwLyACB8MNJdnns6sn/ssV+4UOwIi6MRfth+P1\nlbqnSfsS9octnz91JPYmqZ1s65Qp9VkM/D04TV7OdreZzU1aIOI241u0JOTYgBat\nD5E9QjQGlPwwYWwhlt+r0uIMISa4lwJIWuud2Xm4lJ1JPrzuuJB6VJp3D3eaJNoE\nms1NMvSJTn1Q/6NKSyeSD+901oeJRrtoikGbk4y4r4UlqSUsQhW/AptgswMRnkfg\nyI2SmtD2EC79g2h9MATwQfxgo1maMu47FPNx0zI2vmZdp+5LKeSWbe5RuNK11SCo\nnpyLKRqrtsXlKu0MfFg0+fJ1xqqMjvdGlPj5lo/T5ng4boyTwAgDmn8/rCHlS9yu\nbQpKOzH2dnOB1CXPWEt/kj9wXHUTgygfasOCpn60eMKcyOuSXn0qJJj9Mc3A0Jw3\nD1MPNFnnrnGTa7rWyRWQRYLZLNpZV4MzgIF/g3eJuhfJRJDpAFJmu/XY59RsCjfd\npOW7NYpEwH2KHGv1u0e4EnZRysKNqMqJ/Y3PYSyhdquAwxFxqMCRkmYYheNpvjP8\nPsJXv77KM+O1RGTsEX/IKoGnnBcOlUBNEVMIaUOK3E8jVCxeCGXlesK/xT81MBHS\nXgF523bW5yt5jQ0+gyCNW7RuDRiu/E24bJcqqNYAkhJRlysDBRcQs2vdDuw5+xbP\nF6fc7UT19SEA2KzeAXdQNtSKMsOuwPBBluZpXpjRmdqscYHrcScegRmEbLQsdTs=\n=CRaE\n-----END PGP MESSAGE-----",
+				"fp": "8F50789F12AC6E6206EA870CE5E1C2D43B0E4AB3"
+			},
+			{
+				"created_at": "2024-06-22T23:23:10Z",
+				"enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMAw8Caq2TdS1qARAAhM0snxG2QBGF+kqH46cSfh2egIBfnooi5pSTtR3UBX01\n9B4AmKSC0wv4RcEmYgjS4rlVEkRa+a7V+rhPuIRLKSHvMjfpJKkAqbyawjf8rYR8\nKjh4gGr23+U0tna1TZ1amvZm/fBNfv71Tbb5gTshWnAamuIXevwOyIVlKAVhvAue\ngaLzlbhDbWf1+o1btA3VUdvUvUozrLlg2YJHmrzdyCfmS2SOO7WR9g1PAxJo1yii\ne+1fCQJ4PJOvsxRptuJ3tYS+AVhiHQh0VFU8OPjd9ThPHq4f4yIHu3b/M9a2b0R3\n2I5pVWUFP0/3DqeVg3ovdpaAquSKGJ9KzbHK5CyCHyzQr3AbTbWfH8u1bJdQjVpU\nrTgSSXxyAPf3iCBh4RFhHikNWelBcFcnjPibaxvXhD8zvKgK5RMbl+7OlVMBauxk\n8gKwIihfa78/akChZbZsANWHJd/TErJqc7DUKv0Vit7OUugSSEZ0UanEsVQuRayq\nyBRQsmHmuLwEluF2OP3G5Wn8MjXZ9gm9DgjQjSdn3qL00kIymB3U/fQmW8V0MR/e\nBdAg1WTWLWcAXiVXJvgYPWu6S/NnW4dCD9tZocD8yoqaeUo5BSL1FzFeM1YYZBkk\n0HZuIq9kYxQ5g7AoNmDnR/KoN+FxLipuXxZFg2d7ZV90O/U7JFb7mDCu420nCQTS\nWAFym4eE200cL8bzqho4aM76BnBZD38h7eaDJnG+L7L2E4pzg1bjs6guajx3qbhl\nzb/sclLIrDzV0WfU4X/s1KrIE5E22JwgNMZB26RQ3EG2WabObT/5WIQ=\n=hKk4\n-----END PGP MESSAGE-----",
+				"fp": "67c66d58ac73fd744c2b49720f026aad93752d6a"
+			}
+		],
+		"unencrypted_suffix": "_unencrypted",
+		"version": "3.8.1"
+	}
+}
\ No newline at end of file
diff --git a/systems/x86_64-linux/teal/acme.sops.env.bin.license b/systems/x86_64-linux/teal/acme.sops.env.bin.license
new file mode 100644
index 0000000..bda0f14
--- /dev/null
+++ b/systems/x86_64-linux/teal/acme.sops.env.bin.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2024 Clicks Codes
+
+SPDX-License-Identifier: GPL-3.0-only
diff --git a/systems/x86_64-linux/teal/default.nix b/systems/x86_64-linux/teal/default.nix
index df50319..40a0066 100644
--- a/systems/x86_64-linux/teal/default.nix
+++ b/systems/x86_64-linux/teal/default.nix
@@ -28,7 +28,11 @@
 
       acme = {
         enable = true;
-        email = "minion@clicks.codes";
+        defaults = {
+          email = "minion@clicks.codes";
+          dnsProvider = "cloudflare";
+          environmentFile = config.clicks.secrets."${lib.clicks.secrets.name ./acme.sops.env.bin}".path;
+        };
       };
     };
 
@@ -223,4 +227,6 @@
     file = ./tailscale.sops.json;
     keys = [ "authKey" ];
   };
+
+  clicks.secrets."${lib.clicks.secrets.name ./acme.sops.env.bin}".file = ./acme.sops.env.bin;
 }