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/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;
+ };
+ };
+}