blob: 38a2c749a3e80fe2340d393289cc0cdf2b5ec5b5 [file] [log] [blame]
Skyler Grey2ca6ccd2023-10-14 22:56:43 +00001{ config, lib, pkgs, helpers, base, ... }:
Skyler Greyfe1740c2023-10-21 01:24:18 +00002lib.recursiveUpdate {
Skyler Grey2ca6ccd2023-10-14 22:56:43 +00003 options.clicks = {
4 nginx = {
5 services = lib.mkOption {
6 type = with lib.types;
7 listOf (submodule {
8 options = {
9 host = lib.mkOption { type = str; };
10 extraHosts = lib.mkOption { type = listOf str; };
Skyler Greyf7eb4ae2024-05-06 12:38:44 +000011 specific = lib.mkOption { type = bool; };
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000012 secure = lib.mkOption { type = bool; };
13 service = lib.mkOption {
14 type = let
15 validServiceTypes = {
16 "redirect" = {
Skyler Greyfe1740c2023-10-21 01:24:18 +000017 to = [ "string" str ];
18 permanent = [ "bool" bool ];
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000019 };
Skyler Greyfe1740c2023-10-21 01:24:18 +000020 "reverseproxy" = { to = [ "string" str ]; };
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000021 "php" = {
Skyler Greyfe1740c2023-10-21 01:24:18 +000022 root = [ "string" str ];
23 socket = [ "string" str ];
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000024 };
25 "directory" = {
Skyler Greyfe1740c2023-10-21 01:24:18 +000026 private = [ "bool" bool ];
27 root = [ "string" str ];
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000028 };
Skyler Greyfe1740c2023-10-21 01:24:18 +000029 "file" = { path = [ "string" str ]; };
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000030 "path" = {
Skyler Greyfe1740c2023-10-21 01:24:18 +000031 path = [ "string" str ];
32 service = [ "set" serviceType ];
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000033 };
Skyler Greyfe1740c2023-10-21 01:24:18 +000034 "compose" = { services = [ "list" (listOf serviceType) ]; };
35 "status" = { statusCode = [ "int" int ]; };
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000036 };
37
38 serviceType = mkOptionType {
39 name = "Service";
40
41 description = "clicks Nginx service";
42 descriptionClass = "noun";
43
44 check = (x:
Skyler Greyfe1740c2023-10-21 01:24:18 +000045 if (builtins.typeOf x) != "set" then
46 lib.warn
47 "clicks nginx services must be sets but ${x} is not a set"
48 false
49 else if !(builtins.hasAttr "type" x) then
50 lib.warn
51 "clicks nginx services must have a type attribute but ${x} does not"
52 false
53 else if !(builtins.hasAttr x.type validServiceTypes) then
54 lib.warn
55 "clicks nginx services must have a valid type, but ${x.type} is not one"
56 false
57 else
58 (let
59 optionTypes =
60 (builtins.mapAttrs (n: o: builtins.elemAt o 0)
61 validServiceTypes.${x.type}) // {
62 type = "string";
63 };
64 in (lib.pipe x [
65 (builtins.mapAttrs (n: o:
66 (builtins.hasAttr n optionTypes) && optionTypes.${n}
67 == (builtins.typeOf o)))
68 lib.attrValues
69 (builtins.all (x: x))
70 ]) && (lib.pipe optionTypes [
71 (builtins.mapAttrs (n: _: builtins.hasAttr n x))
72 lib.attrValues
73 (builtins.all (x: x))
74 ])));
Skyler Grey2ca6ccd2023-10-14 22:56:43 +000075 };
76 in serviceType;
77 };
78 type = lib.mkOption { type = strMatching "hosts"; };
79 };
80 });
81 example = lib.literalExpression ''
82 with helpers.nginx; [
83 (Host "example.clicks.codes" (ReverseProxy "generic:1001"))
84 ]'';
85 description = lib.mdDoc ''
86 Connects hostnames to services for your nginx server. We recommend using the Clicks helper to generate these
87 '';
88 default = [ ];
89 };
90 serviceAliases = lib.mkOption {
91 type = with lib.types;
92 listOf (submodule {
93 options = {
94 host = lib.mkOption {
95 type = str;
96 example = "example.clicks.codes";
97 description = ''
98 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
99 '';
100 };
101 aliases = lib.mkOption {
102 type = listOf str;
103 example = [ "example2.clicks.codes" "example.coded.codes" ];
104 description = ''
105 A list of servers to add as aliases
106 '';
107 };
108 type = lib.mkOption { type = strMatching "aliases"; };
109 };
110 });
111 example = lib.literalExpression ''
112 with helpers.nginx; [
113 (Host "example.clicks.codes" (ReverseProxy "generic:1001"))
114 ]'';
115 description = lib.mdDoc ''
116 Adds additional host names to your nginx server. If you're using `clicks.nginx.services`
117 you should generally use a Hosts block instead
118 '';
119 default = [ ];
120 };
121 streams = lib.mkOption {
Skyler Greyfe1740c2023-10-21 01:24:18 +0000122 type = with lib.types;
123 listOf (submodule {
124 options = {
125 internal = lib.mkOption { type = str; };
126 external = lib.mkOption { type = port; };
127 protocol = lib.mkOption { type = strMatching "^(tcp|udp)$"; };
Skyler Grey56b293d2023-10-22 22:53:34 +0000128 haproxy = lib.mkOption { type = bool; };
Skyler Greyfe1740c2023-10-21 01:24:18 +0000129 };
130 });
Skyler Grey2ca6ccd2023-10-14 22:56:43 +0000131 example = lib.literalExpression ''
132 with helpers.nginx; [
133 (Stream 1001 "generic:1002" "tcp")
134 ]'';
135 description = lib.mdDoc ''
136 A list of servers to be placed in the nginx streams block. We recommend using the Clicks helper to generate these
137 '';
138 default = [ ];
139 };
140 };
141 };
142 config = {
143 services.nginx = {
144 enable = true;
145 enableReload = true;
146
Skyler Grey896e9282023-12-22 23:49:10 +0000147 serverNamesHashMaxSize = 4096;
148
Skyler Grey2ca6ccd2023-10-14 22:56:43 +0000149 virtualHosts = lib.recursiveUpdate (helpers.nginx.Merge
150 config.clicks.nginx.services) # clicks.nginx.services
151 (lib.pipe config.clicks.nginx.serviceAliases [
152 (map (alias: {
153 name = alias.host;
154 value.serverAliases = alias.aliases;
155 }))
156 builtins.listToAttrs
157 ]); # clicks.nginx.serviceAliases
158
159 streamConfig = builtins.concatStringsSep "\n" (map (stream: ''
160 server {
Skyler Greyfe1740c2023-10-21 01:24:18 +0000161 listen ${builtins.toString stream.external}${
162 lib.optionalString (stream.protocol == "udp") " udp"
163 };
Skyler Grey56b293d2023-10-22 22:53:34 +0000164 proxy_pass ${stream.internal};
165 ${if stream.haproxy then "proxy_protocol on;" else ""}
Skyler Grey2ca6ccd2023-10-14 22:56:43 +0000166 }
167 '') config.clicks.nginx.streams);
168 };
169
170 networking.firewall.allowedTCPPorts = lib.pipe config.clicks.nginx.streams [
171 (builtins.filter (stream: stream.protocol == "tcp"))
172 (map (stream: stream.external))
173 ];
174 networking.firewall.allowedUDPPorts = lib.pipe config.clicks.nginx.streams [
175 (builtins.filter (stream: stream.protocol == "udp"))
176 (map (stream: stream.external))
177 ];
178
179 security.acme.defaults = {
180 email = "admin@clicks.codes";
Skyler Grey915067d2023-12-03 13:46:53 +0000181 environmentFile = config.sops.secrets.cloudflare_cert__api_token.path;
Skyler Grey2ca6ccd2023-10-14 22:56:43 +0000182 };
183 security.acme.acceptTerms = true;
184
185 sops.secrets.cloudflare_cert__api_token = {
186 mode = "0660";
187 owner = config.users.users.nginx.name;
188 group = config.users.users.acme.group;
Samuel Shuertf68685d2023-10-28 20:07:56 -0400189 sopsFile = ../../secrets/cloudflare-cert.env.bin;
Skyler Grey2ca6ccd2023-10-14 22:56:43 +0000190 format = "binary";
191 };
Skyler Grey4259e932023-10-21 21:37:03 +0000192
193 users.users.nginx.extraGroups = [ config.users.users.acme.group ];
Skyler Grey2ca6ccd2023-10-14 22:56:43 +0000194 };
Skyler Greyfe1740c2023-10-21 01:24:18 +0000195} (if base != null then {
Skyler Greyf7eb4ae2024-05-06 12:38:44 +0000196 config.security.acme.certs = lib.mkForce (builtins.mapAttrs (domain: v:
197 if builtins.match ".*\.auxolotl\.org" domain != null then
198 (lib.filterAttrs (n: _: n != "directory" && n != "credentialsFile") v)
199 else
200 (lib.filterAttrs (n: _: n != "directory" && n != "credentialsFile") v) // {
201 webroot = null;
202 dnsProvider = "cloudflare";
203 }
204 ) base.config.security.acme.certs);
Skyler Greyfe1740c2023-10-21 01:24:18 +0000205} else
206 { })