]>
Commit | Line | Data |
---|---|---|
1a64deeb IB |
1 | { |
2 | description = "Module to handle multiple separate apache instances (using containers)"; | |
3 | inputs.myuids = { | |
4 | url = "path:../myuids"; | |
5 | }; | |
6 | inputs.files-watcher = { | |
7 | url = "path:../files-watcher"; | |
8 | }; | |
9 | ||
10 | outputs = { self, myuids, files-watcher }: { | |
11 | nixosModule = { lib, config, pkgs, options, ... }: | |
12 | with lib; | |
13 | let | |
14 | cfg = config.services.websites; | |
15 | hostConfig = config; | |
16 | toHttpdConfig = icfg: | |
17 | let | |
18 | nosslVhost = ips: cfg: { | |
19 | listen = map (ip: { inherit ip; port = 80; }) ips; | |
20 | hostName = cfg.host; | |
21 | logFormat = "combinedVhost"; | |
22 | documentRoot = cfg.root; | |
23 | extraConfig = '' | |
24 | <Directory ${cfg.root}> | |
25 | DirectoryIndex ${cfg.indexFile} | |
26 | AllowOverride None | |
27 | Require all granted | |
28 | ||
29 | RewriteEngine on | |
30 | RewriteRule ^/(.+) / [L] | |
31 | </Directory> | |
32 | ''; | |
33 | }; | |
34 | toVhost = ips: vhostConf: { | |
35 | acmeRoot = hostConfig.security.acme.certs.${vhostConf.certName}.webroot; | |
36 | forceSSL = vhostConf.forceSSL or true; | |
37 | useACMEHost = vhostConf.certName; | |
38 | logFormat = "combinedVhost"; | |
39 | listen = if vhostConf.forceSSL | |
40 | then lists.flatten (map (ip: [{ inherit ip; port = 443; ssl = true; } { inherit ip; port = 80; }]) ips) | |
41 | else map (ip: { inherit ip; port = 443; ssl = true; }) ips; | |
42 | hostName = builtins.head vhostConf.hosts; | |
43 | serverAliases = builtins.tail vhostConf.hosts or []; | |
44 | documentRoot = vhostConf.root; | |
45 | extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig; | |
46 | }; | |
47 | toVhostNoSSL = ips: vhostConf: { | |
48 | logFormat = "combinedVhost"; | |
49 | listen = map (ip: { inherit ip; port = 80; }) ips; | |
50 | hostName = builtins.head vhostConf.hosts; | |
51 | serverAliases = builtins.tail vhostConf.hosts or []; | |
52 | documentRoot = vhostConf.root; | |
53 | extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig; | |
54 | }; | |
55 | in { | |
56 | enable = true; | |
57 | logPerVirtualHost = true; | |
58 | mpm = "event"; | |
59 | # https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.0.2t&guideline=5.4 | |
60 | # test with https://www.ssllabs.com/ssltest/analyze.html?d=www.immae.eu&s=176.9.151.154&latest | |
61 | sslProtocols = "all -SSLv3 -TLSv1 -TLSv1.1"; | |
62 | sslCiphers = builtins.concatStringsSep ":" [ | |
63 | "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256" | |
64 | "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384" | |
65 | "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305" | |
66 | "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384" | |
67 | ]; | |
68 | inherit (icfg) adminAddr; | |
69 | logFormat = "combinedVhost"; | |
70 | extraModules = lists.unique icfg.modules; | |
71 | extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig; | |
72 | ||
73 | virtualHosts = with attrsets; { | |
74 | ___fallbackVhost = toVhost icfg.ips icfg.fallbackVhost; | |
75 | } // (optionalAttrs icfg.nosslVhost.enable { | |
76 | nosslVhost = nosslVhost icfg.ips icfg.nosslVhost; | |
77 | }) // (mapAttrs' (n: v: nameValuePair ("nossl_" + n) (toVhostNoSSL icfg.ips v)) icfg.vhostNoSSLConfs) | |
78 | // (mapAttrs' (n: v: nameValuePair ("ssl_" + n) (toVhost icfg.ips v)) icfg.vhostConfs); | |
79 | }; | |
80 | in | |
81 | { | |
82 | options.services.websites = with types; { | |
83 | env = mkOption { | |
84 | default = {}; | |
85 | description = "Each type of website to enable will target a distinct httpd server"; | |
86 | type = attrsOf (submodule ({ name, config, ... }: { | |
87 | options = { | |
88 | enable = mkEnableOption "Enable websites of this type"; | |
89 | moduleType = mkOption { | |
90 | type = enum [ "container" "main" ]; | |
91 | default = "container"; | |
92 | description = '' | |
93 | How to deploy the web environment: | |
94 | - container -> inside a dedicated container (running only httpd) | |
95 | - main -> as main services.httpd module | |
96 | ''; | |
97 | }; | |
98 | adminAddr = mkOption { | |
99 | type = str; | |
100 | description = "Admin e-mail address of the instance"; | |
101 | }; | |
102 | user = mkOption { | |
103 | type = str; | |
104 | description = "Username of httpd service"; | |
105 | readOnly = true; | |
106 | default = if config.moduleType == "container" | |
107 | then hostConfig.containers."httpd-${name}".config.services.httpd.user | |
108 | else hostConfig.services.httpd.user; | |
109 | }; | |
110 | group = mkOption { | |
111 | type = str; | |
112 | description = "Group of httpd service"; | |
113 | readOnly = true; | |
114 | default = if config.moduleType == "container" | |
115 | then hostConfig.containers."httpd-${name}".config.services.httpd.group | |
116 | else hostConfig.services.httpd.group; | |
117 | }; | |
118 | httpdName = mkOption { | |
119 | type = str; | |
120 | description = "Name of the httpd instance to assign this type to"; | |
121 | }; | |
122 | ips = mkOption { | |
123 | type = listOf str; | |
124 | default = []; | |
125 | description = "ips to listen to"; | |
126 | }; | |
127 | bindMounts = mkOption { | |
128 | type = attrsOf unspecified; | |
129 | default = {}; | |
130 | description = "bind mounts to add to container"; | |
131 | }; | |
132 | modules = mkOption { | |
133 | type = listOf str; | |
134 | default = []; | |
135 | description = "Additional modules to load in Apache"; | |
136 | }; | |
137 | extraConfig = mkOption { | |
138 | type = listOf lines; | |
139 | default = []; | |
140 | description = "Additional configuration to append to Apache"; | |
141 | }; | |
142 | nosslVhost = mkOption { | |
143 | description = "A default nossl vhost for captive portals"; | |
144 | default = {}; | |
145 | type = submodule { | |
146 | options = { | |
147 | enable = mkEnableOption "Add default no-ssl vhost for this instance"; | |
148 | host = mkOption { | |
149 | type = str; | |
150 | description = "The hostname to use for this vhost"; | |
151 | }; | |
152 | root = mkOption { | |
153 | type = path; | |
154 | description = "The root folder to serve"; | |
155 | }; | |
156 | indexFile = mkOption { | |
157 | type = str; | |
158 | default = "index.html"; | |
159 | description = "The index file to show."; | |
160 | }; | |
161 | }; | |
162 | }; | |
163 | }; | |
164 | fallbackVhost = mkOption { | |
165 | description = "The fallback vhost that will be defined as first vhost in Apache"; | |
166 | type = submodule { | |
167 | options = { | |
168 | certName = mkOption { type = str; }; | |
169 | hosts = mkOption { type = listOf str; }; | |
170 | root = mkOption { type = nullOr path; }; | |
171 | forceSSL = mkOption { | |
172 | type = bool; | |
173 | default = true; | |
174 | description = '' | |
175 | Automatically create a corresponding non-ssl vhost | |
176 | that will only redirect to the ssl version | |
177 | ''; | |
178 | }; | |
179 | extraConfig = mkOption { type = listOf lines; default = []; }; | |
180 | }; | |
181 | }; | |
182 | }; | |
183 | vhostNoSSLConfs = mkOption { | |
184 | default = {}; | |
185 | description = "List of no ssl vhosts to define for Apache"; | |
186 | type = attrsOf (submodule { | |
187 | options = { | |
188 | hosts = mkOption { type = listOf str; }; | |
189 | root = mkOption { type = nullOr path; }; | |
190 | extraConfig = mkOption { type = listOf lines; default = []; }; | |
191 | }; | |
192 | }); | |
193 | }; | |
194 | vhostConfs = mkOption { | |
195 | default = {}; | |
196 | description = "List of vhosts to define for Apache"; | |
197 | type = attrsOf (submodule { | |
198 | options = { | |
199 | certName = mkOption { type = str; }; | |
200 | hosts = mkOption { type = listOf str; }; | |
201 | root = mkOption { type = nullOr path; }; | |
202 | forceSSL = mkOption { | |
203 | type = bool; | |
204 | default = true; | |
205 | description = '' | |
206 | Automatically create a corresponding non-ssl vhost | |
207 | that will only redirect to the ssl version | |
208 | ''; | |
209 | }; | |
210 | extraConfig = mkOption { type = listOf lines; default = []; }; | |
211 | }; | |
212 | }); | |
213 | }; | |
214 | watchPaths = mkOption { | |
215 | type = listOf str; | |
216 | default = []; | |
217 | description = '' | |
218 | Paths to watch that should trigger a reload of httpd | |
219 | ''; | |
220 | }; | |
221 | }; | |
222 | })); | |
223 | }; | |
224 | }; | |
225 | ||
226 | config = lib.mkMerge [ | |
227 | { | |
228 | assertions = [ | |
229 | { | |
230 | assertion = builtins.length (builtins.attrNames (lib.filterAttrs (k: v: v.enable && v.moduleType == "main") cfg.env)) <= 1; | |
231 | message = '' | |
232 | Only one enabled environment can have moduleType = "main" | |
233 | ''; | |
234 | } | |
235 | ]; | |
236 | } | |
237 | ||
238 | { | |
239 | environment.etc = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | |
240 | "httpd/${name}/httpd.conf" { source = (pkgs.nixos { | |
241 | imports = [ | |
242 | { | |
243 | config.security.acme.acceptTerms = true; | |
244 | config.security.acme.preliminarySelfsigned = false; | |
245 | config.security.acme.certs = | |
246 | lib.mapAttrs (n: lib.filterAttrs (n': v': n' != "directory")) config.security.acme.certs; | |
247 | config.security.acme.defaults = config.security.acme.defaults; | |
248 | config.networking.hostName = "${hostConfig.networking.hostName}-${name}"; | |
249 | config.services.httpd = toHttpdConfig icfg; | |
250 | } | |
251 | ]; | |
252 | }).config.services.httpd.configFile; | |
253 | }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env); | |
254 | ||
255 | system.activationScripts.httpd-containers = { | |
256 | deps = [ "etc" ]; | |
257 | text = builtins.concatStringsSep "\n" ( | |
258 | lib.mapAttrsToList (n: v: '' | |
259 | install -d -m 0750 -o ${v.user} -g ${v.group} /var/log/httpd/${n} /var/lib/nixos-containers/httpd-${n}-mounts/conf | |
260 | install -Dm644 -o ${v.user} -g ${v.group} /etc/httpd/${n}/httpd.conf /var/lib/nixos-containers/httpd-${n}-mounts/conf/ | |
261 | '') (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env) | |
262 | ); | |
263 | }; | |
264 | ||
265 | security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg: | |
266 | let | |
267 | containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs | |
268 | ++ [ icfg.fallbackVhost.certName ]); | |
269 | in | |
270 | lib.genAttrs containerCertNames (n: | |
271 | { postRun = "machinectl shell httpd-${name} /run/current-system/sw/bin/systemctl reload httpd.service"; } | |
272 | ) | |
273 | ) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env) | |
274 | ); | |
275 | containers = let hostConfig = config; in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | |
276 | "httpd-${name}" { | |
277 | autoStart = true; | |
278 | privateNetwork = false; | |
279 | bindMounts = { | |
280 | "/var/log/httpd" = { | |
281 | hostPath = "/var/log/httpd/${name}"; | |
282 | isReadOnly = false; | |
283 | }; | |
284 | "/etc/httpd" = { | |
285 | hostPath = "/var/lib/nixos-containers/httpd-${name}-mounts/conf"; | |
286 | }; | |
287 | } // icfg.bindMounts; | |
288 | ||
289 | config = { config, options, ... }: { | |
290 | imports = [ | |
291 | myuids.nixosModule | |
292 | files-watcher.nixosModule | |
293 | ]; | |
294 | config = lib.mkMerge [ | |
295 | { | |
296 | # This value determines the NixOS release with which your system is | |
297 | # to be compatible, in order to avoid breaking some software such as | |
298 | # database servers. You should change this only after NixOS release | |
299 | # notes say you should. | |
300 | # https://nixos.org/nixos/manual/release-notes.html | |
301 | system.stateVersion = "23.05"; # Did you read the comment? | |
302 | } | |
303 | { | |
304 | users.mutableUsers = false; | |
305 | users.allowNoPasswordLogin = true; | |
306 | users.users.acme.uid = config.ids.uids.acme; | |
307 | users.users.acme.group = "acme"; | |
308 | users.groups.acme.gid = config.ids.gids.acme; | |
309 | } | |
310 | { | |
311 | services.logrotate.settings.httpd.enable = false; | |
312 | } | |
313 | { | |
314 | environment.etc."httpd/httpd.conf".enable = false; | |
315 | services.httpd = { | |
316 | enable = true; | |
317 | configFile = "/etc/httpd/httpd.conf"; | |
318 | }; | |
319 | ||
320 | services.filesWatcher.http-config-reload = { | |
321 | paths = [ "/etc/httpd/httpd.conf" ]; | |
322 | waitTime = 2; | |
323 | restart = true; | |
324 | }; | |
325 | services.filesWatcher.httpd = { | |
326 | paths = icfg.watchPaths; | |
327 | waitTime = 5; | |
328 | }; | |
329 | ||
330 | users.users.${icfg.user}.extraGroups = [ "acme" "keys" ]; | |
331 | systemd.services.http-config-reload = { | |
332 | wants = [ "httpd.service" ]; | |
333 | wantedBy = [ "multi-user.target" ]; | |
334 | restartTriggers = [ config.services.httpd.configFile ]; | |
335 | serviceConfig.Type = "oneshot"; | |
336 | serviceConfig.TimeoutSec = 60; | |
337 | serviceConfig.RemainAfterExit = true; | |
338 | script = '' | |
339 | if ${pkgs.systemd}/bin/systemctl -q is-active httpd.service ; then | |
340 | ${config.services.httpd.package.out}/bin/httpd -f ${config.services.httpd.configFile} -t && \ | |
341 | ${pkgs.systemd}/bin/systemctl reload httpd.service | |
342 | fi | |
343 | ''; | |
344 | }; | |
345 | } | |
346 | ]; | |
347 | }; | |
348 | }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env); | |
349 | } | |
350 | ||
351 | { | |
352 | services.httpd = lib.concatMapAttrs (name: toHttpdConfig) | |
353 | (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env); | |
354 | ||
355 | users.users = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | |
356 | config.services.httpd.user { extraGroups = [ "acme" ]; } | |
357 | ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env); | |
358 | ||
359 | services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | |
360 | "httpd" { | |
361 | paths = icfg.watchPaths; | |
362 | waitTime = 5; | |
363 | } | |
364 | ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env); | |
365 | ||
366 | services.logrotate.settings.httpd.enable = false; | |
367 | systemd.services = lib.concatMapAttrs (name: v: { | |
368 | httpd.restartTriggers = lib.mkForce []; | |
369 | }) | |
370 | (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env); | |
371 | ||
372 | security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg: | |
373 | let | |
374 | containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs | |
375 | ++ [ icfg.fallbackVhost.certName ]); | |
376 | in | |
377 | lib.genAttrs containerCertNames (n: | |
378 | { postRun = "systemctl reload httpd.service"; } | |
379 | ) | |
380 | ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env) | |
381 | ); | |
382 | ||
383 | } | |
384 | ]; | |
385 | }; | |
386 | }; | |
387 | } | |
388 | ||
389 |