]> git.immae.eu Git - perso/Immae/Config/Nix.git/blob - flakes/multi-apache-container/flake.nix
Squash changes containing private information
[perso/Immae/Config/Nix.git] / flakes / multi-apache-container / flake.nix
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