{ 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 = '' DirectoryIndex ${cfg.indexFile} AllowOverride None Require all granted RewriteEngine on RewriteRule ^/(.+) / [L] ''; }; 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) ); } ]; }; }; }