]> git.immae.eu Git - perso/Immae/Config/Nix.git/blobdiff - flakes/multi-apache-container/flake.nix
Squash changes containing private information
[perso/Immae/Config/Nix.git] / flakes / multi-apache-container / flake.nix
diff --git a/flakes/multi-apache-container/flake.nix b/flakes/multi-apache-container/flake.nix
new file mode 100644 (file)
index 0000000..fd788f7
--- /dev/null
@@ -0,0 +1,389 @@
+{
+  description = "Module to handle multiple separate apache instances (using containers)";
+  inputs.myuids = {
+    url = "path:../myuids";
+  };
+  inputs.files-watcher = {
+    url = "path:../files-watcher";
+  };
+
+  outputs = { self, myuids, files-watcher }: {
+    nixosModule = { lib, config, pkgs, options, ... }:
+      with lib;
+      let
+        cfg = config.services.websites;
+        hostConfig = config;
+        toHttpdConfig = icfg:
+          let
+            nosslVhost = ips: cfg: {
+              listen = map (ip: { inherit ip; port = 80; }) ips;
+              hostName = cfg.host;
+              logFormat = "combinedVhost";
+              documentRoot = cfg.root;
+              extraConfig = ''
+                <Directory ${cfg.root}>
+                  DirectoryIndex ${cfg.indexFile}
+                  AllowOverride None
+                  Require all granted
+
+                  RewriteEngine on
+                  RewriteRule ^/(.+)   /   [L]
+                </Directory>
+                '';
+            };
+            toVhost = ips: vhostConf: {
+              acmeRoot = hostConfig.security.acme.certs.${vhostConf.certName}.webroot;
+              forceSSL = vhostConf.forceSSL or true;
+              useACMEHost = vhostConf.certName;
+              logFormat = "combinedVhost";
+              listen = if vhostConf.forceSSL
+                then lists.flatten (map (ip: [{ inherit ip; port = 443; ssl = true; } { inherit ip; port = 80; }]) ips)
+                else map (ip: { inherit ip; port = 443; ssl = true; }) ips;
+              hostName = builtins.head vhostConf.hosts;
+              serverAliases = builtins.tail vhostConf.hosts or [];
+              documentRoot = vhostConf.root;
+              extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
+            };
+            toVhostNoSSL = ips: vhostConf: {
+              logFormat = "combinedVhost";
+              listen = map (ip: { inherit ip; port = 80; }) ips;
+              hostName = builtins.head vhostConf.hosts;
+              serverAliases = builtins.tail vhostConf.hosts or [];
+              documentRoot = vhostConf.root;
+              extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
+            };
+          in {
+            enable = true;
+            logPerVirtualHost = true;
+            mpm = "event";
+            # https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.0.2t&guideline=5.4
+            # test with https://www.ssllabs.com/ssltest/analyze.html?d=www.immae.eu&s=176.9.151.154&latest
+            sslProtocols = "all -SSLv3 -TLSv1 -TLSv1.1";
+            sslCiphers = builtins.concatStringsSep ":" [
+              "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256"
+              "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384"
+              "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305"
+              "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384"
+            ];
+            inherit (icfg) adminAddr;
+            logFormat = "combinedVhost";
+            extraModules = lists.unique icfg.modules;
+            extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
+
+            virtualHosts = with attrsets; {
+              ___fallbackVhost = toVhost icfg.ips icfg.fallbackVhost;
+            } // (optionalAttrs icfg.nosslVhost.enable {
+              nosslVhost = nosslVhost icfg.ips icfg.nosslVhost;
+            }) // (mapAttrs' (n: v: nameValuePair ("nossl_" + n) (toVhostNoSSL icfg.ips v)) icfg.vhostNoSSLConfs)
+            // (mapAttrs' (n: v: nameValuePair ("ssl_" + n) (toVhost icfg.ips v)) icfg.vhostConfs);
+          };
+      in
+      {
+        options.services.websites = with types; {
+          env = mkOption {
+            default = {};
+            description = "Each type of website to enable will target a distinct httpd server";
+            type = attrsOf (submodule ({ name, config, ... }: {
+              options = {
+                enable = mkEnableOption "Enable websites of this type";
+                moduleType = mkOption {
+                  type = enum [ "container" "main" ];
+                  default = "container";
+                  description = ''
+                    How to deploy the web environment:
+                    - container  -> inside a dedicated container (running only httpd)
+                    - main       -> as main services.httpd module
+                  '';
+                };
+                adminAddr = mkOption {
+                  type = str;
+                  description = "Admin e-mail address of the instance";
+                };
+                user = mkOption {
+                  type = str;
+                  description = "Username of httpd service";
+                  readOnly = true;
+                  default = if config.moduleType == "container"
+                    then hostConfig.containers."httpd-${name}".config.services.httpd.user
+                    else hostConfig.services.httpd.user;
+                };
+                group = mkOption {
+                  type = str;
+                  description = "Group of httpd service";
+                  readOnly = true;
+                  default = if config.moduleType == "container"
+                    then hostConfig.containers."httpd-${name}".config.services.httpd.group
+                    else hostConfig.services.httpd.group;
+                };
+                httpdName = mkOption {
+                  type = str;
+                  description = "Name of the httpd instance to assign this type to";
+                };
+                ips = mkOption {
+                  type = listOf str;
+                  default = [];
+                  description = "ips to listen to";
+                };
+                bindMounts = mkOption {
+                  type = attrsOf unspecified;
+                  default = {};
+                  description = "bind mounts to add to container";
+                };
+                modules = mkOption {
+                  type = listOf str;
+                  default = [];
+                  description = "Additional modules to load in Apache";
+                };
+                extraConfig = mkOption {
+                  type = listOf lines;
+                  default = [];
+                  description = "Additional configuration to append to Apache";
+                };
+                nosslVhost = mkOption {
+                  description = "A default nossl vhost for captive portals";
+                  default = {};
+                  type = submodule {
+                    options = {
+                      enable = mkEnableOption "Add default no-ssl vhost for this instance";
+                      host = mkOption {
+                        type = str;
+                        description = "The hostname to use for this vhost";
+                      };
+                      root = mkOption {
+                        type = path;
+                        description = "The root folder to serve";
+                      };
+                      indexFile = mkOption {
+                        type = str;
+                        default = "index.html";
+                        description = "The index file to show.";
+                      };
+                    };
+                  };
+                };
+                fallbackVhost = mkOption {
+                  description = "The fallback vhost that will be defined as first vhost in Apache";
+                  type = submodule {
+                    options = {
+                      certName = mkOption { type = str; };
+                      hosts    = mkOption { type = listOf str; };
+                      root     = mkOption { type = nullOr path; };
+                      forceSSL = mkOption {
+                        type = bool;
+                        default = true;
+                        description = ''
+                          Automatically create a corresponding non-ssl vhost
+                          that will only redirect to the ssl version
+                        '';
+                      };
+                      extraConfig = mkOption { type = listOf lines; default = []; };
+                    };
+                  };
+                };
+                vhostNoSSLConfs = mkOption {
+                  default = {};
+                  description = "List of no ssl vhosts to define for Apache";
+                  type = attrsOf (submodule {
+                    options = {
+                      hosts    = mkOption { type = listOf str; };
+                      root     = mkOption { type = nullOr path; };
+                      extraConfig = mkOption { type = listOf lines; default = []; };
+                    };
+                  });
+                };
+                vhostConfs = mkOption {
+                  default = {};
+                  description = "List of vhosts to define for Apache";
+                  type = attrsOf (submodule {
+                    options = {
+                      certName = mkOption { type = str; };
+                      hosts    = mkOption { type = listOf str; };
+                      root     = mkOption { type = nullOr path; };
+                      forceSSL = mkOption {
+                        type = bool;
+                        default = true;
+                        description = ''
+                          Automatically create a corresponding non-ssl vhost
+                          that will only redirect to the ssl version
+                        '';
+                      };
+                      extraConfig = mkOption { type = listOf lines; default = []; };
+                    };
+                  });
+                };
+                watchPaths = mkOption {
+                  type = listOf str;
+                  default = [];
+                  description = ''
+                    Paths to watch that should trigger a reload of httpd
+                    '';
+                };
+              };
+            }));
+          };
+        };
+
+        config = lib.mkMerge [
+          {
+            assertions = [
+              {
+                assertion = builtins.length (builtins.attrNames (lib.filterAttrs (k: v: v.enable && v.moduleType == "main") cfg.env)) <= 1;
+                message = ''
+                  Only one enabled environment can have moduleType = "main"
+                '';
+              }
+            ];
+          }
+
+          {
+            environment.etc = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+              "httpd/${name}/httpd.conf" { source = (pkgs.nixos {
+                imports = [
+                  {
+                    config.security.acme.acceptTerms = true;
+                    config.security.acme.preliminarySelfsigned = false;
+                    config.security.acme.certs =
+                      lib.mapAttrs (n: lib.filterAttrs (n': v': n' != "directory")) config.security.acme.certs;
+                    config.security.acme.defaults = config.security.acme.defaults;
+                    config.networking.hostName = "${hostConfig.networking.hostName}-${name}";
+                    config.services.httpd = toHttpdConfig icfg;
+                  }
+                ];
+              }).config.services.httpd.configFile;
+            }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env);
+
+            system.activationScripts.httpd-containers = {
+              deps = [ "etc" ];
+              text = builtins.concatStringsSep "\n" (
+                lib.mapAttrsToList (n: v: ''
+                  install -d -m 0750 -o ${v.user} -g ${v.group} /var/log/httpd/${n} /var/lib/nixos-containers/httpd-${n}-mounts/conf
+                  install -Dm644 -o ${v.user} -g ${v.group} /etc/httpd/${n}/httpd.conf /var/lib/nixos-containers/httpd-${n}-mounts/conf/
+                '') (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env)
+                );
+            };
+
+            security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg:
+              let
+                containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs
+                  ++ [ icfg.fallbackVhost.certName ]);
+              in
+                lib.genAttrs containerCertNames (n:
+                  { postRun = "machinectl shell httpd-${name} /run/current-system/sw/bin/systemctl reload httpd.service"; }
+                  )
+              ) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env)
+            );
+            containers = let hostConfig = config; in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+              "httpd-${name}" {
+                autoStart = true;
+                privateNetwork = false;
+                bindMounts = {
+                  "/var/log/httpd" = {
+                    hostPath = "/var/log/httpd/${name}";
+                    isReadOnly = false;
+                  };
+                  "/etc/httpd" = {
+                    hostPath = "/var/lib/nixos-containers/httpd-${name}-mounts/conf";
+                  };
+                } // icfg.bindMounts;
+
+                config = { config, options, ... }: {
+                  imports = [
+                    myuids.nixosModule
+                    files-watcher.nixosModule
+                  ];
+                  config = lib.mkMerge [
+                    {
+                      # This value determines the NixOS release with which your system is
+                      # to be compatible, in order to avoid breaking some software such as
+                      # database servers. You should change this only after NixOS release
+                      # notes say you should.
+                      # https://nixos.org/nixos/manual/release-notes.html
+                      system.stateVersion = "23.05"; # Did you read the comment?
+                    }
+                    {
+                      users.mutableUsers = false;
+                      users.allowNoPasswordLogin = true;
+                      users.users.acme.uid = config.ids.uids.acme;
+                      users.users.acme.group = "acme";
+                      users.groups.acme.gid = config.ids.gids.acme;
+                    }
+                    {
+                      services.logrotate.settings.httpd.enable = false;
+                    }
+                    {
+                      environment.etc."httpd/httpd.conf".enable = false;
+                      services.httpd = {
+                        enable = true;
+                        configFile = "/etc/httpd/httpd.conf";
+                      };
+
+                      services.filesWatcher.http-config-reload = {
+                        paths = [ "/etc/httpd/httpd.conf" ];
+                        waitTime = 2;
+                        restart = true;
+                      };
+                      services.filesWatcher.httpd = {
+                        paths = icfg.watchPaths;
+                        waitTime = 5;
+                      };
+
+                      users.users.${icfg.user}.extraGroups = [ "acme" "keys" ];
+                      systemd.services.http-config-reload = {
+                        wants = [ "httpd.service" ];
+                        wantedBy = [ "multi-user.target" ];
+                        restartTriggers = [ config.services.httpd.configFile ];
+                        serviceConfig.Type = "oneshot";
+                        serviceConfig.TimeoutSec = 60;
+                        serviceConfig.RemainAfterExit = true;
+                        script = ''
+                          if ${pkgs.systemd}/bin/systemctl -q is-active httpd.service ; then
+                            ${config.services.httpd.package.out}/bin/httpd -f ${config.services.httpd.configFile} -t && \
+                              ${pkgs.systemd}/bin/systemctl reload httpd.service
+                          fi
+                        '';
+                      };
+                    }
+                  ];
+                };
+              }) (lib.filterAttrs (k: v: v.moduleType == "container" && v.enable) cfg.env);
+          }
+
+          {
+            services.httpd = lib.concatMapAttrs (name: toHttpdConfig)
+              (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+            users.users = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+              config.services.httpd.user { extraGroups = [ "acme" ]; }
+            ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+            services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
+              "httpd" {
+                paths = icfg.watchPaths;
+                waitTime = 5;
+              }
+            ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+            services.logrotate.settings.httpd.enable = false;
+            systemd.services = lib.concatMapAttrs (name: v: {
+              httpd.restartTriggers = lib.mkForce [];
+            })
+              (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env);
+
+            security.acme.certs = lib.mkMerge (lib.mapAttrsToList (name: icfg:
+              let
+                containerCertNames = lib.unique (lib.mapAttrsToList (n: v: v.certName) icfg.vhostConfs
+                  ++ [ icfg.fallbackVhost.certName ]);
+              in
+                lib.genAttrs containerCertNames (n:
+                  { postRun = "systemctl reload httpd.service"; }
+                  )
+              ) (lib.filterAttrs (k: v: v.moduleType == "main" && v.enable) cfg.env)
+            );
+
+          }
+        ];
+      };
+  };
+}
+
+