2 description = "Module to handle multiple separate apache instances (using containers)";
4 url = "path:../myuids";
6 inputs.files-watcher = {
7 url = "path:../files-watcher";
10 outputs = { self, myuids, files-watcher }: {
11 nixosModule = { lib, config, pkgs, options, ... }:
14 cfg = config.services.websites;
18 nosslVhost = ips: cfg: {
19 listen = map (ip: { inherit ip; port = 80; }) ips;
21 logFormat = "combinedVhost";
22 documentRoot = cfg.root;
24 <Directory ${cfg.root}>
25 DirectoryIndex ${cfg.indexFile}
30 RewriteRule ^/(.+) / [L]
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;
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;
57 logPerVirtualHost = true;
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"
68 inherit (icfg) adminAddr;
69 logFormat = "combinedVhost";
70 extraModules = lists.unique icfg.modules;
71 extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
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);
82 options.services.websites = with types; {
85 description = "Each type of website to enable will target a distinct httpd server";
86 type = attrsOf (submodule ({ name, config, ... }: {
88 enable = mkEnableOption "Enable websites of this type";
89 moduleType = mkOption {
90 type = enum [ "container" "main" ];
91 default = "container";
93 How to deploy the web environment:
94 - container -> inside a dedicated container (running only httpd)
95 - main -> as main services.httpd module
98 adminAddr = mkOption {
100 description = "Admin e-mail address of the instance";
104 description = "Username of httpd service";
106 default = if config.moduleType == "container"
107 then hostConfig.containers."httpd-${name}".config.services.httpd.user
108 else hostConfig.services.httpd.user;
112 description = "Group of httpd service";
114 default = if config.moduleType == "container"
115 then hostConfig.containers."httpd-${name}".config.services.httpd.group
116 else hostConfig.services.httpd.group;
118 httpdName = mkOption {
120 description = "Name of the httpd instance to assign this type to";
125 description = "ips to listen to";
127 bindMounts = mkOption {
128 type = attrsOf unspecified;
130 description = "bind mounts to add to container";
135 description = "Additional modules to load in Apache";
137 extraConfig = mkOption {
140 description = "Additional configuration to append to Apache";
142 nosslVhost = mkOption {
143 description = "A default nossl vhost for captive portals";
147 enable = mkEnableOption "Add default no-ssl vhost for this instance";
150 description = "The hostname to use for this vhost";
154 description = "The root folder to serve";
156 indexFile = mkOption {
158 default = "index.html";
159 description = "The index file to show.";
164 fallbackVhost = mkOption {
165 description = "The fallback vhost that will be defined as first vhost in Apache";
168 certName = mkOption { type = str; };
169 hosts = mkOption { type = listOf str; };
170 root = mkOption { type = nullOr path; };
171 forceSSL = mkOption {
175 Automatically create a corresponding non-ssl vhost
176 that will only redirect to the ssl version
179 extraConfig = mkOption { type = listOf lines; default = []; };
183 vhostNoSSLConfs = mkOption {
185 description = "List of no ssl vhosts to define for Apache";
186 type = attrsOf (submodule {
188 hosts = mkOption { type = listOf str; };
189 root = mkOption { type = nullOr path; };
190 extraConfig = mkOption { type = listOf lines; default = []; };
194 vhostConfs = mkOption {
196 description = "List of vhosts to define for Apache";
197 type = attrsOf (submodule {
199 certName = mkOption { type = str; };
200 hosts = mkOption { type = listOf str; };
201 root = mkOption { type = nullOr path; };
202 forceSSL = mkOption {
206 Automatically create a corresponding non-ssl vhost
207 that will only redirect to the ssl version
210 extraConfig = mkOption { type = listOf lines; default = []; };
214 watchPaths = mkOption {
218 Paths to watch that should trigger a reload of httpd
226 config = lib.mkMerge [
230 assertion = builtins.length (builtins.attrNames (lib.filterAttrs (k: v: v.enable && v.moduleType == "main") cfg.env)) <= 1;
232 Only one enabled environment can have moduleType = "main"
239 environment.etc = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
240 "httpd/${name}/httpd.conf" { source = (pkgs.nixos {
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;
252 }).config.services.httpd.configFile;
253 }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env);
255 system.activationScripts.httpd-containers = {
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)
265 security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg:
267 containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs
268 ++ [ icfg.fallbackVhost.certName ]);
270 lib.genAttrs containerCertNames (n:
271 { postRun = "machinectl shell httpd-${name} /run/current-system/sw/bin/systemctl reload httpd.service"; }
273 ) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env)
275 containers = let hostConfig = config; in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
278 privateNetwork = false;
281 hostPath = "/var/log/httpd/${name}";
285 hostPath = "/var/lib/nixos-containers/httpd-${name}-mounts/conf";
287 } // icfg.bindMounts;
289 config = { config, options, ... }: {
292 files-watcher.nixosModule
294 config = lib.mkMerge [
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?
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;
311 services.logrotate.settings.httpd.enable = false;
314 environment.etc."httpd/httpd.conf".enable = false;
317 configFile = "/etc/httpd/httpd.conf";
320 services.filesWatcher.http-config-reload = {
321 paths = [ "/etc/httpd/httpd.conf" ];
325 services.filesWatcher.httpd = {
326 paths = icfg.watchPaths;
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;
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
348 }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env);
352 services.httpd = lib.concatMapAttrs (name: toHttpdConfig)
353 (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
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);
359 services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
361 paths = icfg.watchPaths;
364 ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
366 services.logrotate.settings.httpd.enable = false;
367 systemd.services = lib.concatMapAttrs (name: v: {
368 httpd.restartTriggers = lib.mkForce [];
370 (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
372 security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg:
374 containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs
375 ++ [ icfg.fallbackVhost.certName ]);
377 lib.genAttrs containerCertNames (n:
378 { postRun = "systemctl reload httpd.service"; }
380 ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env)