aboutsummaryrefslogblamecommitdiff
path: root/flakes/multi-apache-container/flake.nix
blob: fd788f7d26013ed3b7715b48d354e654e5b8050b (plain) (tree)




































































































































































































































































































































































































                                                                                                                                           
{
  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)
            );

          }
        ];
      };
  };
}