X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=modules%2Fwebsites%2Fdefault.nix;h=0a78c134ae56211dd4bf82c20f7dd91e7a6c0b4f;hb=ce95026934c4ea8c647365f68eb195459fcdff08;hp=e57f505a86fae43fc3b523c5f9210db6b82a7895;hpb=9ade8f6eb774dc7d19d82a070199b5024786b819;p=perso%2FImmae%2FConfig%2FNix.git diff --git a/modules/websites/default.nix b/modules/websites/default.nix index e57f505..0a78c13 100644 --- a/modules/websites/default.nix +++ b/modules/websites/default.nix @@ -1,121 +1,167 @@ -{ lib, config, ... }: with lib; +{ lib, config, pkgs, ... }: with lib; let cfg = config.services.websites; in { - options.services.websitesCerts = mkOption { - description = "Default websites configuration for certificates as accepted by acme"; - }; - options.services.websites = with types; mkOption { - default = {}; - description = "Each type of website to enable will target a distinct httpd server"; - type = attrsOf (submodule { - options = { - enable = mkEnableOption "Enable websites of this type"; - adminAddr = mkOption { - type = str; - description = "Admin e-mail address of the instance"; - }; - httpdName = mkOption { - type = str; - description = "Name of the httpd instance to assign this type to"; - }; - ips = mkOption { - type = listOf string; - default = []; - description = "ips to listen to"; - }; - 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 = string; - description = "The hostname to use for this vhost"; - }; - root = mkOption { - type = path; - default = ./nosslVhost; - description = "The root folder to serve"; - }; - indexFile = mkOption { - type = string; - default = "index.html"; - description = "The index file to show."; + options.services.websites = with types; { + certs = mkOption { + description = "Default websites configuration for certificates as accepted by acme"; + }; + webappDirs = mkOption { + description = '' + Defines a symlink between /run/current-system/webapps and a store + app directory to be used in http configuration. Permits to avoid + restarting httpd when only the folder name changes. + ''; + type = types.attrsOf types.path; + default = {}; + }; + webappDirsName = mkOption { + type = str; + default = "webapps"; + description = '' + Name of the webapp dir to create in /run/current-system + ''; + }; + env = mkOption { + default = {}; + description = "Each type of website to enable will target a distinct httpd server"; + type = attrsOf (submodule { + options = { + enable = mkEnableOption "Enable websites of this type"; + adminAddr = mkOption { + type = str; + description = "Admin e-mail address of the instance"; + }; + 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"; + }; + 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; + default = ./nosslVhost; + 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 = string; }; - hosts = mkOption { type = listOf string; }; - root = mkOption { type = nullOr path; }; - extraConfig = mkOption { type = listOf lines; default = []; }; + 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 = []; }; + }; }; }; - }; - vhostConfs = mkOption { - default = {}; - description = "List of vhosts to define for Apache"; - type = attrsOf (submodule { - options = { - certName = mkOption { type = string; }; - addToCerts = mkOption { - type = bool; - default = false; - description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null"; + 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 = []; }; }; - certMainHost = mkOption { - type = nullOr string; - description = "Use that host as 'main host' for acme certs"; - default = null; + }); + }; + vhostConfs = mkOption { + default = {}; + description = "List of vhosts to define for Apache"; + type = attrsOf (submodule { + options = { + certName = mkOption { type = str; }; + addToCerts = mkOption { + type = bool; + default = false; + description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null"; + }; + certMainHost = mkOption { + type = nullOr str; + description = "Use that host as 'main host' for acme certs"; + default = null; + }; + 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 = []; }; }; - hosts = mkOption { type = listOf string; }; - root = mkOption { type = nullOr path; }; - extraConfig = mkOption { type = listOf lines; default = []; }; - }; - }); + }); + }; + watchPaths = mkOption { + type = listOf str; + default = []; + description = '' + Paths to watch that should trigger a reload of httpd + ''; + }; }; - }; - }); + }); + }; + # Readonly variables + webappDirsPaths = mkOption { + type = attrsOf path; + readOnly = true; + description = '' + Full paths of the webapp dir + ''; + default = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair + name "/run/current-system/${cfg.webappDirsName}/${name}" + ) cfg.webappDirs; + }; }; config.services.httpd = let - redirectVhost = ips: { # Should go last, catchall http -> https redirect - listen = map (ip: { inherit ip; port = 80; }) ips; - hostName = "redirectSSL"; - serverAliases = [ "*" ]; - enableSSL = false; - logFormat = "combinedVhost"; - documentRoot = "${config.security.acme.directory}/acme-challenge"; - extraConfig = '' - RewriteEngine on - RewriteCond "%{REQUEST_URI}" "!^/\.well-known" - RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301] - # To redirect in specific "VirtualHost *:80", do - # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1 - # rather than rewrite - ''; - }; nosslVhost = ips: cfg: { listen = map (ip: { inherit ip; port = 80; }) ips; hostName = cfg.host; - enableSSL = false; logFormat = "combinedVhost"; documentRoot = cfg.root; extraConfig = '' @@ -130,12 +176,20 @@ in ''; }; toVhost = ips: vhostConf: { - enableSSL = true; - sslServerCert = "${config.security.acme.directory}/${vhostConf.certName}/cert.pem"; - sslServerKey = "${config.security.acme.directory}/${vhostConf.certName}/key.pem"; - sslServerChain = "${config.security.acme.directory}/${vhostConf.certName}/chain.pem"; + forceSSL = vhostConf.forceSSL or true; + useACMEHost = vhostConf.certName; logFormat = "combinedVhost"; - listen = map (ip: { inherit ip; port = 443; }) ips; + 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; @@ -144,23 +198,40 @@ in in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair icfg.httpdName (mkIf icfg.enable { enable = true; - listen = map (ip: { inherit ip; port = 443; }) icfg.ips; - stateDir = "/run/httpd_${name}"; logPerVirtualHost = true; multiProcessingModule = "worker"; + # 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 = [ (toVhost icfg.ips icfg.fallbackVhost) ] - ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ] - ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs) - ++ [ (redirectVhost icfg.ips) ]; + + 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); }) - ) cfg; + ) cfg.env; + + config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair + "httpd${icfg.httpdName}" { + paths = icfg.watchPaths; + waitTime = 5; + } + ) cfg.env; config.security.acme.certs = let - typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg; + typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg.env; flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v: attrValues v.vhostConfs ) typesToManage); @@ -186,7 +257,7 @@ in ); in attrsets.mapAttrs (k: g: if (!isNull (groupToDomain g)) - then config.services.websitesCerts // { + then cfg.certs // { domain = groupToDomain g; extraDomains = builtins.listToAttrs ( map (d: attrsets.nameValuePair d null) (extraDomains g)); @@ -196,4 +267,50 @@ in map (d: attrsets.nameValuePair d null) (extraDomains g)); } ) groupedCerts; + + config.system.extraSystemBuilderCmds = lib.mkIf (builtins.length (builtins.attrValues cfg.webappDirs) > 0) '' + mkdir -p $out/${cfg.webappDirsName} + ${builtins.concatStringsSep "\n" + (attrsets.mapAttrsToList + (name: path: "ln -s ${path} $out/${cfg.webappDirsName}/${name}") cfg.webappDirs) + } + ''; + + config.systemd.services = let + package = httpdName: config.services.httpd.${httpdName}.package.out; + cfgFile = httpdName: config.services.httpd.${httpdName}.configFile; + serviceChange = attrsets.mapAttrs' (name: icfg: + attrsets.nameValuePair + "httpd${icfg.httpdName}" { + stopIfChanged = false; + serviceConfig.ExecStart = + lib.mkForce "@${package icfg.httpdName}/bin/httpd httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf"; + serviceConfig.ExecStop = + lib.mkForce "${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -k graceful-stop"; + serviceConfig.ExecReload = + lib.mkForce "${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -k graceful"; + } + ) cfg.env; + serviceReload = attrsets.mapAttrs' (name: icfg: + attrsets.nameValuePair + "httpd${icfg.httpdName}-config-reload" { + wants = [ "httpd${icfg.httpdName}.service" ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ (cfgFile icfg.httpdName) ]; + # commented, because can cause extra delays during activate for this config: + # services.nginx.virtualHosts."_".locations."/".proxyPass = "http://blabla:3000"; + # stopIfChanged = false; + serviceConfig.Type = "oneshot"; + serviceConfig.TimeoutSec = 60; + script = '' + if ${pkgs.systemd}/bin/systemctl -q is-active httpd${icfg.httpdName}.service ; then + ${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -t && \ + ${pkgs.systemd}/bin/systemctl reload httpd${icfg.httpdName}.service + fi + ''; + serviceConfig.RemainAfterExit = true; + } + ) cfg.env; + in + serviceChange // serviceReload; }