diff options
Diffstat (limited to 'nixos/modules/services/web-servers')
35 files changed, 7046 insertions, 0 deletions
diff --git a/nixos/modules/services/web-servers/agate.nix b/nixos/modules/services/web-servers/agate.nix new file mode 100644 index 00000000000..3afdb561c0b --- /dev/null +++ b/nixos/modules/services/web-servers/agate.nix @@ -0,0 +1,148 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.agate; +in +{ + options = { + services.agate = { + enable = mkEnableOption "Agate Server"; + + package = mkOption { + type = types.package; + default = pkgs.agate; + defaultText = literalExpression "pkgs.agate"; + description = "The package to use"; + }; + + addresses = mkOption { + type = types.listOf types.str; + default = [ "0.0.0.0:1965" ]; + description = '' + Addresses to listen on, IP:PORT, if you haven't disabled forwarding + only set IPv4. + ''; + }; + + contentDir = mkOption { + default = "/var/lib/agate/content"; + type = types.path; + description = "Root of the content directory."; + }; + + certificatesDir = mkOption { + default = "/var/lib/agate/certificates"; + type = types.path; + description = "Root of the certificate directory."; + }; + + hostnames = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + Domain name of this Gemini server, enables checking hostname and port + in requests. (multiple occurences means basic vhosts) + ''; + }; + + language = mkOption { + default = null; + type = types.nullOr types.str; + description = "RFC 4646 Language code for text/gemini documents."; + }; + + onlyTls_1_3 = mkOption { + default = false; + type = types.bool; + description = "Only use TLSv1.3 (default also allows TLSv1.2)."; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ "" ]; + example = [ "--log-ip" ]; + description = "Extra arguments to use running agate."; + }; + }; + }; + + config = mkIf cfg.enable { + # available for generating certs by hand + # it can be a bit arduous with openssl + environment.systemPackages = [ cfg.package ]; + + systemd.services.agate = { + description = "Agate"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "network-online.target" ]; + + script = + let + prefixKeyList = key: list: concatMap (v: [ key v ]) list; + addresses = prefixKeyList "--addr" cfg.addresses; + hostnames = prefixKeyList "--hostname" cfg.hostnames; + in + '' + exec ${cfg.package}/bin/agate ${ + escapeShellArgs ( + [ + "--content" "${cfg.contentDir}" + "--certs" "${cfg.certificatesDir}" + ] ++ + addresses ++ + (optionals (cfg.hostnames != []) hostnames) ++ + (optionals (cfg.language != null) [ "--lang" cfg.language ]) ++ + (optionals cfg.onlyTls_1_3 [ "--only-tls13" ]) ++ + (optionals (cfg.extraArgs != []) cfg.extraArgs) + ) + } + ''; + + serviceConfig = { + Restart = "always"; + RestartSec = "5s"; + DynamicUser = true; + StateDirectory = "agate"; + + # Security options: + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + + # ProtectClock= adds DeviceAllow=char-rtc r + DeviceAllow = ""; + + LockPersonality = true; + + PrivateTmp = true; + PrivateDevices = true; + PrivateUsers = true; + + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + + RestrictNamespaces = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictRealtime = true; + + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ + "@system-service" + "~@cpu-emulation" + "~@debug" + "~@keyring" + "~@memlock" + "~@obsolete" + "~@privileged" + "~@setuid" + ]; + }; + }; + }; +} diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix new file mode 100644 index 00000000000..d817ff6019a --- /dev/null +++ b/nixos/modules/services/web-servers/apache-httpd/default.nix @@ -0,0 +1,839 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.httpd; + + certs = config.security.acme.certs; + + runtimeDir = "/run/httpd"; + + pkg = cfg.package.out; + + apachectl = pkgs.runCommand "apachectl" { meta.priority = -1; } '' + mkdir -p $out/bin + cp ${pkg}/bin/apachectl $out/bin/apachectl + sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f /etc/httpd/httpd.conf|' + ''; + + php = cfg.phpPackage.override { apacheHttpd = pkg; }; + + phpModuleName = let + majorVersion = lib.versions.major (lib.getVersion php); + in (if majorVersion == "8" then "php" else "php${majorVersion}"); + + mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; }; + + vhosts = attrValues cfg.virtualHosts; + + # certName is used later on to determine systemd service names. + acmeEnabledVhosts = map (hostOpts: hostOpts // { + certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName; + }) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts); + + dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts); + + mkListenInfo = hostOpts: + if hostOpts.listen != [] then + hostOpts.listen + else + optionals (hostOpts.onlySSL || hostOpts.addSSL || hostOpts.forceSSL) (map (addr: { ip = addr; port = 443; ssl = true; }) hostOpts.listenAddresses) ++ + optionals (!hostOpts.onlySSL) (map (addr: { ip = addr; port = 80; ssl = false; }) hostOpts.listenAddresses) + ; + + listenInfo = unique (concatMap mkListenInfo vhosts); + + enableHttp2 = any (vhost: vhost.http2) vhosts; + enableSSL = any (listen: listen.ssl) listenInfo; + enableUserDir = any (vhost: vhost.enableUserDir) vhosts; + + # NOTE: generally speaking order of modules is very important + modules = + [ # required apache modules our httpd service cannot run without + "authn_core" "authz_core" + "log_config" + "mime" "autoindex" "negotiation" "dir" + "alias" "rewrite" + "unixd" "slotmem_shm" "socache_shmcb" + "mpm_${cfg.mpm}" + ] + ++ (if cfg.mpm == "prefork" then [ "cgi" ] else [ "cgid" ]) + ++ optional enableHttp2 "http2" + ++ optional enableSSL "ssl" + ++ optional enableUserDir "userdir" + ++ optional cfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } + ++ optional cfg.enablePHP { name = phpModuleName; path = "${php}/modules/lib${phpModuleName}.so"; } + ++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } + ++ cfg.extraModules; + + loggingConf = (if cfg.logFormat != "none" then '' + ErrorLog ${cfg.logDir}/error.log + + LogLevel notice + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + LogFormat "%{Referer}i -> %U" referer + LogFormat "%{User-agent}i" agent + + CustomLog ${cfg.logDir}/access.log ${cfg.logFormat} + '' else '' + ErrorLog /dev/null + ''); + + + browserHacks = '' + <IfModule mod_setenvif.c> + BrowserMatch "Mozilla/2" nokeepalive + BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 + BrowserMatch "RealPlayer 4\.0" force-response-1.0 + BrowserMatch "Java/1\.0" force-response-1.0 + BrowserMatch "JDK/1\.0" force-response-1.0 + BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully + BrowserMatch "^WebDrive" redirect-carefully + BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully + BrowserMatch "^gnome-vfs" redirect-carefully + </IfModule> + ''; + + + sslConf = '' + <IfModule mod_ssl.c> + SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000) + + Mutex posixsem + + SSLRandomSeed startup builtin + SSLRandomSeed connect builtin + + SSLProtocol ${cfg.sslProtocols} + SSLCipherSuite ${cfg.sslCiphers} + SSLHonorCipherOrder on + </IfModule> + ''; + + + mimeConf = '' + TypesConfig ${pkg}/conf/mime.types + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + AddType application/x-httpd-php .php .phtml + + <IfModule mod_mime_magic.c> + MIMEMagicFile ${pkg}/conf/magic + </IfModule> + ''; + + luaSetPaths = let + # support both lua and lua.withPackages derivations + luaversion = cfg.package.lua5.lua.luaversion or cfg.package.lua5.luaversion; + in + '' + <IfModule mod_lua.c> + LuaPackageCPath ${cfg.package.lua5}/lib/lua/${luaversion}/?.so + LuaPackagePath ${cfg.package.lua5}/share/lua/${luaversion}/?.lua + </IfModule> + ''; + + mkVHostConf = hostOpts: + let + adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr; + listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts); + listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts); + + useACME = hostOpts.enableACME || hostOpts.useACMEHost != null; + sslCertDir = + if hostOpts.enableACME then certs.${hostOpts.hostName}.directory + else if hostOpts.useACMEHost != null then certs.${hostOpts.useACMEHost}.directory + else abort "This case should never happen."; + + sslServerCert = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerCert; + sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey; + sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain; + + acmeChallenge = optionalString (useACME && hostOpts.acmeRoot != null) '' + Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/" + <Directory "${hostOpts.acmeRoot}"> + AllowOverride None + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + Require method GET POST OPTIONS + Require all granted + </Directory> + ''; + in + optionalString (listen != []) '' + <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listen}> + ServerName ${hostOpts.hostName} + ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases} + ServerAdmin ${adminAddr} + <IfModule mod_ssl.c> + SSLEngine off + </IfModule> + ${acmeChallenge} + ${if hostOpts.forceSSL then '' + <IfModule mod_rewrite.c> + RewriteEngine on + RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC] + RewriteCond %{HTTPS} off + RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} + </IfModule> + '' else mkVHostCommonConf hostOpts} + </VirtualHost> + '' + + optionalString (listenSSL != []) '' + <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listenSSL}> + ServerName ${hostOpts.hostName} + ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases} + ServerAdmin ${adminAddr} + SSLEngine on + SSLCertificateFile ${sslServerCert} + SSLCertificateKeyFile ${sslServerKey} + ${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"} + ${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"} + ${acmeChallenge} + ${mkVHostCommonConf hostOpts} + </VirtualHost> + '' + ; + + mkVHostCommonConf = hostOpts: + let + documentRoot = if hostOpts.documentRoot != null + then hostOpts.documentRoot + else pkgs.emptyDirectory + ; + + mkLocations = locations: concatStringsSep "\n" (map (config: '' + <Location ${config.location}> + ${optionalString (config.proxyPass != null) '' + <IfModule mod_proxy.c> + ProxyPass ${config.proxyPass} + ProxyPassReverse ${config.proxyPass} + </IfModule> + ''} + ${optionalString (config.index != null) '' + <IfModule mod_dir.c> + DirectoryIndex ${config.index} + </IfModule> + ''} + ${optionalString (config.alias != null) '' + <IfModule mod_alias.c> + Alias "${config.alias}" + </IfModule> + ''} + ${config.extraConfig} + </Location> + '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))); + in + '' + ${optionalString cfg.logPerVirtualHost '' + ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log + CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat} + ''} + + ${optionalString (hostOpts.robotsEntries != "") '' + Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries} + ''} + + DocumentRoot "${documentRoot}" + + <Directory "${documentRoot}"> + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + </Directory> + + ${optionalString hostOpts.enableUserDir '' + UserDir public_html + UserDir disabled root + <Directory "/home/*/public_html"> + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + <Limit GET POST OPTIONS> + Require all granted + </Limit> + <LimitExcept GET POST OPTIONS> + Require all denied + </LimitExcept> + </Directory> + ''} + + ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") '' + RedirectPermanent / ${hostOpts.globalRedirect} + ''} + + ${ + let makeDirConf = elem: '' + Alias ${elem.urlPath} ${elem.dir}/ + <Directory ${elem.dir}> + Options +Indexes + Require all granted + AllowOverride All + </Directory> + ''; + in concatMapStrings makeDirConf hostOpts.servedDirs + } + + ${mkLocations hostOpts.locations} + ${hostOpts.extraConfig} + '' + ; + + + confFile = pkgs.writeText "httpd.conf" '' + + ServerRoot ${pkg} + ServerName ${config.networking.hostName} + DefaultRuntimeDir ${runtimeDir}/runtime + + PidFile ${runtimeDir}/httpd.pid + + ${optionalString (cfg.mpm != "prefork") '' + # mod_cgid requires this. + ScriptSock ${runtimeDir}/cgisock + ''} + + <IfModule prefork.c> + MaxClients ${toString cfg.maxClients} + MaxRequestsPerChild ${toString cfg.maxRequestsPerChild} + </IfModule> + + ${let + toStr = listen: "Listen ${listen.ip}:${toString listen.port} ${if listen.ssl then "https" else "http"}"; + uniqueListen = uniqList {inputList = map toStr listenInfo;}; + in concatStringsSep "\n" uniqueListen + } + + User ${cfg.user} + Group ${cfg.group} + + ${let + mkModule = module: + if isString module then { name = module; path = "${pkg}/modules/mod_${module}.so"; } + else if isAttrs module then { inherit (module) name path; } + else throw "Expecting either a string or attribute set including a name and path."; + in + concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (unique (map mkModule modules)) + } + + AddHandler type-map var + + <Files ~ "^\.ht"> + Require all denied + </Files> + + ${mimeConf} + ${loggingConf} + ${browserHacks} + + Include ${pkg}/conf/extra/httpd-default.conf + Include ${pkg}/conf/extra/httpd-autoindex.conf + Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf + Include ${pkg}/conf/extra/httpd-languages.conf + + TraceEnable off + + ${sslConf} + + ${optionalString cfg.package.luaSupport luaSetPaths} + + # Fascist default - deny access to everything. + <Directory /> + Options FollowSymLinks + AllowOverride None + Require all denied + </Directory> + + # But do allow access to files in the store so that we don't have + # to generate <Directory> clauses for every generated file that we + # want to serve. + <Directory /nix/store> + Require all granted + </Directory> + + ${cfg.extraConfig} + + ${concatMapStringsSep "\n" mkVHostConf vhosts} + ''; + + # Generate the PHP configuration file. Should probably be factored + # out into a separate module. + phpIni = pkgs.runCommand "php.ini" + { options = cfg.phpOptions; + preferLocalBuild = true; + } + '' + cat ${php}/etc/php.ini > $out + cat ${php.phpIni} > $out + echo "$options" >> $out + ''; + + mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; +in + + +{ + + imports = [ + (mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.") + (mkRemovedOptionModule [ "services" "httpd" "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.") + (mkRenamedOptionModule [ "services" "httpd" "multiProcessingModule" ] [ "services" "httpd" "mpm" ]) + + # virtualHosts options + (mkRemovedOptionModule [ "services" "httpd" "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + ]; + + # interface + + options = { + + services.httpd = { + + enable = mkEnableOption "the Apache HTTP Server"; + + package = mkOption { + type = types.package; + default = pkgs.apacheHttpd; + defaultText = literalExpression "pkgs.apacheHttpd"; + description = '' + Overridable attribute of the Apache HTTP Server package to use. + ''; + }; + + configFile = mkOption { + type = types.path; + default = confFile; + defaultText = literalExpression "confFile"; + example = literalExpression ''pkgs.writeText "httpd.conf" "# my custom config file ..."''; + description = '' + Override the configuration file used by Apache. By default, + NixOS generates one automatically. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Configuration lines appended to the generated Apache + configuration file. Note that this mechanism will not work + when <option>configFile</option> is overridden. + ''; + }; + + extraModules = mkOption { + type = types.listOf types.unspecified; + default = []; + example = literalExpression '' + [ + "proxy_connect" + { name = "jk"; path = "''${pkgs.tomcat_connectors}/modules/mod_jk.so"; } + ] + ''; + description = '' + Additional Apache modules to be used. These can be + specified as a string in the case of modules distributed + with Apache, or as an attribute set specifying the + <varname>name</varname> and <varname>path</varname> of the + module. + ''; + }; + + adminAddr = mkOption { + type = types.str; + example = "admin@example.org"; + description = "E-mail address of the server administrator."; + }; + + logFormat = mkOption { + type = types.str; + default = "common"; + example = "combined"; + description = '' + Log format for log files. Possible values are: combined, common, referer, agent, none. + See <link xlink:href="https://httpd.apache.org/docs/2.4/logs.html"/> for more details. + ''; + }; + + logPerVirtualHost = mkOption { + type = types.bool; + default = true; + description = '' + If enabled, each virtual host gets its own + <filename>access.log</filename> and + <filename>error.log</filename>, namely suffixed by the + <option>hostName</option> of the virtual host. + ''; + }; + + user = mkOption { + type = types.str; + default = "wwwrun"; + description = '' + User account under which httpd children processes run. + + If you require the main httpd process to run as + <literal>root</literal> add the following configuration: + <programlisting> + systemd.services.httpd.serviceConfig.User = lib.mkForce "root"; + </programlisting> + ''; + }; + + group = mkOption { + type = types.str; + default = "wwwrun"; + description = '' + Group under which httpd children processes run. + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/httpd"; + description = '' + Directory for Apache's log files. It is created automatically. + ''; + }; + + virtualHosts = mkOption { + type = with types; attrsOf (submodule (import ./vhost-options.nix)); + default = { + localhost = { + documentRoot = "${pkg}/htdocs"; + }; + }; + defaultText = literalExpression '' + { + localhost = { + documentRoot = "''${package.out}/htdocs"; + }; + } + ''; + example = literalExpression '' + { + "foo.example.com" = { + forceSSL = true; + documentRoot = "/var/www/foo.example.com" + }; + "bar.example.com" = { + addSSL = true; + documentRoot = "/var/www/bar.example.com"; + }; + } + ''; + description = '' + Specification of the virtual hosts served by Apache. Each + element should be an attribute set specifying the + configuration of the virtual host. + ''; + }; + + enableMellon = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the mod_auth_mellon module."; + }; + + enablePHP = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the PHP module."; + }; + + phpPackage = mkOption { + type = types.package; + default = pkgs.php; + defaultText = literalExpression "pkgs.php"; + description = '' + Overridable attribute of the PHP package to use. + ''; + }; + + enablePerl = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the Perl module (mod_perl)."; + }; + + phpOptions = mkOption { + type = types.lines; + default = ""; + example = + '' + date.timezone = "CET" + ''; + description = '' + Options appended to the PHP configuration file <filename>php.ini</filename>. + ''; + }; + + mpm = mkOption { + type = types.enum [ "event" "prefork" "worker" ]; + default = "event"; + example = "worker"; + description = + '' + Multi-processing module to be used by Apache. Available + modules are <literal>prefork</literal> (handles each + request in a separate child process), <literal>worker</literal> + (hybrid approach that starts a number of child processes + each running a number of threads) and <literal>event</literal> + (the default; a recent variant of <literal>worker</literal> + that handles persistent connections more efficiently). + ''; + }; + + maxClients = mkOption { + type = types.int; + default = 150; + example = 8; + description = "Maximum number of httpd processes (prefork)"; + }; + + maxRequestsPerChild = mkOption { + type = types.int; + default = 0; + example = 500; + description = '' + Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited. + ''; + }; + + sslCiphers = mkOption { + type = types.str; + default = "HIGH:!aNULL:!MD5:!EXP"; + description = "Cipher Suite available for negotiation in SSL proxy handshake."; + }; + + sslProtocols = mkOption { + type = types.str; + default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1"; + example = "All -SSLv2 -SSLv3"; + description = "Allowed SSL/TLS protocol versions."; + }; + }; + + }; + + # implementation + + config = mkIf cfg.enable { + + assertions = [ + { + assertion = all (hostOpts: !hostOpts.enableSSL) vhosts; + message = '' + The option `services.httpd.virtualHosts.<name>.enableSSL` no longer has any effect; please remove it. + Select one of `services.httpd.virtualHosts.<name>.addSSL`, `services.httpd.virtualHosts.<name>.forceSSL`, + or `services.httpd.virtualHosts.<name>.onlySSL`. + ''; + } + { + assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts; + message = '' + Options `services.httpd.virtualHosts.<name>.addSSL`, + `services.httpd.virtualHosts.<name>.onlySSL` and `services.httpd.virtualHosts.<name>.forceSSL` + are mutually exclusive. + ''; + } + { + assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts; + message = '' + Options `services.httpd.virtualHosts.<name>.enableACME` and + `services.httpd.virtualHosts.<name>.useACMEHost` are mutually exclusive. + ''; + } + ] ++ map (name: mkCertOwnershipAssertion { + inherit (cfg) group user; + cert = config.security.acme.certs.${name}; + groups = config.users.groups; + }) dependentCertNames; + + warnings = + mapAttrsToList (name: hostOpts: '' + Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS. + '') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts); + + users.users = optionalAttrs (cfg.user == "wwwrun") { + wwwrun = { + group = cfg.group; + description = "Apache httpd user"; + uid = config.ids.uids.wwwrun; + }; + }; + + users.groups = optionalAttrs (cfg.group == "wwwrun") { + wwwrun.gid = config.ids.gids.wwwrun; + }; + + security.acme.certs = let + acmePairs = map (hostOpts: let + hasRoot = hostOpts.acmeRoot != null; + in nameValuePair hostOpts.hostName { + group = mkDefault cfg.group; + # if acmeRoot is null inherit config.security.acme + # Since config.security.acme.certs.<cert>.webroot's own default value + # should take precedence set priority higher than mkOptionDefault + webroot = mkOverride (if hasRoot then 1000 else 2000) hostOpts.acmeRoot; + # Also nudge dnsProvider to null in case it is inherited + dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null; + extraDomainNames = hostOpts.serverAliases; + # Use the vhost-specific email address if provided, otherwise let + # security.acme.email or security.acme.certs.<cert>.email be used. + email = mkOverride 2000 (if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr); + # Filter for enableACME-only vhosts. Don't want to create dud certs + }) (filter (hostOpts: hostOpts.useACMEHost == null) acmeEnabledVhosts); + in listToAttrs acmePairs; + + # httpd requires a stable path to the configuration file for reloads + environment.etc."httpd/httpd.conf".source = cfg.configFile; + environment.systemPackages = [ + apachectl + pkg + ]; + + services.logrotate = optionalAttrs (cfg.logFormat != "none") { + enable = mkDefault true; + paths.httpd = { + path = "${cfg.logDir}/*.log"; + user = cfg.user; + group = cfg.group; + frequency = "daily"; + keep = 28; + extraConfig = '' + sharedscripts + compress + delaycompress + postrotate + systemctl reload httpd.service > /dev/null 2>/dev/null || true + endscript + ''; + }; + }; + + services.httpd.phpOptions = + '' + ; Don't advertise PHP + expose_php = off + '' + optionalString (config.time.timeZone != null) '' + + ; Apparently PHP doesn't use $TZ. + date.timezone = "${config.time.timeZone}" + ''; + + services.httpd.extraModules = mkBefore [ + # HTTP authentication mechanisms: basic and digest. + "auth_basic" "auth_digest" + + # Authentication: is the user who he claims to be? + "authn_file" "authn_dbm" "authn_anon" + + # Authorization: is the user allowed access? + "authz_user" "authz_groupfile" "authz_host" + + # Other modules. + "ext_filter" "include" "env" "mime_magic" + "cern_meta" "expires" "headers" "usertrack" "setenvif" + "dav" "status" "asis" "info" "dav_fs" + "vhost_alias" "imagemap" "actions" "speling" + "proxy" "proxy_http" + "cache" "cache_disk" + + # For compatibility with old configurations, the new module mod_access_compat is provided. + "access_compat" + ]; + + systemd.tmpfiles.rules = + let + svc = config.systemd.services.httpd.serviceConfig; + in + [ + "d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}" + "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}" + ]; + + systemd.services.httpd = { + description = "Apache HTTPD"; + wantedBy = [ "multi-user.target" ]; + wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames); + after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames; + before = map (certName: "acme-${certName}.service") dependentCertNames; + restartTriggers = [ cfg.configFile ]; + + path = [ pkg pkgs.coreutils pkgs.gnugrep ]; + + environment = + optionalAttrs cfg.enablePHP { PHPRC = phpIni; } + // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; }; + + preStart = + '' + # Get rid of old semaphores. These tend to accumulate across + # server restarts, eventually preventing it from restarting + # successfully. + for i in $(${pkgs.util-linux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do + ${pkgs.util-linux}/bin/ipcrm -s $i + done + ''; + + serviceConfig = { + ExecStart = "@${pkg}/bin/httpd httpd -f /etc/httpd/httpd.conf"; + ExecStop = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful-stop"; + ExecReload = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful"; + User = cfg.user; + Group = cfg.group; + Type = "forking"; + PIDFile = "${runtimeDir}/httpd.pid"; + Restart = "always"; + RestartSec = "5s"; + RuntimeDirectory = "httpd httpd/runtime"; + RuntimeDirectoryMode = "0750"; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + }; + }; + + # postRun hooks on cert renew can't be used to restart Apache since renewal + # runs as the unprivileged acme user. sslTargets are added to wantedBy + before + # which allows the acme-finished-$cert.target to signify the successful updating + # of certs end-to-end. + systemd.services.httpd-config-reload = let + sslServices = map (certName: "acme-${certName}.service") dependentCertNames; + sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames; + in mkIf (sslServices != []) { + wantedBy = sslServices ++ [ "multi-user.target" ]; + # Before the finished targets, after the renew services. + # This service might be needed for HTTP-01 challenges, but we only want to confirm + # certs are updated _after_ config has been reloaded. + before = sslTargets; + after = sslServices; + restartTriggers = [ cfg.configFile ]; + # Block reloading if not all certs exist yet. + # Happens when config changes add new vhosts/certs. + unitConfig.ConditionPathExists = map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames; + serviceConfig = { + Type = "oneshot"; + TimeoutSec = 60; + ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active httpd.service"; + ExecStartPre = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -t"; + ExecStart = "/run/current-system/systemd/bin/systemctl reload httpd.service"; + }; + }; + + }; +} diff --git a/nixos/modules/services/web-servers/apache-httpd/location-options.nix b/nixos/modules/services/web-servers/apache-httpd/location-options.nix new file mode 100644 index 00000000000..8ea88f94f97 --- /dev/null +++ b/nixos/modules/services/web-servers/apache-httpd/location-options.nix @@ -0,0 +1,54 @@ +{ config, lib, name, ... }: +let + inherit (lib) mkOption types; +in +{ + options = { + + proxyPass = mkOption { + type = with types; nullOr str; + default = null; + example = "http://www.example.org/"; + description = '' + Sets up a simple reverse proxy as described by <link xlink:href="https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html#simple" />. + ''; + }; + + index = mkOption { + type = with types; nullOr str; + default = null; + example = "index.php index.html"; + description = '' + Adds DirectoryIndex directive. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_dir.html#directoryindex" />. + ''; + }; + + alias = mkOption { + type = with types; nullOr path; + default = null; + example = "/your/alias/directory"; + description = '' + Alias directory for requests. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_alias.html#alias" />. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + These lines go to the end of the location verbatim. + ''; + }; + + priority = mkOption { + type = types.int; + default = 1000; + description = '' + Order of this location block in relation to the others in the vhost. + The semantics are the same as with `lib.mkOrder`. Smaller values have + a greater priority. + ''; + }; + + }; +} diff --git a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix new file mode 100644 index 00000000000..c52ab2c596e --- /dev/null +++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix @@ -0,0 +1,295 @@ +{ config, lib, name, ... }: +let + inherit (lib) literalExpression mkOption nameValuePair types; +in +{ + options = { + + hostName = mkOption { + type = types.str; + default = name; + description = "Canonical hostname for the server."; + }; + + serverAliases = mkOption { + type = types.listOf types.str; + default = []; + example = ["www.example.org" "www.example.org:8080" "example.org"]; + description = '' + Additional names of virtual hosts served by this virtual host configuration. + ''; + }; + + listen = mkOption { + type = with types; listOf (submodule ({ + options = { + port = mkOption { + type = types.port; + description = "Port to listen on"; + }; + ip = mkOption { + type = types.str; + default = "*"; + description = "IP to listen on. 0.0.0.0 for IPv4 only, * for all."; + }; + ssl = mkOption { + type = types.bool; + default = false; + description = "Whether to enable SSL (https) support."; + }; + }; + })); + default = []; + example = [ + { ip = "195.154.1.1"; port = 443; ssl = true;} + { ip = "192.154.1.1"; port = 80; } + { ip = "*"; port = 8080; } + ]; + description = '' + Listen addresses and ports for this virtual host. + <note> + <para> + This option overrides <literal>addSSL</literal>, <literal>forceSSL</literal> and <literal>onlySSL</literal>. + </para> + <para> + If you only want to set the addresses manually and not the ports, take a look at <literal>listenAddresses</literal>. + </para> + </note> + ''; + }; + + listenAddresses = mkOption { + type = with types; nonEmptyListOf str; + + description = '' + Listen addresses for this virtual host. + Compared to <literal>listen</literal> this only sets the addreses + and the ports are chosen automatically. + ''; + default = [ "*" ]; + example = [ "127.0.0.1" ]; + }; + + enableSSL = mkOption { + type = types.bool; + visible = false; + default = false; + }; + + addSSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTPS in addition to plain HTTP. This will set defaults for + <literal>listen</literal> to listen on all interfaces on the respective default + ports (80, 443). + ''; + }; + + onlySSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTPS and reject plain HTTP connections. This will set + defaults for <literal>listen</literal> to listen on all interfaces on port 443. + ''; + }; + + forceSSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to add a separate nginx server block that permanently redirects (301) + all plain HTTP traffic to HTTPS. This will set defaults for + <literal>listen</literal> to listen on all interfaces on the respective default + ports (80, 443), where the non-SSL listens are used for the redirect vhosts. + ''; + }; + + enableACME = mkOption { + type = types.bool; + default = false; + description = '' + Whether to ask Let's Encrypt to sign a certificate for this vhost. + Alternately, you can use an existing certificate through <option>useACMEHost</option>. + ''; + }; + + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A host of an existing Let's Encrypt certificate to use. + This is useful if you have many subdomains and want to avoid hitting the + <link xlink:href="https://letsencrypt.org/docs/rate-limits/">rate limit</link>. + Alternately, you can generate a certificate through <option>enableACME</option>. + <emphasis>Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using <xref linkend="opt-security.acme.certs"/>.</emphasis> + ''; + }; + + acmeRoot = mkOption { + type = types.nullOr types.str; + default = "/var/lib/acme/acme-challenge"; + description = '' + Directory for the acme challenge which is PUBLIC, don't put certs or keys in here. + Set to null to inherit from config.security.acme. + ''; + }; + + sslServerCert = mkOption { + type = types.path; + example = "/var/host.cert"; + description = "Path to server SSL certificate."; + }; + + sslServerKey = mkOption { + type = types.path; + example = "/var/host.key"; + description = "Path to server SSL certificate key."; + }; + + sslServerChain = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/ca.pem"; + description = "Path to server SSL chain file."; + }; + + http2 = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable HTTP 2. HTTP/2 is supported in all multi-processing modules that come with httpd. <emphasis>However, if you use the prefork mpm, there will + be severe restrictions.</emphasis> Refer to <link xlink:href="https://httpd.apache.org/docs/2.4/howto/http2.html#mpm-config"/> for details. + ''; + }; + + adminAddr = mkOption { + type = types.nullOr types.str; + default = null; + example = "admin@example.org"; + description = "E-mail address of the server administrator."; + }; + + documentRoot = mkOption { + type = types.nullOr types.path; + default = null; + example = "/data/webserver/docs"; + description = '' + The path of Apache's document root directory. If left undefined, + an empty directory in the Nix store will be used as root. + ''; + }; + + servedDirs = mkOption { + type = types.listOf types.attrs; + default = []; + example = [ + { urlPath = "/nix"; + dir = "/home/eelco/Dev/nix-homepage"; + } + ]; + description = '' + This option provides a simple way to serve static directories. + ''; + }; + + servedFiles = mkOption { + type = types.listOf types.attrs; + default = []; + example = [ + { urlPath = "/foo/bar.png"; + file = "/home/eelco/some-file.png"; + } + ]; + description = '' + This option provides a simple way to serve individual, static files. + + <note><para> + This option has been deprecated and will be removed in a future + version of NixOS. You can achieve the same result by making use of + the <literal>locations.<name>.alias</literal> option. + </para></note> + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + <Directory /home> + Options FollowSymlinks + AllowOverride All + </Directory> + ''; + description = '' + These lines go to httpd.conf verbatim. They will go after + directories and directory aliases defined by default. + ''; + }; + + enableUserDir = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable serving <filename>~/public_html</filename> as + <literal>/~<replaceable>username</replaceable></literal>. + ''; + }; + + globalRedirect = mkOption { + type = types.nullOr types.str; + default = null; + example = "http://newserver.example.org/"; + description = '' + If set, all requests for this host are redirected permanently to + the given URL. + ''; + }; + + logFormat = mkOption { + type = types.str; + default = "common"; + example = "combined"; + description = '' + Log format for Apache's log files. Possible values are: combined, common, referer, agent. + ''; + }; + + robotsEntries = mkOption { + type = types.lines; + default = ""; + example = "Disallow: /foo/"; + description = '' + Specification of pages to be ignored by web crawlers. See <link + xlink:href='http://www.robotstxt.org/'/> for details. + ''; + }; + + locations = mkOption { + type = with types; attrsOf (submodule (import ./location-options.nix)); + default = {}; + example = literalExpression '' + { + "/" = { + proxyPass = "http://localhost:3000"; + }; + "/foo/bar.png" = { + alias = "/home/eelco/some-file.png"; + }; + }; + ''; + description = '' + Declarative location config. See <link + xlink:href="https://httpd.apache.org/docs/2.4/mod/core.html#location"/> for details. + ''; + }; + + }; + + config = { + + locations = builtins.listToAttrs (map (elem: nameValuePair elem.urlPath { alias = elem.file; }) config.servedFiles); + + }; +} diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix new file mode 100644 index 00000000000..2b8c6f2e308 --- /dev/null +++ b/nixos/modules/services/web-servers/caddy/default.nix @@ -0,0 +1,339 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.caddy; + + virtualHosts = attrValues cfg.virtualHosts; + acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts; + + mkVHostConf = hostOpts: + let + sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory; + in + '' + ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} { + bind ${concatStringsSep " " hostOpts.listenAddresses} + ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"} + log { + ${hostOpts.logFormat} + } + + ${hostOpts.extraConfig} + } + ''; + + configFile = + let + Caddyfile = pkgs.writeText "Caddyfile" '' + { + ${cfg.globalConfig} + } + ${cfg.extraConfig} + ''; + + Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } '' + ${cfg.package}/bin/caddy fmt ${Caddyfile} > $out + ''; + in + if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile; + + acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts); + + mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; +in +{ + imports = [ + (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2") + (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ]) + (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ]) + ]; + + # interface + options.services.caddy = { + enable = mkEnableOption "Caddy web server"; + + user = mkOption { + default = "caddy"; + type = types.str; + description = '' + User account under which caddy runs. + + <note><para> + If left as the default value this user will automatically be created + on system activation, otherwise you are responsible for + ensuring the user exists before the Caddy service starts. + </para></note> + ''; + }; + + group = mkOption { + default = "caddy"; + type = types.str; + description = '' + Group account under which caddy runs. + + <note><para> + If left as the default value this user will automatically be created + on system activation, otherwise you are responsible for + ensuring the user exists before the Caddy service starts. + </para></note> + ''; + }; + + package = mkOption { + default = pkgs.caddy; + defaultText = literalExpression "pkgs.caddy"; + type = types.package; + description = '' + Caddy package to use. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/caddy"; + description = '' + The data directory for caddy. + + <note> + <para> + If left as the default value this directory will automatically be created + before the Caddy server starts, otherwise you are responsible for ensuring + the directory exists with appropriate ownership and permissions. + </para> + <para> + Caddy v2 replaced <literal>CADDYPATH</literal> with XDG directories. + See <link xlink:href="https://caddyserver.com/docs/conventions#file-locations"/>. + </para> + </note> + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/caddy"; + description = '' + Directory for storing Caddy access logs. + + <note><para> + If left as the default value this directory will automatically be created + before the Caddy server starts, otherwise the sysadmin is responsible for + ensuring the directory exists with appropriate ownership and permissions. + </para></note> + ''; + }; + + logFormat = mkOption { + type = types.lines; + default = '' + level ERROR + ''; + example = literalExpression '' + mkForce "level INFO"; + ''; + description = '' + Configuration for the default logger. See + <link xlink:href="https://caddyserver.com/docs/caddyfile/options#log"/> + for details. + ''; + }; + + configFile = mkOption { + type = types.path; + default = configFile; + defaultText = "A Caddyfile automatically generated by values from services.caddy.*"; + example = literalExpression '' + pkgs.writeText "Caddyfile" ''' + example.com + + root * /var/www/wordpress + php_fastcgi unix//run/php/php-version-fpm.sock + file_server + '''; + ''; + description = '' + Override the configuration file used by Caddy. By default, + NixOS generates one automatically. + ''; + }; + + adapter = mkOption { + default = "caddyfile"; + example = "nginx"; + type = types.str; + description = '' + Name of the config adapter to use. + See <link xlink:href="https://caddyserver.com/docs/config-adapters"/> + for the full list. + + <note><para> + Any value other than <literal>caddyfile</literal> is only valid when + providing your own <option>configFile</option>. + </para></note> + ''; + }; + + resume = mkOption { + default = false; + type = types.bool; + description = '' + Use saved config, if any (and prefer over any specified configuration passed with <literal>--config</literal>). + ''; + }; + + globalConfig = mkOption { + type = types.lines; + default = ""; + example = '' + debug + servers { + protocol { + experimental_http3 + } + } + ''; + description = '' + Additional lines of configuration appended to the global config section + of the <literal>Caddyfile</literal>. + + Refer to <link xlink:href="https://caddyserver.com/docs/caddyfile/options#global-options"/> + for details on supported values. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + example.com { + encode gzip + log + root /srv/http + } + ''; + description = '' + Additional lines of configuration appended to the automatically + generated <literal>Caddyfile</literal>. + ''; + }; + + virtualHosts = mkOption { + type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; })); + default = {}; + example = literalExpression '' + { + "hydra.example.com" = { + serverAliases = [ "www.hydra.example.com" ]; + extraConfig = ''' + encode gzip + root /srv/http + '''; + }; + }; + ''; + description = '' + Declarative specification of virtual hosts served by Caddy. + ''; + }; + + acmeCA = mkOption { + default = "https://acme-v02.api.letsencrypt.org/directory"; + example = "https://acme-staging-v02.api.letsencrypt.org/directory"; + type = with types; nullOr str; + description = '' + The URL to the ACME CA's directory. It is strongly recommended to set + this to Let's Encrypt's staging endpoint for testing or development. + + Set it to <literal>null</literal> if you want to write a more + fine-grained configuration manually. + ''; + }; + + email = mkOption { + default = null; + type = with types; nullOr str; + description = '' + Your email address. Mainly used when creating an ACME account with your + CA, and is highly recommended in case there are problems with your + certificates. + ''; + }; + + }; + + # implementation + config = mkIf cfg.enable { + + assertions = [ + { assertion = cfg.adapter != "caddyfile" -> cfg.configFile != configFile; + message = "Any value other than 'caddyfile' is only valid when providing your own `services.caddy.configFile`"; + } + ] ++ map (name: mkCertOwnershipAssertion { + inherit (cfg) group user; + cert = config.security.acme.certs.${name}; + groups = config.users.groups; + }) acmeHosts; + + services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts; + services.caddy.globalConfig = '' + ${optionalString (cfg.email != null) "email ${cfg.email}"} + ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"} + log { + ${cfg.logFormat} + } + ''; + + systemd.packages = [ cfg.package ]; + systemd.services.caddy = { + wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts; + after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts; + before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts; + + wantedBy = [ "multi-user.target" ]; + startLimitIntervalSec = 14400; + startLimitBurst = 10; + + serviceConfig = { + # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= + # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect. + ExecStart = [ "" "${cfg.package}/bin/caddy run --config ${cfg.configFile} --adapter ${cfg.adapter} ${optionalString cfg.resume "--resume"}" ]; + ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${cfg.configFile} --adapter ${cfg.adapter}" ]; + + ExecStartPre = "${cfg.package}/bin/caddy validate --config ${cfg.configFile} --adapter ${cfg.adapter}"; + User = cfg.user; + Group = cfg.group; + ReadWriteDirectories = cfg.dataDir; + StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ]; + LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ]; + Restart = "on-abnormal"; + SupplementaryGroups = mkIf (length acmeVHosts != 0) [ "acme" ]; + + # TODO: attempt to upstream these options + NoNewPrivileges = true; + PrivateDevices = true; + ProtectHome = true; + }; + }; + + users.users = optionalAttrs (cfg.user == "caddy") { + caddy = { + group = cfg.group; + uid = config.ids.uids.caddy; + home = cfg.dataDir; + }; + }; + + users.groups = optionalAttrs (cfg.group == "caddy") { + caddy.gid = config.ids.gids.caddy; + }; + + security.acme.certs = + let + reloads = map (useACMEHost: nameValuePair useACMEHost { reloadServices = [ "caddy.service" ]; }) acmeHosts; + in + listToAttrs reloads; + + }; +} diff --git a/nixos/modules/services/web-servers/caddy/vhost-options.nix b/nixos/modules/services/web-servers/caddy/vhost-options.nix new file mode 100644 index 00000000000..f240ec605c2 --- /dev/null +++ b/nixos/modules/services/web-servers/caddy/vhost-options.nix @@ -0,0 +1,79 @@ +{ cfg }: +{ config, lib, name, ... }: +let + inherit (lib) literalExpression mkOption types; +in +{ + options = { + + hostName = mkOption { + type = types.str; + default = name; + description = "Canonical hostname for the server."; + }; + + serverAliases = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "www.example.org" "example.org" ]; + description = '' + Additional names of virtual hosts served by this virtual host configuration. + ''; + }; + + listenAddresses = mkOption { + type = with types; listOf str; + description = '' + A list of host interfaces to bind to for this virtual host. + ''; + default = [ ]; + example = [ "127.0.0.1" "::1" ]; + }; + + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A host of an existing Let's Encrypt certificate to use. + This is mostly useful if you use DNS challenges but Caddy does not + currently support your provider. + + <emphasis>Note that this option does not create any certificates, nor + does it add subdomains to existing ones – you will need to create them + manually using <xref linkend="opt-security.acme.certs"/>. Additionally, + you should probably add the <literal>caddy</literal> user to the + <literal>acme</literal> group to grant access to the certificates.</emphasis> + ''; + }; + + logFormat = mkOption { + type = types.lines; + default = '' + output file ${cfg.logDir}/access-${config.hostName}.log + ''; + defaultText = '' + output file ''${config.services.caddy.logDir}/access-''${hostName}.log + ''; + example = literalExpression '' + mkForce ''' + output discard + '''; + ''; + description = '' + Configuration for HTTP request logging (also known as access logs). See + <link xlink:href="https://caddyserver.com/docs/caddyfile/directives/log#log"/> + for details. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional lines of configuration appended to this virtual host in the + automatically generated <literal>Caddyfile</literal>. + ''; + }; + + }; +} diff --git a/nixos/modules/services/web-servers/darkhttpd.nix b/nixos/modules/services/web-servers/darkhttpd.nix new file mode 100644 index 00000000000..f6b693139a1 --- /dev/null +++ b/nixos/modules/services/web-servers/darkhttpd.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.darkhttpd; + + args = concatStringsSep " " ([ + cfg.rootDir + "--port ${toString cfg.port}" + "--addr ${cfg.address}" + ] ++ cfg.extraArgs + ++ optional cfg.hideServerId "--no-server-id" + ++ optional config.networking.enableIPv6 "--ipv6"); + +in { + options.services.darkhttpd = with types; { + enable = mkEnableOption "DarkHTTPd web server"; + + port = mkOption { + default = 80; + type = types.port; + description = '' + Port to listen on. + Pass 0 to let the system choose any free port for you. + ''; + }; + + address = mkOption { + default = "127.0.0.1"; + type = str; + description = '' + Address to listen on. + Pass `all` to listen on all interfaces. + ''; + }; + + rootDir = mkOption { + type = path; + description = '' + Path from which to serve files. + ''; + }; + + hideServerId = mkOption { + type = bool; + default = true; + description = '' + Don't identify the server type in headers or directory listings. + ''; + }; + + extraArgs = mkOption { + type = listOf str; + default = []; + description = '' + Additional configuration passed to the executable. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.darkhttpd = { + description = "Dark HTTPd"; + wants = [ "network.target" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + DynamicUser = true; + ExecStart = "${pkgs.darkhttpd}/bin/darkhttpd ${args}"; + AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + Restart = "on-failure"; + RestartSec = "2s"; + }; + }; + }; +} diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix new file mode 100644 index 00000000000..a64a187255a --- /dev/null +++ b/nixos/modules/services/web-servers/fcgiwrap.nix @@ -0,0 +1,72 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.fcgiwrap; +in { + + options = { + services.fcgiwrap = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable fcgiwrap, a server for running CGI applications over FastCGI."; + }; + + preforkProcesses = mkOption { + type = types.int; + default = 1; + description = "Number of processes to prefork."; + }; + + socketType = mkOption { + type = types.enum [ "unix" "tcp" "tcp6" ]; + default = "unix"; + description = "Socket type: 'unix', 'tcp' or 'tcp6'."; + }; + + socketAddress = mkOption { + type = types.str; + default = "/run/fcgiwrap.sock"; + example = "1.2.3.4:5678"; + description = "Socket address. In case of a UNIX socket, this should be its filesystem path."; + }; + + user = mkOption { + type = types.nullOr types.str; + default = null; + description = "User permissions for the socket."; + }; + + group = mkOption { + type = types.nullOr types.str; + default = null; + description = "Group permissions for the socket."; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.fcgiwrap = { + after = [ "nss-user-lookup.target" ]; + wantedBy = optional (cfg.socketType != "unix") "multi-user.target"; + + serviceConfig = { + ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${ + if (cfg.socketType != "unix") then "-s ${cfg.socketType}:${cfg.socketAddress}" else "" + }"; + } // (if cfg.user != null && cfg.group != null then { + User = cfg.user; + Group = cfg.group; + } else { } ); + }; + + systemd.sockets = if (cfg.socketType == "unix") then { + fcgiwrap = { + wantedBy = [ "sockets.target" ]; + socketConfig.ListenStream = cfg.socketAddress; + }; + } else { }; + }; +} diff --git a/nixos/modules/services/web-servers/hitch/default.nix b/nixos/modules/services/web-servers/hitch/default.nix new file mode 100644 index 00000000000..1812f225b74 --- /dev/null +++ b/nixos/modules/services/web-servers/hitch/default.nix @@ -0,0 +1,111 @@ +{ config, lib, pkgs, ...}: +let + cfg = config.services.hitch; + ocspDir = lib.optionalString cfg.ocsp-stapling.enabled "/var/cache/hitch/ocsp"; + hitchConfig = with lib; pkgs.writeText "hitch.conf" (concatStringsSep "\n" [ + ("backend = \"${cfg.backend}\"") + (concatMapStrings (s: "frontend = \"${s}\"\n") cfg.frontend) + (concatMapStrings (s: "pem-file = \"${s}\"\n") cfg.pem-files) + ("ciphers = \"${cfg.ciphers}\"") + ("ocsp-dir = \"${ocspDir}\"") + "user = \"${cfg.user}\"" + "group = \"${cfg.group}\"" + cfg.extraConfig + ]); +in +with lib; +{ + options = { + services.hitch = { + enable = mkEnableOption "Hitch Server"; + + backend = mkOption { + type = types.str; + description = '' + The host and port Hitch connects to when receiving + a connection in the form [HOST]:PORT + ''; + }; + + ciphers = mkOption { + type = types.str; + default = "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; + description = "The list of ciphers to use"; + }; + + frontend = mkOption { + type = types.either types.str (types.listOf types.str); + default = "[127.0.0.1]:443"; + description = '' + The port and interface of the listen endpoint in the ++ form [HOST]:PORT[+CERT]. + ''; + apply = toList; + }; + + pem-files = mkOption { + type = types.listOf types.path; + default = []; + description = "PEM files to use"; + }; + + ocsp-stapling = { + enabled = mkOption { + type = types.bool; + default = true; + description = "Whether to enable OCSP Stapling"; + }; + }; + + user = mkOption { + type = types.str; + default = "hitch"; + description = "The user to run as"; + }; + + group = mkOption { + type = types.str; + default = "hitch"; + description = "The group to run as"; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Additional configuration lines"; + }; + }; + + }; + + config = mkIf cfg.enable { + + systemd.services.hitch = { + description = "Hitch"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + preStart = '' + ${pkgs.hitch}/sbin/hitch -t --config ${hitchConfig} + '' + (optionalString cfg.ocsp-stapling.enabled '' + mkdir -p ${ocspDir} + chown -R hitch:hitch ${ocspDir} + ''); + serviceConfig = { + Type = "forking"; + ExecStart = "${pkgs.hitch}/sbin/hitch --daemon --config ${hitchConfig}"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + Restart = "always"; + RestartSec = "5s"; + LimitNOFILE = 131072; + }; + }; + + environment.systemPackages = [ pkgs.hitch ]; + + users.users.hitch = { + group = "hitch"; + isSystemUser = true; + }; + users.groups.hitch = {}; + }; +} diff --git a/nixos/modules/services/web-servers/hydron.nix b/nixos/modules/services/web-servers/hydron.nix new file mode 100644 index 00000000000..a4a5a435b2e --- /dev/null +++ b/nixos/modules/services/web-servers/hydron.nix @@ -0,0 +1,165 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.hydron; +in with lib; { + options.services.hydron = { + enable = mkEnableOption "hydron"; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/hydron"; + example = "/home/okina/hydron"; + description = "Location where hydron runs and stores data."; + }; + + interval = mkOption { + type = types.str; + default = "weekly"; + example = "06:00"; + description = '' + How often we run hydron import and possibly fetch tags. Runs by default every week. + + The format is described in + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry>. + ''; + }; + + password = mkOption { + type = types.str; + default = "hydron"; + example = "dumbpass"; + description = "Password for the hydron database."; + }; + + passwordFile = mkOption { + type = types.path; + default = "/run/keys/hydron-password-file"; + example = "/home/okina/hydron/keys/pass"; + description = "Password file for the hydron database."; + }; + + postgresArgs = mkOption { + type = types.str; + description = "Postgresql connection arguments."; + example = '' + { + "driver": "postgres", + "connection": "user=hydron password=dumbpass dbname=hydron sslmode=disable" + } + ''; + }; + + postgresArgsFile = mkOption { + type = types.path; + default = "/run/keys/hydron-postgres-args"; + example = "/home/okina/hydron/keys/postgres"; + description = "Postgresql connection arguments file."; + }; + + listenAddress = mkOption { + type = types.nullOr types.str; + default = null; + example = "127.0.0.1:8010"; + description = "Listen on a specific IP address and port."; + }; + + importPaths = mkOption { + type = types.listOf types.path; + default = []; + example = [ "/home/okina/Pictures" ]; + description = "Paths that hydron will recursively import."; + }; + + fetchTags = mkOption { + type = types.bool; + default = true; + description = "Fetch tags for imported images and webm from gelbooru."; + }; + }; + + config = mkIf cfg.enable { + services.hydron.passwordFile = mkDefault (pkgs.writeText "hydron-password-file" cfg.password); + services.hydron.postgresArgsFile = mkDefault (pkgs.writeText "hydron-postgres-args" cfg.postgresArgs); + services.hydron.postgresArgs = mkDefault '' + { + "driver": "postgres", + "connection": "user=hydron password=${cfg.password} host=/run/postgresql dbname=hydron sslmode=disable" + } + ''; + + services.postgresql = { + enable = true; + ensureDatabases = [ "hydron" ]; + ensureUsers = [ + { name = "hydron"; + ensurePermissions = { "DATABASE hydron" = "ALL PRIVILEGES"; }; + } + ]; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0750 hydron hydron - -" + "d '${cfg.dataDir}/.hydron' - hydron hydron - -" + "d '${cfg.dataDir}/images' - hydron hydron - -" + "Z '${cfg.dataDir}' - hydron hydron - -" + + "L+ '${cfg.dataDir}/.hydron/db_conf.json' - - - - ${cfg.postgresArgsFile}" + ]; + + systemd.services.hydron = { + description = "hydron"; + after = [ "network.target" "postgresql.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + User = "hydron"; + Group = "hydron"; + ExecStart = "${pkgs.hydron}/bin/hydron serve" + + optionalString (cfg.listenAddress != null) " -a ${cfg.listenAddress}"; + }; + }; + + systemd.services.hydron-fetch = { + description = "Import paths into hydron and possibly fetch tags"; + + serviceConfig = { + Type = "oneshot"; + User = "hydron"; + Group = "hydron"; + ExecStart = "${pkgs.hydron}/bin/hydron import " + + optionalString cfg.fetchTags "-f " + + (escapeShellArg cfg.dataDir) + "/images " + (escapeShellArgs cfg.importPaths); + }; + }; + + systemd.timers.hydron-fetch = { + description = "Automatically import paths into hydron and possibly fetch tags"; + after = [ "network.target" "hydron.service" ]; + wantedBy = [ "timers.target" ]; + + timerConfig = { + Persistent = true; + OnCalendar = cfg.interval; + }; + }; + + users = { + groups.hydron.gid = config.ids.gids.hydron; + + users.hydron = { + description = "hydron server service user"; + home = cfg.dataDir; + group = "hydron"; + uid = config.ids.uids.hydron; + }; + }; + }; + + imports = [ + (mkRenamedOptionModule [ "services" "hydron" "baseDir" ] [ "services" "hydron" "dataDir" ]) + ]; + + meta.maintainers = with maintainers; [ chiiruno ]; +} diff --git a/nixos/modules/services/web-servers/jboss/builder.sh b/nixos/modules/services/web-servers/jboss/builder.sh new file mode 100644 index 00000000000..0e5af324c13 --- /dev/null +++ b/nixos/modules/services/web-servers/jboss/builder.sh @@ -0,0 +1,72 @@ +set -e + +source $stdenv/setup + +mkdir -p $out/bin + +cat > $out/bin/control <<EOF +mkdir -p $logDir +chown -R $user $logDir +export PATH=$PATH:$su/bin + +start() +{ + su $user -s /bin/sh -c "$jboss/bin/run.sh \ + -Djboss.server.base.dir=$serverDir \ + -Djboss.server.base.url=file://$serverDir \ + -Djboss.server.temp.dir=$tempDir \ + -Djboss.server.log.dir=$logDir \ + -Djboss.server.lib.url=$libUrl \ + -c default" +} + +stop() +{ + su $user -s /bin/sh -c "$jboss/bin/shutdown.sh -S" +} + +if test "\$1" = start +then + trap stop 15 + + start +elif test "\$1" = stop +then + stop +elif test "\$1" = init +then + echo "Are you sure you want to create a new server instance (old server instance will be lost!)?" + read answer + + if ! test \$answer = "yes" + then + exit 1 + fi + + rm -rf $serverDir + mkdir -p $serverDir + cd $serverDir + cp -av $jboss/server/default . + sed -i -e "s|deploy/|$deployDir|" default/conf/jboss-service.xml + + if ! test "$useJK" = "" + then + sed -i -e 's|<attribute name="UseJK">false</attribute>|<attribute name="UseJK">true</attribute>|' default/deploy/jboss-web.deployer/META-INF/jboss-service.xml + sed -i -e 's|<Engine name="jboss.web" defaultHost="localhost">|<Engine name="jboss.web" defaultHost="localhost" jvmRoute="node1">|' default/deploy/jboss-web.deployer/server.xml + fi + + # Make files accessible for the server user + + chown -R $user $serverDir + for i in \`find $serverDir -type d\` + do + chmod 755 \$i + done + for i in \`find $serverDir -type f\` + do + chmod 644 \$i + done +fi +EOF + +chmod +x $out/bin/* diff --git a/nixos/modules/services/web-servers/jboss/default.nix b/nixos/modules/services/web-servers/jboss/default.nix new file mode 100644 index 00000000000..d243e0f3f1b --- /dev/null +++ b/nixos/modules/services/web-servers/jboss/default.nix @@ -0,0 +1,88 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.jboss; + + jbossService = pkgs.stdenv.mkDerivation { + name = "jboss-server"; + builder = ./builder.sh; + inherit (pkgs) jboss su; + inherit (cfg) tempDir logDir libUrl deployDir serverDir user useJK; + }; + +in + +{ + + ###### interface + + options = { + + services.jboss = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable JBoss. WARNING : this package is outdated and is known to have vulnerabilities."; + }; + + tempDir = mkOption { + default = "/tmp"; + type = types.str; + description = "Location where JBoss stores its temp files"; + }; + + logDir = mkOption { + default = "/var/log/jboss"; + type = types.str; + description = "Location of the logfile directory of JBoss"; + }; + + serverDir = mkOption { + description = "Location of the server instance files"; + default = "/var/jboss/server"; + type = types.str; + }; + + deployDir = mkOption { + description = "Location of the deployment files"; + default = "/nix/var/nix/profiles/default/server/default/deploy/"; + type = types.str; + }; + + libUrl = mkOption { + default = "file:///nix/var/nix/profiles/default/server/default/lib"; + description = "Location where the shared library JARs are stored"; + type = types.str; + }; + + user = mkOption { + default = "nobody"; + description = "User account under which jboss runs."; + type = types.str; + }; + + useJK = mkOption { + type = types.bool; + default = false; + description = "Whether to use to connector to the Apache HTTP server"; + }; + + }; + + }; + + + ###### implementation + + config = mkIf config.services.jboss.enable { + systemd.services.jboss = { + description = "JBoss server"; + script = "${jbossService}/bin/control start"; + wantedBy = [ "multi-user.target" ]; + }; + }; +} diff --git a/nixos/modules/services/web-servers/lighttpd/cgit.nix b/nixos/modules/services/web-servers/lighttpd/cgit.nix new file mode 100644 index 00000000000..8cd6d020940 --- /dev/null +++ b/nixos/modules/services/web-servers/lighttpd/cgit.nix @@ -0,0 +1,93 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.lighttpd.cgit; + pathPrefix = if stringLength cfg.subdir == 0 then "" else "/" + cfg.subdir; + configFile = pkgs.writeText "cgitrc" + '' + # default paths to static assets + css=${pathPrefix}/cgit.css + logo=${pathPrefix}/cgit.png + favicon=${pathPrefix}/favicon.ico + + # user configuration + ${cfg.configText} + ''; +in +{ + + options.services.lighttpd.cgit = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + If true, enable cgit (fast web interface for git repositories) as a + sub-service in lighttpd. + ''; + }; + + subdir = mkOption { + default = "cgit"; + example = ""; + type = types.str; + description = '' + The subdirectory in which to serve cgit. The web application will be + accessible at http://yourserver/''${subdir} + ''; + }; + + configText = mkOption { + default = ""; + example = literalExpression '' + ''' + source-filter=''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py + about-filter=''${pkgs.cgit}/lib/cgit/filters/about-formatting.sh + cache-size=1000 + scan-path=/srv/git + ''' + ''; + type = types.lines; + description = '' + Verbatim contents of the cgit runtime configuration file. Documentation + (with cgitrc example file) is available in "man cgitrc". Or online: + http://git.zx2c4.com/cgit/tree/cgitrc.5.txt + ''; + }; + + }; + + config = mkIf cfg.enable { + + # make the cgitrc manpage available + environment.systemPackages = [ pkgs.cgit ]; + + # declare module dependencies + services.lighttpd.enableModules = [ "mod_cgi" "mod_alias" "mod_setenv" ]; + + services.lighttpd.extraConfig = '' + $HTTP["url"] =~ "^/${cfg.subdir}" { + cgi.assign = ( + "cgit.cgi" => "${pkgs.cgit}/cgit/cgit.cgi" + ) + alias.url = ( + "${pathPrefix}/cgit.css" => "${pkgs.cgit}/cgit/cgit.css", + "${pathPrefix}/cgit.png" => "${pkgs.cgit}/cgit/cgit.png", + "${pathPrefix}" => "${pkgs.cgit}/cgit/cgit.cgi" + ) + setenv.add-environment = ( + "CGIT_CONFIG" => "${configFile}" + ) + } + ''; + + systemd.services.lighttpd.preStart = '' + mkdir -p /var/cache/cgit + chown lighttpd:lighttpd /var/cache/cgit + ''; + + }; + +} diff --git a/nixos/modules/services/web-servers/lighttpd/collectd.nix b/nixos/modules/services/web-servers/lighttpd/collectd.nix new file mode 100644 index 00000000000..5f091591daf --- /dev/null +++ b/nixos/modules/services/web-servers/lighttpd/collectd.nix @@ -0,0 +1,62 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + cfg = config.services.lighttpd.collectd; + opt = options.services.lighttpd.collectd; + + collectionConf = pkgs.writeText "collection.conf" '' + datadir: "${config.services.collectd.dataDir}" + libdir: "${config.services.collectd.package}/lib/collectd" + ''; + + defaultCollectionCgi = config.services.collectd.package.overrideDerivation(old: { + name = "collection.cgi"; + dontConfigure = true; + buildPhase = "true"; + installPhase = '' + substituteInPlace contrib/collection.cgi --replace '"/etc/collection.conf"' '$ENV{COLLECTION_CONF}' + cp contrib/collection.cgi $out + ''; + }); +in +{ + + options.services.lighttpd.collectd = { + + enable = mkEnableOption "collectd subservice accessible at http://yourserver/collectd"; + + collectionCgi = mkOption { + type = types.path; + default = defaultCollectionCgi; + defaultText = literalDocBook '' + <literal>config.${options.services.collectd.package}</literal> configured for lighttpd + ''; + description = '' + Path to collection.cgi script from (collectd sources)/contrib/collection.cgi + This option allows to use a customized version + ''; + }; + }; + + config = mkIf cfg.enable { + services.lighttpd.enableModules = [ "mod_cgi" "mod_alias" "mod_setenv" ]; + + services.lighttpd.extraConfig = '' + $HTTP["url"] =~ "^/collectd" { + cgi.assign = ( + ".cgi" => "${pkgs.perl}/bin/perl" + ) + alias.url = ( + "/collectd" => "${cfg.collectionCgi}" + ) + setenv.add-environment = ( + "PERL5LIB" => "${with pkgs.perlPackages; makePerlPath [ CGI HTMLParser URI pkgs.rrdtool ]}", + "COLLECTION_CONF" => "${collectionConf}" + ) + } + ''; + }; + +} diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix new file mode 100644 index 00000000000..05e897c8cc9 --- /dev/null +++ b/nixos/modules/services/web-servers/lighttpd/default.nix @@ -0,0 +1,268 @@ +# NixOS module for lighttpd web server + +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.lighttpd; + + # List of known lighttpd modules, ordered by how the lighttpd documentation + # recommends them being imported: + # http://redmine.lighttpd.net/projects/1/wiki/Server_modulesDetails + # + # Some modules are always imported and should not appear in the config: + # disallowedModules = [ "mod_indexfile" "mod_dirlisting" "mod_staticfile" ]; + # + # For full module list, see the output of running ./configure in the lighttpd + # source. + allKnownModules = [ + "mod_rewrite" + "mod_redirect" + "mod_alias" + "mod_access" + "mod_auth" + "mod_status" + "mod_simple_vhost" + "mod_evhost" + "mod_userdir" + "mod_secdownload" + "mod_fastcgi" + "mod_proxy" + "mod_cgi" + "mod_ssi" + "mod_compress" + "mod_usertrack" + "mod_expire" + "mod_rrdtool" + "mod_accesslog" + # Remaining list of modules, order assumed to be unimportant. + "mod_authn_dbi" + "mod_authn_file" + "mod_authn_gssapi" + "mod_authn_ldap" + "mod_authn_mysql" + "mod_authn_pam" + "mod_authn_sasl" + "mod_cml" + "mod_deflate" + "mod_evasive" + "mod_extforward" + "mod_flv_streaming" + "mod_geoip" + "mod_magnet" + "mod_mysql_vhost" + "mod_openssl" # since v1.4.46 + "mod_scgi" + "mod_setenv" + "mod_trigger_b4_dl" + "mod_uploadprogress" + "mod_vhostdb" # since v1.4.46 + "mod_webdav" + "mod_wstunnel" # since v1.4.46 + ]; + + maybeModuleString = moduleName: + if elem moduleName cfg.enableModules then ''"${moduleName}"'' else ""; + + modulesIncludeString = concatStringsSep ",\n" + (filter (x: x != "") (map maybeModuleString allKnownModules)); + + configFile = if cfg.configText != "" then + pkgs.writeText "lighttpd.conf" '' + ${cfg.configText} + '' + else + pkgs.writeText "lighttpd.conf" '' + server.document-root = "${cfg.document-root}" + server.port = ${toString cfg.port} + server.username = "lighttpd" + server.groupname = "lighttpd" + + # As for why all modules are loaded here, instead of having small + # server.modules += () entries in each sub-service extraConfig snippet, + # read this: + # + # http://redmine.lighttpd.net/projects/1/wiki/Server_modulesDetails + # http://redmine.lighttpd.net/issues/2337 + # + # Basically, lighttpd doesn't want to load (or even silently ignore) a + # module for a second time, and there is no way to check if a module has + # been loaded already. So if two services were to put the same module in + # server.modules += (), that would break the lighttpd configuration. + server.modules = ( + ${modulesIncludeString} + ) + + # Logging (logs end up in systemd journal) + accesslog.use-syslog = "enable" + server.errorlog-use-syslog = "enable" + + ${lib.optionalString cfg.enableUpstreamMimeTypes '' + include "${pkgs.lighttpd}/share/lighttpd/doc/config/conf.d/mime.conf" + ''} + + static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" ) + index-file.names = ( "index.html" ) + + ${if cfg.mod_userdir then '' + userdir.path = "public_html" + '' else ""} + + ${if cfg.mod_status then '' + status.status-url = "/server-status" + status.statistics-url = "/server-statistics" + status.config-url = "/server-config" + '' else ""} + + ${cfg.extraConfig} + ''; + +in + +{ + + options = { + + services.lighttpd = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enable the lighttpd web server. + ''; + }; + + package = mkOption { + default = pkgs.lighttpd; + defaultText = "pkgs.lighttpd"; + type = types.package; + description = '' + lighttpd package to use. + ''; + }; + + port = mkOption { + default = 80; + type = types.port; + description = '' + TCP port number for lighttpd to bind to. + ''; + }; + + document-root = mkOption { + default = "/srv/www"; + type = types.path; + description = '' + Document-root of the web server. Must be readable by the "lighttpd" user. + ''; + }; + + mod_userdir = mkOption { + default = false; + type = types.bool; + description = '' + If true, requests in the form /~user/page.html are rewritten to take + the file public_html/page.html from the home directory of the user. + ''; + }; + + enableModules = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "mod_cgi" "mod_status" ]; + description = '' + List of lighttpd modules to enable. Sub-services take care of + enabling modules as needed, so this option is mainly for when you + want to add custom stuff to + <option>services.lighttpd.extraConfig</option> that depends on a + certain module. + ''; + }; + + enableUpstreamMimeTypes = mkOption { + type = types.bool; + default = true; + description = '' + Whether to include the list of mime types bundled with lighttpd + (upstream). If you disable this, no mime types will be added by + NixOS and you will have to add your own mime types in + <option>services.lighttpd.extraConfig</option>. + ''; + }; + + mod_status = mkOption { + default = false; + type = types.bool; + description = '' + Show server status overview at /server-status, statistics at + /server-statistics and list of loaded modules at /server-config. + ''; + }; + + configText = mkOption { + default = ""; + type = types.lines; + example = "...verbatim config file contents..."; + description = '' + Overridable config file contents to use for lighttpd. By default, use + the contents automatically generated by NixOS. + ''; + }; + + extraConfig = mkOption { + default = ""; + type = types.lines; + description = '' + These configuration lines will be appended to the generated lighttpd + config file. Note that this mechanism does not work when the manual + <option>configText</option> option is used. + ''; + }; + + }; + + }; + + config = mkIf cfg.enable { + + assertions = [ + { assertion = all (x: elem x allKnownModules) cfg.enableModules; + message = '' + One (or more) modules in services.lighttpd.enableModules are + unrecognized. + + Known modules: ${toString allKnownModules} + + services.lighttpd.enableModules: ${toString cfg.enableModules} + ''; + } + ]; + + services.lighttpd.enableModules = mkMerge + [ (mkIf cfg.mod_status [ "mod_status" ]) + (mkIf cfg.mod_userdir [ "mod_userdir" ]) + # always load mod_accesslog so that we can log to the journal + [ "mod_accesslog" ] + ]; + + systemd.services.lighttpd = { + description = "Lighttpd Web Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.ExecStart = "${cfg.package}/sbin/lighttpd -D -f ${configFile}"; + # SIGINT => graceful shutdown + serviceConfig.KillSignal = "SIGINT"; + }; + + users.users.lighttpd = { + group = "lighttpd"; + description = "lighttpd web server privilege separation user"; + uid = config.ids.uids.lighttpd; + }; + + users.groups.lighttpd.gid = config.ids.gids.lighttpd; + }; +} diff --git a/nixos/modules/services/web-servers/lighttpd/gitweb.nix b/nixos/modules/services/web-servers/lighttpd/gitweb.nix new file mode 100644 index 00000000000..c494d6966a7 --- /dev/null +++ b/nixos/modules/services/web-servers/lighttpd/gitweb.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.gitweb; + package = pkgs.gitweb.override (optionalAttrs cfg.gitwebTheme { + gitwebTheme = true; + }); + +in +{ + + options.services.lighttpd.gitweb = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + If true, enable gitweb in lighttpd. Access it at http://yourserver/gitweb + ''; + }; + + }; + + config = mkIf config.services.lighttpd.gitweb.enable { + + # declare module dependencies + services.lighttpd.enableModules = [ "mod_cgi" "mod_redirect" "mod_alias" "mod_setenv" ]; + + services.lighttpd.extraConfig = '' + $HTTP["url"] =~ "^/gitweb" { + cgi.assign = ( + ".cgi" => "${pkgs.perl}/bin/perl" + ) + url.redirect = ( + "^/gitweb$" => "/gitweb/" + ) + alias.url = ( + "/gitweb/static/" => "${package}/static/", + "/gitweb/" => "${package}/gitweb.cgi" + ) + setenv.add-environment = ( + "GITWEB_CONFIG" => "${cfg.gitwebConfigFile}", + "HOME" => "${cfg.projectroot}" + ) + } + ''; + + }; + +} diff --git a/nixos/modules/services/web-servers/mighttpd2.nix b/nixos/modules/services/web-servers/mighttpd2.nix new file mode 100644 index 00000000000..f9b1a8b6ccc --- /dev/null +++ b/nixos/modules/services/web-servers/mighttpd2.nix @@ -0,0 +1,132 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.mighttpd2; + configFile = pkgs.writeText "mighty-config" cfg.config; + routingFile = pkgs.writeText "mighty-routing" cfg.routing; +in { + options.services.mighttpd2 = { + enable = mkEnableOption "Mighttpd2 web server"; + + config = mkOption { + default = ""; + example = '' + # Example configuration for Mighttpd 2 + Port: 80 + # IP address or "*" + Host: * + Debug_Mode: Yes # Yes or No + # If available, "nobody" is much more secure for User:. + User: root + # If available, "nobody" is much more secure for Group:. + Group: root + Pid_File: /run/mighty.pid + Logging: Yes # Yes or No + Log_File: /var/log/mighty # The directory must be writable by User: + Log_File_Size: 16777216 # bytes + Log_Backup_Number: 10 + Index_File: index.html + Index_Cgi: index.cgi + Status_File_Dir: /usr/local/share/mighty/status + Connection_Timeout: 30 # seconds + Fd_Cache_Duration: 10 # seconds + # Server_Name: Mighttpd/3.x.y + Tls_Port: 443 + Tls_Cert_File: cert.pem # should change this with an absolute path + # should change this with comma-separated absolute paths + Tls_Chain_Files: chain.pem + # Currently, Tls_Key_File must not be encrypted. + Tls_Key_File: privkey.pem # should change this with an absolute path + Service: 0 # 0 is HTTP only, 1 is HTTPS only, 2 is both + ''; + type = types.lines; + description = '' + Verbatim config file to use + (see http://www.mew.org/~kazu/proj/mighttpd/en/config.html) + ''; + }; + + routing = mkOption { + default = ""; + example = '' + # Example routing for Mighttpd 2 + + # Domain lists + [localhost www.example.com] + + # Entries are looked up in the specified order + # All paths must end with "/" + + # A path to CGI scripts should be specified with "=>" + /~alice/cgi-bin/ => /home/alice/public_html/cgi-bin/ + + # A path to static files should be specified with "->" + /~alice/ -> /home/alice/public_html/ + /cgi-bin/ => /export/cgi-bin/ + + # Reverse proxy rules should be specified with ">>" + # /path >> host:port/path2 + # Either "host" or ":port" can be committed, but not both. + /app/cal/ >> example.net/calendar/ + # Yesod app in the same server + /app/wiki/ >> 127.0.0.1:3000/ + + / -> /export/www/ + ''; + type = types.lines; + description = '' + Verbatim routing file to use + (see http://www.mew.org/~kazu/proj/mighttpd/en/config.html) + ''; + }; + + cores = mkOption { + default = null; + type = types.nullOr types.int; + description = '' + How many cores to use. + If null it will be determined automatically + ''; + }; + + }; + + config = mkIf cfg.enable { + assertions = + [ { assertion = cfg.routing != ""; + message = "You need at least one rule in mighttpd2.routing"; + } + ]; + systemd.services.mighttpd2 = { + description = "Mighttpd2 web server"; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = '' + ${pkgs.haskellPackages.mighttpd2}/bin/mighty \ + ${configFile} \ + ${routingFile} \ + +RTS -N${optionalString (cfg.cores != null) "${cfg.cores}"} + ''; + Type = "simple"; + User = "mighttpd2"; + Group = "mighttpd2"; + Restart = "on-failure"; + AmbientCapabilities = "cap_net_bind_service"; + CapabilityBoundingSet = "cap_net_bind_service"; + }; + }; + + users.users.mighttpd2 = { + group = "mighttpd2"; + uid = config.ids.uids.mighttpd2; + isSystemUser = true; + }; + + users.groups.mighttpd2.gid = config.ids.gids.mighttpd2; + }; + + meta.maintainers = with lib.maintainers; [ fgaz ]; +} diff --git a/nixos/modules/services/web-servers/minio.nix b/nixos/modules/services/web-servers/minio.nix new file mode 100644 index 00000000000..c345e3f2467 --- /dev/null +++ b/nixos/modules/services/web-servers/minio.nix @@ -0,0 +1,130 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.minio; + + legacyCredentials = cfg: pkgs.writeText "minio-legacy-credentials" '' + MINIO_ROOT_USER=${cfg.accessKey} + MINIO_ROOT_PASSWORD=${cfg.secretKey} + ''; +in +{ + meta.maintainers = [ maintainers.bachp ]; + + options.services.minio = { + enable = mkEnableOption "Minio Object Storage"; + + listenAddress = mkOption { + default = ":9000"; + type = types.str; + description = "IP address and port of the server."; + }; + + consoleAddress = mkOption { + default = ":9001"; + type = types.str; + description = "IP address and port of the web UI (console)."; + }; + + dataDir = mkOption { + default = [ "/var/lib/minio/data" ]; + type = types.listOf types.path; + description = "The list of data directories for storing the objects. Use one path for regular operation and the minimum of 4 endpoints for Erasure Code mode."; + }; + + configDir = mkOption { + default = "/var/lib/minio/config"; + type = types.path; + description = "The config directory, for the access keys and other settings."; + }; + + accessKey = mkOption { + default = ""; + type = types.str; + description = '' + Access key of 5 to 20 characters in length that clients use to access the server. + This overrides the access key that is generated by minio on first startup and stored inside the + <literal>configDir</literal> directory. + ''; + }; + + secretKey = mkOption { + default = ""; + type = types.str; + description = '' + Specify the Secret key of 8 to 40 characters in length that clients use to access the server. + This overrides the secret key that is generated by minio on first startup and stored inside the + <literal>configDir</literal> directory. + ''; + }; + + rootCredentialsFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + File containing the MINIO_ROOT_USER, default is "minioadmin", and + MINIO_ROOT_PASSWORD (length >= 8), default is "minioadmin"; in the format of + an EnvironmentFile=, as described by systemd.exec(5). + ''; + example = "/etc/nixos/minio-root-credentials"; + }; + + region = mkOption { + default = "us-east-1"; + type = types.str; + description = '' + The physical location of the server. By default it is set to us-east-1, which is same as AWS S3's and Minio's default region. + ''; + }; + + browser = mkOption { + default = true; + type = types.bool; + description = "Enable or disable access to web UI."; + }; + + package = mkOption { + default = pkgs.minio; + defaultText = literalExpression "pkgs.minio"; + type = types.package; + description = "Minio package to use."; + }; + }; + + config = mkIf cfg.enable { + warnings = optional ((cfg.accessKey != "") || (cfg.secretKey != "")) "services.minio.`accessKey` and services.minio.`secretKey` are deprecated, please use services.minio.`rootCredentialsFile` instead."; + + systemd.tmpfiles.rules = [ + "d '${cfg.configDir}' - minio minio - -" + ] ++ (map (x: "d '" + x + "' - minio minio - - ") cfg.dataDir); + + systemd.services.minio = { + description = "Minio Object Storage"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --console-address ${cfg.consoleAddress} --config-dir=${cfg.configDir} ${toString cfg.dataDir}"; + Type = "simple"; + User = "minio"; + Group = "minio"; + LimitNOFILE = 65536; + EnvironmentFile = if (cfg.rootCredentialsFile != null) then cfg.rootCredentialsFile + else if ((cfg.accessKey != "") || (cfg.secretKey != "")) then (legacyCredentials cfg) + else null; + }; + environment = { + MINIO_REGION = "${cfg.region}"; + MINIO_BROWSER = "${if cfg.browser then "on" else "off"}"; + }; + }; + + users.users.minio = { + group = "minio"; + uid = config.ids.uids.minio; + }; + + users.groups.minio.gid = config.ids.uids.minio; + }; +} diff --git a/nixos/modules/services/web-servers/molly-brown.nix b/nixos/modules/services/web-servers/molly-brown.nix new file mode 100644 index 00000000000..0bd8b3316cb --- /dev/null +++ b/nixos/modules/services/web-servers/molly-brown.nix @@ -0,0 +1,101 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.molly-brown; + settingsFormat = pkgs.formats.toml { }; + configFile = settingsFormat.generate "molly-brown.toml" cfg.settings; +in { + + options.services.molly-brown = { + + enable = mkEnableOption "Molly-Brown Gemini server"; + + port = mkOption { + default = 1965; + type = types.port; + description = '' + TCP port for molly-brown to bind to. + ''; + }; + + hostName = mkOption { + type = types.str; + default = config.networking.hostName; + defaultText = literalExpression "config.networking.hostName"; + description = '' + The hostname to respond to requests for. Requests for URLs with + other hosts will result in a status 53 (PROXY REQUEST REFUSED) + response. + ''; + }; + + certPath = mkOption { + type = types.path; + example = "/var/lib/acme/example.com/cert.pem"; + description = '' + Path to TLS certificate. An ACME certificate and key may be + shared with an HTTP server, but only if molly-brown has + permissions allowing it to read such keys. + + As an example: + <programlisting> + systemd.services.molly-brown.serviceConfig.SupplementaryGroups = + [ config.security.acme.certs."example.com".group ]; + </programlisting> + ''; + }; + + keyPath = mkOption { + type = types.path; + example = "/var/lib/acme/example.com/key.pem"; + description = "Path to TLS key. See <option>CertPath</option>."; + }; + + docBase = mkOption { + type = types.path; + example = "/var/lib/molly-brown"; + description = "Base directory for Gemini content."; + }; + + settings = mkOption { + inherit (settingsFormat) type; + default = { }; + description = '' + molly-brown configuration. Refer to + <link xlink:href="https://tildegit.org/solderpunk/molly-brown/src/branch/master/example.conf"/> + for details on supported values. + ''; + }; + + }; + + config = mkIf cfg.enable { + + services.molly-brown.settings = let logDir = "/var/log/molly-brown"; + in { + Port = cfg.port; + Hostname = cfg.hostName; + CertPath = cfg.certPath; + KeyPath = cfg.keyPath; + DocBase = cfg.docBase; + AccessLog = "${logDir}/access.log"; + ErrorLog = "${logDir}/error.log"; + }; + + systemd.services.molly-brown = { + description = "Molly Brown gemini server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + DynamicUser = true; + LogsDirectory = "molly-brown"; + ExecStart = "${pkgs.molly-brown}/bin/molly-brown -c ${configFile}"; + Restart = "always"; + }; + }; + + }; + +} diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix new file mode 100644 index 00000000000..e046c28dd6b --- /dev/null +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -0,0 +1,1005 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.nginx; + certs = config.security.acme.certs; + vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts; + acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null) vhostsConfigs; + dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts); + virtualHosts = mapAttrs (vhostName: vhostConfig: + let + serverName = if vhostConfig.serverName != null + then vhostConfig.serverName + else vhostName; + certName = if vhostConfig.useACMEHost != null + then vhostConfig.useACMEHost + else serverName; + in + vhostConfig // { + inherit serverName certName; + } // (optionalAttrs (vhostConfig.enableACME || vhostConfig.useACMEHost != null) { + sslCertificate = "${certs.${certName}.directory}/fullchain.pem"; + sslCertificateKey = "${certs.${certName}.directory}/key.pem"; + sslTrustedCertificate = if vhostConfig.sslTrustedCertificate != null + then vhostConfig.sslTrustedCertificate + else "${certs.${certName}.directory}/chain.pem"; + }) + ) cfg.virtualHosts; + enableIPv6 = config.networking.enableIPv6; + + defaultFastcgiParams = { + SCRIPT_FILENAME = "$document_root$fastcgi_script_name"; + QUERY_STRING = "$query_string"; + REQUEST_METHOD = "$request_method"; + CONTENT_TYPE = "$content_type"; + CONTENT_LENGTH = "$content_length"; + + SCRIPT_NAME = "$fastcgi_script_name"; + REQUEST_URI = "$request_uri"; + DOCUMENT_URI = "$document_uri"; + DOCUMENT_ROOT = "$document_root"; + SERVER_PROTOCOL = "$server_protocol"; + REQUEST_SCHEME = "$scheme"; + HTTPS = "$https if_not_empty"; + + GATEWAY_INTERFACE = "CGI/1.1"; + SERVER_SOFTWARE = "nginx/$nginx_version"; + + REMOTE_ADDR = "$remote_addr"; + REMOTE_PORT = "$remote_port"; + SERVER_ADDR = "$server_addr"; + SERVER_PORT = "$server_port"; + SERVER_NAME = "$server_name"; + + REDIRECT_STATUS = "200"; + }; + + recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" '' + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + ''; + + upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: '' + upstream ${name} { + ${toString (flip mapAttrsToList upstream.servers (name: server: '' + server ${name} ${optionalString server.backup "backup"}; + ''))} + ${upstream.extraConfig} + } + '')); + + commonHttpConfig = '' + # The mime type definitions included with nginx are very incomplete, so + # we use a list of mime types from the mailcap package, which is also + # used by most other Linux distributions by default. + include ${pkgs.mailcap}/etc/nginx/mime.types; + # When recommendedOptimisation is disabled nginx fails to start because the mailmap mime.types database + # contains 1026 enries and the default is only 1024. Setting to a higher number to remove the need to + # overwrite it because nginx does not allow duplicated settings. + types_hash_max_size 4096; + + include ${cfg.package}/conf/fastcgi.conf; + include ${cfg.package}/conf/uwsgi_params; + + default_type application/octet-stream; + ''; + + configFile = pkgs.writers.writeNginxConfig "nginx.conf" '' + pid /run/nginx/nginx.pid; + error_log ${cfg.logError}; + daemon off; + + ${cfg.config} + + ${optionalString (cfg.eventsConfig != "" || cfg.config == "") '' + events { + ${cfg.eventsConfig} + } + ''} + + ${optionalString (cfg.httpConfig == "" && cfg.config == "") '' + http { + ${commonHttpConfig} + + ${optionalString (cfg.resolver.addresses != []) '' + resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"} ${optionalString (!cfg.resolver.ipv6) "ipv6=off"}; + ''} + ${upstreamConfig} + + ${optionalString (cfg.recommendedOptimisation) '' + # optimisation + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + ''} + + ssl_protocols ${cfg.sslProtocols}; + ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"} + ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"} + + ${optionalString (cfg.recommendedTlsSettings) '' + # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate + + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + # Breaks forward secrecy: https://github.com/mozilla/server-side-tls/issues/135 + ssl_session_tickets off; + # We don't enable insecure ciphers by default, so this allows + # clients to pick the most performant, per https://github.com/mozilla/server-side-tls/issues/260 + ssl_prefer_server_ciphers off; + + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + ''} + + ${optionalString (cfg.recommendedGzipSettings) '' + gzip on; + gzip_proxied any; + gzip_comp_level 5; + gzip_types + application/atom+xml + application/javascript + application/json + application/xml + application/xml+rss + image/svg+xml + text/css + text/javascript + text/plain + text/xml; + gzip_vary on; + ''} + + ${optionalString (cfg.recommendedProxySettings) '' + proxy_redirect off; + proxy_connect_timeout ${cfg.proxyTimeout}; + proxy_send_timeout ${cfg.proxyTimeout}; + proxy_read_timeout ${cfg.proxyTimeout}; + proxy_http_version 1.1; + include ${recommendedProxyConfig}; + ''} + + ${optionalString (cfg.mapHashBucketSize != null) '' + map_hash_bucket_size ${toString cfg.mapHashBucketSize}; + ''} + + ${optionalString (cfg.mapHashMaxSize != null) '' + map_hash_max_size ${toString cfg.mapHashMaxSize}; + ''} + + ${optionalString (cfg.serverNamesHashBucketSize != null) '' + server_names_hash_bucket_size ${toString cfg.serverNamesHashBucketSize}; + ''} + + ${optionalString (cfg.serverNamesHashMaxSize != null) '' + server_names_hash_max_size ${toString cfg.serverNamesHashMaxSize}; + ''} + + # $connection_upgrade is used for websocket proxying + map $http_upgrade $connection_upgrade { + default upgrade; + ''' close; + } + client_max_body_size ${cfg.clientMaxBodySize}; + + server_tokens ${if cfg.serverTokens then "on" else "off"}; + + ${cfg.commonHttpConfig} + + ${vhosts} + + ${optionalString cfg.statusPage '' + server { + listen 80; + ${optionalString enableIPv6 "listen [::]:80;" } + + server_name localhost; + + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + ${optionalString enableIPv6 "allow ::1;"} + deny all; + } + } + ''} + + ${cfg.appendHttpConfig} + }''} + + ${optionalString (cfg.httpConfig != "") '' + http { + ${commonHttpConfig} + ${cfg.httpConfig} + }''} + + ${optionalString (cfg.streamConfig != "") '' + stream { + ${cfg.streamConfig} + } + ''} + + ${cfg.appendConfig} + ''; + + configPath = if cfg.enableReload + then "/etc/nginx/nginx.conf" + else configFile; + + execCommand = "${cfg.package}/bin/nginx -c '${configPath}'"; + + vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost: + let + onlySSL = vhost.onlySSL || vhost.enableSSL; + hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL; + + defaultListen = + if vhost.listen != [] then vhost.listen + else + let addrs = if vhost.listenAddresses != [] then vhost.listenAddresses else cfg.defaultListenAddresses; + in optionals (hasSSL || vhost.rejectSSL) (map (addr: { inherit addr; port = 443; ssl = true; }) addrs) + ++ optionals (!onlySSL) (map (addr: { inherit addr; port = 80; ssl = false; }) addrs); + + hostListen = + if vhost.forceSSL + then filter (x: x.ssl) defaultListen + else defaultListen; + + listenString = { addr, port, ssl, extraParameters ? [], ... }: + "listen ${addr}:${toString port} " + + optionalString ssl "ssl " + + optionalString (ssl && vhost.http2) "http2 " + + optionalString vhost.default "default_server " + + optionalString (extraParameters != []) (concatStringsSep " " extraParameters) + + ";" + + (if ssl && vhost.http3 then '' + # UDP listener for **QUIC+HTTP/3 + listen ${addr}:${toString port} http3 reuseport; + # Advertise that HTTP/3 is available + add_header Alt-Svc 'h3=":443"'; + # Sent when QUIC was used + add_header QUIC-Status $quic; + '' else ""); + + redirectListen = filter (x: !x.ssl) defaultListen; + + acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) '' + location /.well-known/acme-challenge { + ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"} + ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"} + auth_basic off; + } + ${optionalString (vhost.acmeFallbackHost != null) '' + location @acme-fallback { + auth_basic off; + proxy_pass http://${vhost.acmeFallbackHost}; + } + ''} + ''; + + in '' + ${optionalString vhost.forceSSL '' + server { + ${concatMapStringsSep "\n" listenString redirectListen} + + server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; + ${acmeLocation} + location / { + return 301 https://$host$request_uri; + } + } + ''} + + server { + ${concatMapStringsSep "\n" listenString hostListen} + server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; + ${acmeLocation} + ${optionalString (vhost.root != null) "root ${vhost.root};"} + ${optionalString (vhost.globalRedirect != null) '' + return 301 http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri; + ''} + ${optionalString hasSSL '' + ssl_certificate ${vhost.sslCertificate}; + ssl_certificate_key ${vhost.sslCertificateKey}; + ''} + ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) '' + ssl_trusted_certificate ${vhost.sslTrustedCertificate}; + ''} + ${optionalString vhost.rejectSSL '' + ssl_reject_handshake on; + ''} + ${optionalString (hasSSL && vhost.kTLS) '' + ssl_conf_command Options KTLS; + ''} + + ${mkBasicAuth vhostName vhost} + + ${mkLocations vhost.locations} + + ${vhost.extraConfig} + } + '' + ) virtualHosts); + mkLocations = locations: concatStringsSep "\n" (map (config: '' + location ${config.location} { + ${optionalString (config.proxyPass != null && !cfg.proxyResolveWhileRunning) + "proxy_pass ${config.proxyPass};" + } + ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) '' + set $nix_proxy_target "${config.proxyPass}"; + proxy_pass $nix_proxy_target; + ''} + ${optionalString config.proxyWebsockets '' + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + ''} + ${concatStringsSep "\n" + (mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'') + (optionalAttrs (config.fastcgiParams != {}) + (defaultFastcgiParams // config.fastcgiParams)))} + ${optionalString (config.index != null) "index ${config.index};"} + ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"} + ${optionalString (config.root != null) "root ${config.root};"} + ${optionalString (config.alias != null) "alias ${config.alias};"} + ${optionalString (config.return != null) "return ${config.return};"} + ${config.extraConfig} + ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"} + ${mkBasicAuth "sublocation" config} + } + '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))); + + mkBasicAuth = name: zone: optionalString (zone.basicAuthFile != null || zone.basicAuth != {}) (let + auth_file = if zone.basicAuthFile != null + then zone.basicAuthFile + else mkHtpasswd name zone.basicAuth; + in '' + auth_basic secured; + auth_basic_user_file ${auth_file}; + ''); + mkHtpasswd = name: authDef: pkgs.writeText "${name}.htpasswd" ( + concatStringsSep "\n" (mapAttrsToList (user: password: '' + ${user}:{PLAIN}${password} + '') authDef) + ); + + mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; +in + +{ + options = { + services.nginx = { + enable = mkEnableOption "Nginx Web Server"; + + statusPage = mkOption { + default = false; + type = types.bool; + description = " + Enable status page reachable from localhost on http://127.0.0.1/nginx_status. + "; + }; + + recommendedTlsSettings = mkOption { + default = false; + type = types.bool; + description = " + Enable recommended TLS settings. + "; + }; + + recommendedOptimisation = mkOption { + default = false; + type = types.bool; + description = " + Enable recommended optimisation settings. + "; + }; + + recommendedGzipSettings = mkOption { + default = false; + type = types.bool; + description = " + Enable recommended gzip settings. + "; + }; + + recommendedProxySettings = mkOption { + default = false; + type = types.bool; + description = " + Enable recommended proxy settings. + "; + }; + + proxyTimeout = mkOption { + type = types.str; + default = "60s"; + example = "20s"; + description = " + Change the proxy related timeouts in recommendedProxySettings. + "; + }; + + defaultListenAddresses = mkOption { + type = types.listOf types.str; + default = [ "0.0.0.0" ] ++ optional enableIPv6 "[::0]"; + defaultText = literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"''; + example = literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]''; + description = " + If vhosts do not specify listenAddresses, use these addresses by default. + "; + }; + + package = mkOption { + default = pkgs.nginxStable; + defaultText = literalExpression "pkgs.nginxStable"; + type = types.package; + apply = p: p.override { + modules = p.modules ++ cfg.additionalModules; + }; + description = " + Nginx package to use. This defaults to the stable version. Note + that the nginx team recommends to use the mainline version which + available in nixpkgs as <literal>nginxMainline</literal>. + "; + }; + + additionalModules = mkOption { + default = []; + type = types.listOf (types.attrsOf types.anything); + example = literalExpression "[ pkgs.nginxModules.brotli ]"; + description = '' + Additional <link xlink:href="https://www.nginx.com/resources/wiki/modules/">third-party nginx modules</link> + to install. Packaged modules are available in + <literal>pkgs.nginxModules</literal>. + ''; + }; + + logError = mkOption { + default = "stderr"; + type = types.str; + description = " + Configures logging. + The first parameter defines a file that will store the log. The + special value stderr selects the standard error file. Logging to + syslog can be configured by specifying the “syslog:” prefix. + The second parameter determines the level of logging, and can be + one of the following: debug, info, notice, warn, error, crit, + alert, or emerg. Log levels above are listed in the order of + increasing severity. Setting a certain log level will cause all + messages of the specified and more severe log levels to be logged. + If this parameter is omitted then error is used. + "; + }; + + preStart = mkOption { + type = types.lines; + default = ""; + description = " + Shell commands executed before the service's nginx is started. + "; + }; + + config = mkOption { + type = types.str; + default = ""; + description = '' + Verbatim <filename>nginx.conf</filename> configuration. + This is mutually exclusive to any other config option for + <filename>nginx.conf</filename> except for + <itemizedlist> + <listitem><para><xref linkend="opt-services.nginx.appendConfig" /> + </para></listitem> + <listitem><para><xref linkend="opt-services.nginx.httpConfig" /> + </para></listitem> + <listitem><para><xref linkend="opt-services.nginx.logError" /> + </para></listitem> + </itemizedlist> + + If additional verbatim config in addition to other options is needed, + <xref linkend="opt-services.nginx.appendConfig" /> should be used instead. + ''; + }; + + appendConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Configuration lines appended to the generated Nginx + configuration file. Commonly used by different modules + providing http snippets. <option>appendConfig</option> + can be specified more than once and it's value will be + concatenated (contrary to <option>config</option> which + can be set only once). + ''; + }; + + commonHttpConfig = mkOption { + type = types.lines; + default = ""; + example = '' + resolver 127.0.0.1 valid=5s; + + log_format myformat '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent"'; + ''; + description = '' + With nginx you must provide common http context definitions before + they are used, e.g. log_format, resolver, etc. inside of server + or location contexts. Use this attribute to set these definitions + at the appropriate location. + ''; + }; + + httpConfig = mkOption { + type = types.lines; + default = ""; + description = " + Configuration lines to be set inside the http block. + This is mutually exclusive with the structured configuration + via virtualHosts and the recommendedXyzSettings configuration + options. See appendHttpConfig for appending to the generated http block. + "; + }; + + streamConfig = mkOption { + type = types.lines; + default = ""; + example = '' + server { + listen 127.0.0.1:53 udp reuseport; + proxy_timeout 20s; + proxy_pass 192.168.0.1:53535; + } + ''; + description = " + Configuration lines to be set inside the stream block. + "; + }; + + eventsConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Configuration lines to be set inside the events block. + ''; + }; + + appendHttpConfig = mkOption { + type = types.lines; + default = ""; + description = " + Configuration lines to be appended to the generated http block. + This is mutually exclusive with using config and httpConfig for + specifying the whole http block verbatim. + "; + }; + + enableReload = mkOption { + default = false; + type = types.bool; + description = '' + Reload nginx when configuration file changes (instead of restart). + The configuration file is exposed at <filename>/etc/nginx/nginx.conf</filename>. + See also <literal>systemd.services.*.restartIfChanged</literal>. + ''; + }; + + user = mkOption { + type = types.str; + default = "nginx"; + description = "User account under which nginx runs."; + }; + + group = mkOption { + type = types.str; + default = "nginx"; + description = "Group account under which nginx runs."; + }; + + serverTokens = mkOption { + type = types.bool; + default = false; + description = "Show nginx version in headers and error pages."; + }; + + clientMaxBodySize = mkOption { + type = types.str; + default = "10m"; + description = "Set nginx global client_max_body_size."; + }; + + sslCiphers = mkOption { + type = types.nullOr types.str; + # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate + default = "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"; + description = "Ciphers to choose from when negotiating TLS handshakes."; + }; + + sslProtocols = mkOption { + type = types.str; + default = "TLSv1.2 TLSv1.3"; + example = "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"; + description = "Allowed TLS protocol versions."; + }; + + sslDhparam = mkOption { + type = types.nullOr types.path; + default = null; + example = "/path/to/dhparams.pem"; + description = "Path to DH parameters file."; + }; + + proxyResolveWhileRunning = mkOption { + type = types.bool; + default = false; + description = '' + Resolves domains of proxyPass targets at runtime + and not only at start, you have to set + services.nginx.resolver, too. + ''; + }; + + mapHashBucketSize = mkOption { + type = types.nullOr (types.enum [ 32 64 128 ]); + default = null; + description = '' + Sets the bucket size for the map variables hash tables. Default + value depends on the processor’s cache line size. + ''; + }; + + mapHashMaxSize = mkOption { + type = types.nullOr types.ints.positive; + default = null; + description = '' + Sets the maximum size of the map variables hash tables. + ''; + }; + + serverNamesHashBucketSize = mkOption { + type = types.nullOr types.ints.positive; + default = null; + description = '' + Sets the bucket size for the server names hash tables. Default + value depends on the processor’s cache line size. + ''; + }; + + serverNamesHashMaxSize = mkOption { + type = types.nullOr types.ints.positive; + default = null; + description = '' + Sets the maximum size of the server names hash tables. + ''; + }; + + resolver = mkOption { + type = types.submodule { + options = { + addresses = mkOption { + type = types.listOf types.str; + default = []; + example = literalExpression ''[ "[::1]" "127.0.0.1:5353" ]''; + description = "List of resolvers to use"; + }; + valid = mkOption { + type = types.str; + default = ""; + example = "30s"; + description = '' + By default, nginx caches answers using the TTL value of a response. + An optional valid parameter allows overriding it + ''; + }; + ipv6 = mkOption { + type = types.bool; + default = true; + description = '' + By default, nginx will look up both IPv4 and IPv6 addresses while resolving. + If looking up of IPv6 addresses is not desired, the ipv6=off parameter can be + specified. + ''; + }; + }; + }; + description = '' + Configures name servers used to resolve names of upstream servers into addresses + ''; + default = {}; + }; + + upstreams = mkOption { + type = types.attrsOf (types.submodule { + options = { + servers = mkOption { + type = types.attrsOf (types.submodule { + options = { + backup = mkOption { + type = types.bool; + default = false; + description = '' + Marks the server as a backup server. It will be passed + requests when the primary servers are unavailable. + ''; + }; + }; + }); + description = '' + Defines the address and other parameters of the upstream servers. + ''; + default = {}; + example = { "127.0.0.1:8000" = {}; }; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + These lines go to the end of the upstream verbatim. + ''; + }; + }; + }); + description = '' + Defines a group of servers to use as proxy target. + ''; + default = {}; + example = literalExpression '' + "backend_server" = { + servers = { "127.0.0.1:8000" = {}; }; + extraConfig = '''' + keepalive 16; + ''''; + }; + ''; + }; + + virtualHosts = mkOption { + type = types.attrsOf (types.submodule (import ./vhost-options.nix { + inherit config lib; + })); + default = { + localhost = {}; + }; + example = literalExpression '' + { + "hydra.example.com" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://localhost:3000"; + }; + }; + }; + ''; + description = "Declarative vhost config"; + }; + }; + }; + + imports = [ + (mkRemovedOptionModule [ "services" "nginx" "stateDir" ] '' + The Nginx log directory has been moved to /var/log/nginx, the cache directory + to /var/cache/nginx. The option services.nginx.stateDir has been removed. + '') + ]; + + config = mkIf cfg.enable { + # TODO: test user supplied config file pases syntax test + + warnings = + let + deprecatedSSL = name: config: optional config.enableSSL + '' + config.services.nginx.virtualHosts.<name>.enableSSL is deprecated, + use config.services.nginx.virtualHosts.<name>.onlySSL instead. + ''; + + in flatten (mapAttrsToList deprecatedSSL virtualHosts); + + assertions = + let + hostOrAliasIsNull = l: l.root == null || l.alias == null; + in [ + { + assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts); + message = "Only one of nginx root or alias can be specified on a location."; + } + + { + assertion = all (host: with host; + count id [ addSSL (onlySSL || enableSSL) forceSSL rejectSSL ] <= 1 + ) (attrValues virtualHosts); + message = '' + Options services.nginx.service.virtualHosts.<name>.addSSL, + services.nginx.virtualHosts.<name>.onlySSL, + services.nginx.virtualHosts.<name>.forceSSL and + services.nginx.virtualHosts.<name>.rejectSSL are mutually exclusive. + ''; + } + + { + assertion = any (host: host.rejectSSL) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.19.4"; + message = '' + services.nginx.virtualHosts.<name>.rejectSSL requires nginx version + 1.19.4 or above; see the documentation for services.nginx.package. + ''; + } + + { + assertion = any (host: host.kTLS) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.21.4"; + message = '' + services.nginx.virtualHosts.<name>.kTLS requires nginx version + 1.21.4 or above; see the documentation for services.nginx.package. + ''; + } + + { + assertion = all (host: !(host.enableACME && host.useACMEHost != null)) (attrValues virtualHosts); + message = '' + Options services.nginx.service.virtualHosts.<name>.enableACME and + services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive. + ''; + } + ] ++ map (name: mkCertOwnershipAssertion { + inherit (cfg) group user; + cert = config.security.acme.certs.${name}; + groups = config.users.groups; + }) dependentCertNames; + + systemd.services.nginx = { + description = "Nginx Web Server"; + wantedBy = [ "multi-user.target" ]; + wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames); + after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames; + # Nginx needs to be started in order to be able to request certificates + # (it's hosting the acme challenge after all) + # This fixes https://github.com/NixOS/nixpkgs/issues/81842 + before = map (certName: "acme-${certName}.service") dependentCertNames; + stopIfChanged = false; + preStart = '' + ${cfg.preStart} + ${execCommand} -t + ''; + + startLimitIntervalSec = 60; + serviceConfig = { + ExecStart = execCommand; + ExecReload = [ + "${execCommand} -t" + "${pkgs.coreutils}/bin/kill -HUP $MAINPID" + ]; + Restart = "always"; + RestartSec = "10s"; + # User and group + User = cfg.user; + Group = cfg.group; + # Runtime directory and mode + RuntimeDirectory = "nginx"; + RuntimeDirectoryMode = "0750"; + # Cache directory and mode + CacheDirectory = "nginx"; + CacheDirectoryMode = "0750"; + # Logs directory and mode + LogsDirectory = "nginx"; + LogsDirectoryMode = "0750"; + # Proc filesystem + ProcSubset = "pid"; + ProtectProc = "invisible"; + # New file permissions + UMask = "0027"; # 0640 / 0750 + # Capabilities + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ]; + # Security + NoNewPrivileges = true; + # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html) + ProtectSystem = "strict"; + ProtectHome = mkDefault true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = !((builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules) || (cfg.package == pkgs.openresty)); + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + PrivateMounts = true; + # System Call Filtering + SystemCallArchitectures = "native"; + SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" ] + ++ optionals ((cfg.package != pkgs.tengine) && (!lib.any (mod: (mod.disableIPC or false)) cfg.package.modules)) [ "~@ipc" ]; + }; + }; + + environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload { + source = configFile; + }; + + # This service waits for all certificates to be available + # before reloading nginx configuration. + # sslTargets are added to wantedBy + before + # which allows the acme-finished-$cert.target to signify the successful updating + # of certs end-to-end. + systemd.services.nginx-config-reload = let + sslServices = map (certName: "acme-${certName}.service") dependentCertNames; + sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames; + in mkIf (cfg.enableReload || sslServices != []) { + wants = optionals (cfg.enableReload) [ "nginx.service" ]; + wantedBy = sslServices ++ [ "multi-user.target" ]; + # Before the finished targets, after the renew services. + # This service might be needed for HTTP-01 challenges, but we only want to confirm + # certs are updated _after_ config has been reloaded. + before = sslTargets; + after = sslServices; + restartTriggers = optionals (cfg.enableReload) [ configFile ]; + # Block reloading if not all certs exist yet. + # Happens when config changes add new vhosts/certs. + unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames); + serviceConfig = { + Type = "oneshot"; + TimeoutSec = 60; + ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service"; + ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service"; + }; + }; + + security.acme.certs = let + acmePairs = map (vhostConfig: let + hasRoot = vhostConfig.acmeRoot != null; + in nameValuePair vhostConfig.serverName { + group = mkDefault cfg.group; + # if acmeRoot is null inherit config.security.acme + # Since config.security.acme.certs.<cert>.webroot's own default value + # should take precedence set priority higher than mkOptionDefault + webroot = mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot; + # Also nudge dnsProvider to null in case it is inherited + dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null; + extraDomainNames = vhostConfig.serverAliases; + # Filter for enableACME-only vhosts. Don't want to create dud certs + }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts); + in listToAttrs acmePairs; + + users.users = optionalAttrs (cfg.user == "nginx") { + nginx = { + group = cfg.group; + isSystemUser = true; + uid = config.ids.uids.nginx; + }; + }; + + users.groups = optionalAttrs (cfg.group == "nginx") { + nginx.gid = config.ids.gids.nginx; + }; + + services.logrotate.paths.nginx = mapAttrs (_: mkDefault) { + path = "/var/log/nginx/*.log"; + frequency = "weekly"; + keep = 26; + extraConfig = '' + compress + delaycompress + postrotate + [ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid` + endscript + ''; + }; + }; +} diff --git a/nixos/modules/services/web-servers/nginx/gitweb.nix b/nixos/modules/services/web-servers/nginx/gitweb.nix new file mode 100644 index 00000000000..db45577a46d --- /dev/null +++ b/nixos/modules/services/web-servers/nginx/gitweb.nix @@ -0,0 +1,94 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.nginx.gitweb; + gitwebConfig = config.services.gitweb; + package = pkgs.gitweb.override (optionalAttrs gitwebConfig.gitwebTheme { + gitwebTheme = true; + }); + +in +{ + + options.services.nginx.gitweb = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + If true, enable gitweb in nginx. + ''; + }; + + location = mkOption { + default = "/gitweb"; + type = types.str; + description = '' + Location to serve gitweb on. + ''; + }; + + user = mkOption { + default = "nginx"; + type = types.str; + description = '' + Existing user that the CGI process will belong to. (Default almost surely will do.) + ''; + }; + + group = mkOption { + default = "nginx"; + type = types.str; + description = '' + Group that the CGI process will belong to. (Set to <literal>config.services.gitolite.group</literal> if you are using gitolite.) + ''; + }; + + virtualHost = mkOption { + default = "_"; + type = types.str; + description = '' + VirtualHost to serve gitweb on. Default is catch-all. + ''; + }; + + }; + + config = mkIf cfg.enable { + + systemd.services.gitweb = { + description = "GitWeb service"; + script = "${package}/gitweb.cgi --fastcgi --nproc=1"; + environment = { + FCGI_SOCKET_PATH = "/run/gitweb/gitweb.sock"; + }; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + RuntimeDirectory = [ "gitweb" ]; + }; + wantedBy = [ "multi-user.target" ]; + }; + + services.nginx = { + virtualHosts.${cfg.virtualHost} = { + locations."${cfg.location}/static/" = { + alias = "${package}/static/"; + }; + locations."${cfg.location}/" = { + extraConfig = '' + include ${config.services.nginx.package}/conf/fastcgi_params; + fastcgi_param GITWEB_CONFIG ${gitwebConfig.gitwebConfigFile}; + fastcgi_pass unix:/run/gitweb/gitweb.sock; + ''; + }; + }; + }; + + }; + + meta.maintainers = with maintainers; [ ]; + +} diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix new file mode 100644 index 00000000000..6fd00b38697 --- /dev/null +++ b/nixos/modules/services/web-servers/nginx/location-options.nix @@ -0,0 +1,132 @@ +# This file defines the options that can be used both for the Nginx +# main server configuration, and for the virtual hosts. (The latter +# has additional options that affect the web server as a whole, like +# the user/group to run under.) + +{ lib }: + +with lib; + +{ + options = { + basicAuth = mkOption { + type = types.attrsOf types.str; + default = {}; + example = literalExpression '' + { + user = "password"; + }; + ''; + description = '' + Basic Auth protection for a vhost. + + WARNING: This is implemented to store the password in plain text in the + Nix store. + ''; + }; + + basicAuthFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Basic Auth password file for a vhost. + Can be created via: <command>htpasswd -c <filename> <username></command>. + + WARNING: The generate file contains the users' passwords in a + non-cryptographically-securely hashed way. + ''; + }; + + proxyPass = mkOption { + type = types.nullOr types.str; + default = null; + example = "http://www.example.org/"; + description = '' + Adds proxy_pass directive and sets recommended proxy headers if + recommendedProxySettings is enabled. + ''; + }; + + proxyWebsockets = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to support proxying websocket connections with HTTP/1.1. + ''; + }; + + index = mkOption { + type = types.nullOr types.str; + default = null; + example = "index.php index.html"; + description = '' + Adds index directive. + ''; + }; + + tryFiles = mkOption { + type = types.nullOr types.str; + default = null; + example = "$uri =404"; + description = '' + Adds try_files directive. + ''; + }; + + root = mkOption { + type = types.nullOr types.path; + default = null; + example = "/your/root/directory"; + description = '' + Root directory for requests. + ''; + }; + + alias = mkOption { + type = types.nullOr types.path; + default = null; + example = "/your/alias/directory"; + description = '' + Alias directory for requests. + ''; + }; + + return = mkOption { + type = types.nullOr types.str; + default = null; + example = "301 http://example.com$request_uri"; + description = '' + Adds a return directive, for e.g. redirections. + ''; + }; + + fastcgiParams = mkOption { + type = types.attrsOf (types.either types.str types.path); + default = {}; + description = '' + FastCGI parameters to override. Unlike in the Nginx + configuration file, overriding only some default parameters + won't unset the default values for other parameters. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + These lines go to the end of the location verbatim. + ''; + }; + + priority = mkOption { + type = types.int; + default = 1000; + description = '' + Order of this location block in relation to the others in the vhost. + The semantics are the same as with `lib.mkOrder`. Smaller values have + a greater priority. + ''; + }; + }; +} diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix new file mode 100644 index 00000000000..c4e8285dc48 --- /dev/null +++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix @@ -0,0 +1,288 @@ +# This file defines the options that can be used both for the Nginx +# main server configuration, and for the virtual hosts. (The latter +# has additional options that affect the web server as a whole, like +# the user/group to run under.) + +{ config, lib, ... }: + +with lib; +{ + options = { + serverName = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Name of this virtual host. Defaults to attribute name in virtualHosts. + ''; + example = "example.org"; + }; + + serverAliases = mkOption { + type = types.listOf types.str; + default = []; + example = ["www.example.org" "example.org"]; + description = '' + Additional names of virtual hosts served by this virtual host configuration. + ''; + }; + + listen = mkOption { + type = with types; listOf (submodule { options = { + addr = mkOption { type = str; description = "IP address."; }; + port = mkOption { type = int; description = "Port number."; default = 80; }; + ssl = mkOption { type = bool; description = "Enable SSL."; default = false; }; + extraParameters = mkOption { type = listOf str; description = "Extra parameters of this listen directive."; default = []; example = [ "reuseport" "deferred" ]; }; + }; }); + default = []; + example = [ + { addr = "195.154.1.1"; port = 443; ssl = true;} + { addr = "192.154.1.1"; port = 80; } + ]; + description = '' + Listen addresses and ports for this virtual host. + IPv6 addresses must be enclosed in square brackets. + Note: this option overrides <literal>addSSL</literal> + and <literal>onlySSL</literal>. + + If you only want to set the addresses manually and not + the ports, take a look at <literal>listenAddresses</literal> + ''; + }; + + listenAddresses = mkOption { + type = with types; listOf str; + + description = '' + Listen addresses for this virtual host. + Compared to <literal>listen</literal> this only sets the addreses + and the ports are choosen automatically. + + Note: This option overrides <literal>enableIPv6</literal> + ''; + default = []; + example = [ "127.0.0.1" "::1" ]; + }; + + enableACME = mkOption { + type = types.bool; + default = false; + description = '' + Whether to ask Let's Encrypt to sign a certificate for this vhost. + Alternately, you can use an existing certificate through <option>useACMEHost</option>. + ''; + }; + + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A host of an existing Let's Encrypt certificate to use. + This is useful if you have many subdomains and want to avoid hitting the + <link xlink:href="https://letsencrypt.org/docs/rate-limits/">rate limit</link>. + Alternately, you can generate a certificate through <option>enableACME</option>. + <emphasis>Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using <xref linkend="opt-security.acme.certs"/>.</emphasis> + ''; + }; + + acmeRoot = mkOption { + type = types.nullOr types.str; + default = "/var/lib/acme/acme-challenge"; + description = '' + Directory for the acme challenge which is PUBLIC, don't put certs or keys in here. + Set to null to inherit from config.security.acme. + ''; + }; + + acmeFallbackHost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Host which to proxy requests to if acme challenge is not found. Useful + if you want multiple hosts to be able to verify the same domain name. + ''; + }; + + addSSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTPS in addition to plain HTTP. This will set defaults for + <literal>listen</literal> to listen on all interfaces on the respective default + ports (80, 443). + ''; + }; + + onlySSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTPS and reject plain HTTP connections. This will set + defaults for <literal>listen</literal> to listen on all interfaces on port 443. + ''; + }; + + enableSSL = mkOption { + type = types.bool; + visible = false; + default = false; + }; + + forceSSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to add a separate nginx server block that permanently redirects (301) + all plain HTTP traffic to HTTPS. This will set defaults for + <literal>listen</literal> to listen on all interfaces on the respective default + ports (80, 443), where the non-SSL listens are used for the redirect vhosts. + ''; + }; + + rejectSSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to listen for and reject all HTTPS connections to this vhost. Useful in + <link linkend="opt-services.nginx.virtualHosts._name_.default">default</link> + server blocks to avoid serving the certificate for another vhost. Uses the + <literal>ssl_reject_handshake</literal> directive available in nginx versions + 1.19.4 and above. + ''; + }; + + kTLS = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable kTLS support. + Implementing TLS in the kernel (kTLS) improves performance by significantly + reducing the need for copying operations between user space and the kernel. + Required Nginx version 1.21.4 or later. + ''; + }; + + sslCertificate = mkOption { + type = types.path; + example = "/var/host.cert"; + description = "Path to server SSL certificate."; + }; + + sslCertificateKey = mkOption { + type = types.path; + example = "/var/host.key"; + description = "Path to server SSL certificate key."; + }; + + sslTrustedCertificate = mkOption { + type = types.nullOr types.path; + default = null; + example = literalExpression ''"''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"''; + description = "Path to root SSL certificate for stapling and client certificates."; + }; + + http2 = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable HTTP 2. + Note that (as of writing) due to nginx's implementation, to disable + HTTP 2 you have to disable it on all vhosts that use a given + IP address / port. + If there is one server block configured to enable http2,then it is + enabled for all server blocks on this IP. + See https://stackoverflow.com/a/39466948/263061. + ''; + }; + + http3 = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTP 3. + This requires using <literal>pkgs.nginxQuic</literal> package + which can be achieved by setting <literal>services.nginx.package = pkgs.nginxQuic;</literal>. + Note that HTTP 3 support is experimental and + *not* yet recommended for production. + Read more at https://quic.nginx.org/ + ''; + }; + + root = mkOption { + type = types.nullOr types.path; + default = null; + example = "/data/webserver/docs"; + description = '' + The path of the web root directory. + ''; + }; + + default = mkOption { + type = types.bool; + default = false; + description = '' + Makes this vhost the default. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + These lines go to the end of the vhost verbatim. + ''; + }; + + globalRedirect = mkOption { + type = types.nullOr types.str; + default = null; + example = "newserver.example.org"; + description = '' + If set, all requests for this host are redirected permanently to + the given hostname. + ''; + }; + + basicAuth = mkOption { + type = types.attrsOf types.str; + default = {}; + example = literalExpression '' + { + user = "password"; + }; + ''; + description = '' + Basic Auth protection for a vhost. + + WARNING: This is implemented to store the password in plain text in the + Nix store. + ''; + }; + + basicAuthFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Basic Auth password file for a vhost. + Can be created via: <command>htpasswd -c <filename> <username></command>. + + WARNING: The generate file contains the users' passwords in a + non-cryptographically-securely hashed way. + ''; + }; + + locations = mkOption { + type = types.attrsOf (types.submodule (import ./location-options.nix { + inherit lib; + })); + default = {}; + example = literalExpression '' + { + "/" = { + proxyPass = "http://localhost:3000"; + }; + }; + ''; + description = "Declarative location config"; + }; + }; +} diff --git a/nixos/modules/services/web-servers/phpfpm/default.nix b/nixos/modules/services/web-servers/phpfpm/default.nix new file mode 100644 index 00000000000..87c68fa074a --- /dev/null +++ b/nixos/modules/services/web-servers/phpfpm/default.nix @@ -0,0 +1,282 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.phpfpm; + + runtimeDir = "/run/phpfpm"; + + toStr = value: + if true == value then "yes" + else if false == value then "no" + else toString value; + + fpmCfgFile = pool: poolOpts: pkgs.writeText "phpfpm-${pool}.conf" '' + [global] + ${concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings)} + ${optionalString (cfg.extraConfig != null) cfg.extraConfig} + + [${pool}] + ${concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") poolOpts.settings)} + ${concatStringsSep "\n" (mapAttrsToList (n: v: "env[${n}] = ${toStr v}") poolOpts.phpEnv)} + ${optionalString (poolOpts.extraConfig != null) poolOpts.extraConfig} + ''; + + phpIni = poolOpts: pkgs.runCommand "php.ini" { + inherit (poolOpts) phpPackage phpOptions; + preferLocalBuild = true; + passAsFile = [ "phpOptions" ]; + } '' + cat ${poolOpts.phpPackage}/etc/php.ini $phpOptionsPath > $out + ''; + + poolOpts = { name, ... }: + let + poolOpts = cfg.pools.${name}; + in + { + options = { + socket = mkOption { + type = types.str; + readOnly = true; + description = '' + Path to the unix socket file on which to accept FastCGI requests. + <note><para>This option is read-only and managed by NixOS.</para></note> + ''; + example = "${runtimeDir}/<name>.sock"; + }; + + listen = mkOption { + type = types.str; + default = ""; + example = "/path/to/unix/socket"; + description = '' + The address on which to accept FastCGI requests. + ''; + }; + + phpPackage = mkOption { + type = types.package; + default = cfg.phpPackage; + defaultText = literalExpression "config.services.phpfpm.phpPackage"; + description = '' + The PHP package to use for running this PHP-FPM pool. + ''; + }; + + phpOptions = mkOption { + type = types.lines; + description = '' + "Options appended to the PHP configuration file <filename>php.ini</filename> used for this PHP-FPM pool." + ''; + }; + + phpEnv = lib.mkOption { + type = with types; attrsOf str; + default = {}; + description = '' + Environment variables used for this PHP-FPM pool. + ''; + example = literalExpression '' + { + HOSTNAME = "$HOSTNAME"; + TMP = "/tmp"; + TMPDIR = "/tmp"; + TEMP = "/tmp"; + } + ''; + }; + + user = mkOption { + type = types.str; + description = "User account under which this pool runs."; + }; + + group = mkOption { + type = types.str; + description = "Group account under which this pool runs."; + }; + + settings = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = {}; + description = '' + PHP-FPM pool directives. Refer to the "List of pool directives" section of + <link xlink:href="https://www.php.net/manual/en/install.fpm.configuration.php"/> + for details. Note that settings names must be enclosed in quotes (e.g. + <literal>"pm.max_children"</literal> instead of <literal>pm.max_children</literal>). + ''; + example = literalExpression '' + { + "pm" = "dynamic"; + "pm.max_children" = 75; + "pm.start_servers" = 10; + "pm.min_spare_servers" = 5; + "pm.max_spare_servers" = 20; + "pm.max_requests" = 500; + } + ''; + }; + + extraConfig = mkOption { + type = with types; nullOr lines; + default = null; + description = '' + Extra lines that go into the pool configuration. + See the documentation on <literal>php-fpm.conf</literal> for + details on configuration directives. + ''; + }; + }; + + config = { + socket = if poolOpts.listen == "" then "${runtimeDir}/${name}.sock" else poolOpts.listen; + group = mkDefault poolOpts.user; + phpOptions = mkBefore cfg.phpOptions; + + settings = mapAttrs (name: mkDefault){ + listen = poolOpts.socket; + user = poolOpts.user; + group = poolOpts.group; + }; + }; + }; + +in { + imports = [ + (mkRemovedOptionModule [ "services" "phpfpm" "poolConfigs" ] "Use services.phpfpm.pools instead.") + (mkRemovedOptionModule [ "services" "phpfpm" "phpIni" ] "") + ]; + + options = { + services.phpfpm = { + settings = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = {}; + description = '' + PHP-FPM global directives. Refer to the "List of global php-fpm.conf directives" section of + <link xlink:href="https://www.php.net/manual/en/install.fpm.configuration.php"/> + for details. Note that settings names must be enclosed in quotes (e.g. + <literal>"pm.max_children"</literal> instead of <literal>pm.max_children</literal>). + You need not specify the options <literal>error_log</literal> or + <literal>daemonize</literal> here, since they are generated by NixOS. + ''; + }; + + extraConfig = mkOption { + type = with types; nullOr lines; + default = null; + description = '' + Extra configuration that should be put in the global section of + the PHP-FPM configuration file. Do not specify the options + <literal>error_log</literal> or + <literal>daemonize</literal> here, since they are generated by + NixOS. + ''; + }; + + phpPackage = mkOption { + type = types.package; + default = pkgs.php; + defaultText = literalExpression "pkgs.php"; + description = '' + The PHP package to use for running the PHP-FPM service. + ''; + }; + + phpOptions = mkOption { + type = types.lines; + default = ""; + example = + '' + date.timezone = "CET" + ''; + description = '' + Options appended to the PHP configuration file <filename>php.ini</filename>. + ''; + }; + + pools = mkOption { + type = types.attrsOf (types.submodule poolOpts); + default = {}; + example = literalExpression '' + { + mypool = { + user = "php"; + group = "php"; + phpPackage = pkgs.php; + settings = { + "pm" = "dynamic"; + "pm.max_children" = 75; + "pm.start_servers" = 10; + "pm.min_spare_servers" = 5; + "pm.max_spare_servers" = 20; + "pm.max_requests" = 500; + }; + } + }''; + description = '' + PHP-FPM pools. If no pools are defined, the PHP-FPM + service is disabled. + ''; + }; + }; + }; + + config = mkIf (cfg.pools != {}) { + + warnings = + mapAttrsToList (pool: poolOpts: '' + Using config.services.phpfpm.pools.${pool}.listen is deprecated and will become unsupported in a future release. Please reference the read-only option config.services.phpfpm.pools.${pool}.socket to access the path of your socket. + '') (filterAttrs (pool: poolOpts: poolOpts.listen != "") cfg.pools) ++ + mapAttrsToList (pool: poolOpts: '' + Using config.services.phpfpm.pools.${pool}.extraConfig is deprecated and will become unsupported in a future release. Please migrate your configuration to config.services.phpfpm.pools.${pool}.settings. + '') (filterAttrs (pool: poolOpts: poolOpts.extraConfig != null) cfg.pools) ++ + optional (cfg.extraConfig != null) '' + Using config.services.phpfpm.extraConfig is deprecated and will become unsupported in a future release. Please migrate your configuration to config.services.phpfpm.settings. + '' + ; + + services.phpfpm.settings = { + error_log = "syslog"; + daemonize = false; + }; + + systemd.slices.phpfpm = { + description = "PHP FastCGI Process manager pools slice"; + }; + + systemd.targets.phpfpm = { + description = "PHP FastCGI Process manager pools target"; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.services = mapAttrs' (pool: poolOpts: + nameValuePair "phpfpm-${pool}" { + description = "PHP FastCGI Process Manager service for pool ${pool}"; + after = [ "network.target" ]; + wantedBy = [ "phpfpm.target" ]; + partOf = [ "phpfpm.target" ]; + serviceConfig = let + cfgFile = fpmCfgFile pool poolOpts; + iniFile = phpIni poolOpts; + in { + Slice = "phpfpm.slice"; + PrivateDevices = true; + PrivateTmp = true; + ProtectSystem = "full"; + ProtectHome = true; + # XXX: We need AF_NETLINK to make the sendmail SUID binary from postfix work + RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + Type = "notify"; + ExecStart = "${poolOpts.phpPackage}/bin/php-fpm -y ${cfgFile} -c ${iniFile}"; + ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID"; + RuntimeDirectory = "phpfpm"; + RuntimeDirectoryPreserve = true; # Relevant when multiple processes are running + Restart = "always"; + }; + } + ) cfg.pools; + }; +} diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix new file mode 100644 index 00000000000..0b460755f50 --- /dev/null +++ b/nixos/modules/services/web-servers/pomerium.nix @@ -0,0 +1,135 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + format = pkgs.formats.yaml {}; +in +{ + options.services.pomerium = { + enable = mkEnableOption "the Pomerium authenticating reverse proxy"; + + configFile = mkOption { + type = with types; nullOr path; + default = null; + description = "Path to Pomerium config YAML. If set, overrides services.pomerium.settings."; + }; + + useACMEHost = mkOption { + type = with types; nullOr str; + default = null; + description = '' + If set, use a NixOS-generated ACME certificate with the specified name. + + Note that this will require you to use a non-HTTP-based challenge, or + disable Pomerium's in-built HTTP redirect server by setting + http_redirect_addr to null and use a different HTTP server for serving + the challenge response. + + If you're using an HTTP-based challenge, you should use the + Pomerium-native autocert option instead. + ''; + }; + + settings = mkOption { + description = '' + The contents of Pomerium's config.yaml, in Nix expressions. + + Specifying configFile will override this in its entirety. + + See <link xlink:href="https://pomerium.io/reference/">the Pomerium + configuration reference</link> for more information about what to put + here. + ''; + default = {}; + type = format.type; + }; + + secretsFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + Path to file containing secrets for Pomerium, in systemd + EnvironmentFile format. See the systemd.exec(5) man page. + ''; + }; + }; + + config = let + cfg = config.services.pomerium; + cfgFile = if cfg.configFile != null then cfg.configFile else (format.generate "pomerium.yaml" cfg.settings); + in mkIf cfg.enable ({ + systemd.services.pomerium = { + description = "Pomerium authenticating reverse proxy"; + wants = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target"); + after = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target"); + wantedBy = [ "multi-user.target" ]; + environment = optionalAttrs (cfg.useACMEHost != null) { + CERTIFICATE_FILE = "fullchain.pem"; + CERTIFICATE_KEY_FILE = "key.pem"; + }; + startLimitIntervalSec = 60; + script = '' + if [[ -v CREDENTIALS_DIRECTORY ]]; then + cd "$CREDENTIALS_DIRECTORY" + fi + exec "${pkgs.pomerium}/bin/pomerium" -config "${cfgFile}" + ''; + + serviceConfig = { + DynamicUser = true; + StateDirectory = [ "pomerium" ]; + + PrivateUsers = false; # breaks CAP_NET_BIND_SERVICE + MemoryDenyWriteExecute = false; # breaks LuaJIT + + NoNewPrivileges = true; + PrivateTmp = true; + PrivateDevices = true; + DevicePolicy = "closed"; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectKernelLogs = true; + RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + SystemCallArchitectures = "native"; + + EnvironmentFile = cfg.secretsFile; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + + LoadCredential = optionals (cfg.useACMEHost != null) [ + "fullchain.pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem" + "key.pem:/var/lib/acme/${cfg.useACMEHost}/key.pem" + ]; + }; + }; + + # postRun hooks on cert renew can't be used to restart Nginx since renewal + # runs as the unprivileged acme user. sslTargets are added to wantedBy + before + # which allows the acme-finished-$cert.target to signify the successful updating + # of certs end-to-end. + systemd.services.pomerium-config-reload = mkIf (cfg.useACMEHost != null) { + # TODO(lukegb): figure out how to make config reloading work with credentials. + + wantedBy = [ "acme-finished-${cfg.useACMEHost}.target" "multi-user.target" ]; + # Before the finished targets, after the renew services. + before = [ "acme-finished-${cfg.useACMEHost}.target" ]; + after = [ "acme-${cfg.useACMEHost}.service" ]; + # Block reloading if not all certs exist yet. + unitConfig.ConditionPathExists = [ "${config.security.acme.certs.${cfg.useACMEHost}.directory}/fullchain.pem" ]; + serviceConfig = { + Type = "oneshot"; + TimeoutSec = 60; + ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active pomerium.service"; + ExecStart = "/run/current-system/systemd/bin/systemctl --no-block restart pomerium.service"; + }; + }; + }); +} diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix new file mode 100644 index 00000000000..877097cf378 --- /dev/null +++ b/nixos/modules/services/web-servers/tomcat.nix @@ -0,0 +1,423 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.tomcat; + tomcat = cfg.package; +in + +{ + + meta = { + maintainers = with maintainers; [ danbst ]; + }; + + ###### interface + + options = { + + services.tomcat = { + enable = mkEnableOption "Apache Tomcat"; + + package = mkOption { + type = types.package; + default = pkgs.tomcat9; + defaultText = literalExpression "pkgs.tomcat9"; + example = lib.literalExpression "pkgs.tomcat9"; + description = '' + Which tomcat package to use. + ''; + }; + + purifyOnStart = mkOption { + type = types.bool; + default = false; + description = '' + On startup, the `baseDir` directory is populated with various files, + subdirectories and symlinks. If this option is enabled, these items + (except for the `logs` and `work` subdirectories) are first removed. + This prevents interference from remainders of an old configuration + (libraries, webapps, etc.), so it's recommended to enable this option. + ''; + }; + + baseDir = mkOption { + type = lib.types.path; + default = "/var/tomcat"; + description = '' + Location where Tomcat stores configuration files, web applications + and logfiles. Note that it is partially cleared on each service startup + if `purifyOnStart` is enabled. + ''; + }; + + logDirs = mkOption { + default = []; + type = types.listOf types.path; + description = "Directories to create in baseDir/logs/"; + }; + + extraConfigFiles = mkOption { + default = []; + type = types.listOf types.path; + description = "Extra configuration files to pull into the tomcat conf directory"; + }; + + extraEnvironment = mkOption { + type = types.listOf types.str; + default = []; + example = [ "ENVIRONMENT=production" ]; + description = "Environment Variables to pass to the tomcat service"; + }; + + extraGroups = mkOption { + default = []; + type = types.listOf types.str; + example = [ "users" ]; + description = "Defines extra groups to which the tomcat user belongs."; + }; + + user = mkOption { + type = types.str; + default = "tomcat"; + description = "User account under which Apache Tomcat runs."; + }; + + group = mkOption { + type = types.str; + default = "tomcat"; + description = "Group account under which Apache Tomcat runs."; + }; + + javaOpts = mkOption { + type = types.either (types.listOf types.str) types.str; + default = ""; + description = "Parameters to pass to the Java Virtual Machine which spawns Apache Tomcat"; + }; + + catalinaOpts = mkOption { + type = types.either (types.listOf types.str) types.str; + default = ""; + description = "Parameters to pass to the Java Virtual Machine which spawns the Catalina servlet container"; + }; + + sharedLibs = mkOption { + type = types.listOf types.str; + default = []; + description = "List containing JAR files or directories with JAR files which are libraries shared by the web applications"; + }; + + serverXml = mkOption { + type = types.lines; + default = ""; + description = " + Verbatim server.xml configuration. + This is mutually exclusive with the virtualHosts options. + "; + }; + + commonLibs = mkOption { + type = types.listOf types.str; + default = []; + description = "List containing JAR files or directories with JAR files which are libraries shared by the web applications and the servlet container"; + }; + + webapps = mkOption { + type = types.listOf types.path; + default = [ tomcat.webapps ]; + defaultText = literalExpression "[ config.services.tomcat.package.webapps ]"; + description = "List containing WAR files or directories with WAR files which are web applications to be deployed on Tomcat"; + }; + + virtualHosts = mkOption { + type = types.listOf (types.submodule { + options = { + name = mkOption { + type = types.str; + description = "name of the virtualhost"; + }; + aliases = mkOption { + type = types.listOf types.str; + description = "aliases of the virtualhost"; + default = []; + }; + webapps = mkOption { + type = types.listOf types.path; + description = '' + List containing web application WAR files and/or directories containing + web applications and configuration files for the virtual host. + ''; + default = []; + }; + }; + }); + default = []; + description = "List consisting of a virtual host name and a list of web applications to deploy on each virtual host"; + }; + + logPerVirtualHost = mkOption { + type = types.bool; + default = false; + description = "Whether to enable logging per virtual host."; + }; + + jdk = mkOption { + type = types.package; + default = pkgs.jdk; + defaultText = literalExpression "pkgs.jdk"; + description = "Which JDK to use."; + }; + + axis2 = { + + enable = mkOption { + default = false; + type = types.bool; + description = "Whether to enable an Apache Axis2 container"; + }; + + services = mkOption { + default = []; + type = types.listOf types.str; + description = "List containing AAR files or directories with AAR files which are web services to be deployed on Axis2"; + }; + + }; + + }; + + }; + + + ###### implementation + + config = mkIf config.services.tomcat.enable { + + users.groups.tomcat.gid = config.ids.gids.tomcat; + + users.users.tomcat = + { uid = config.ids.uids.tomcat; + description = "Tomcat user"; + home = "/homeless-shelter"; + group = "tomcat"; + extraGroups = cfg.extraGroups; + }; + + systemd.services.tomcat = { + description = "Apache Tomcat server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + preStart = '' + ${lib.optionalString cfg.purifyOnStart '' + # Delete most directories/symlinks we create from the existing base directory, + # to get rid of remainders of an old configuration. + # The list of directories to delete is taken from the "mkdir" command below, + # excluding "logs" (because logs are valuable) and "work" (because normally + # session files are there), and additionally including "bin". + rm -rf ${cfg.baseDir}/{conf,virtualhosts,temp,lib,shared/lib,webapps,bin} + ''} + + # Create the base directory + mkdir -p \ + ${cfg.baseDir}/{conf,virtualhosts,logs,temp,lib,shared/lib,webapps,work} + chown ${cfg.user}:${cfg.group} \ + ${cfg.baseDir}/{conf,virtualhosts,logs,temp,lib,shared/lib,webapps,work} + + # Create a symlink to the bin directory of the tomcat component + ln -sfn ${tomcat}/bin ${cfg.baseDir}/bin + + # Symlink the config files in the conf/ directory (except for catalina.properties and server.xml) + for i in $(ls ${tomcat}/conf | grep -v catalina.properties | grep -v server.xml); do + ln -sfn ${tomcat}/conf/$i ${cfg.baseDir}/conf/`basename $i` + done + + ${if cfg.extraConfigFiles != [] then '' + for i in ${toString cfg.extraConfigFiles}; do + ln -sfn $i ${cfg.baseDir}/conf/`basename $i` + done + '' else ""} + + # Create a modified catalina.properties file + # Change all references from CATALINA_HOME to CATALINA_BASE and add support for shared libraries + sed -e 's|''${catalina.home}|''${catalina.base}|g' \ + -e 's|shared.loader=|shared.loader=''${catalina.base}/shared/lib/*.jar|' \ + ${tomcat}/conf/catalina.properties > ${cfg.baseDir}/conf/catalina.properties + + ${if cfg.serverXml != "" then '' + cp -f ${pkgs.writeTextDir "server.xml" cfg.serverXml}/* ${cfg.baseDir}/conf/ + '' else + let + hostElementForVirtualHost = virtualHost: '' + <Host name="${virtualHost.name}" appBase="virtualhosts/${virtualHost.name}/webapps" + unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> + '' + concatStrings (innerElementsForVirtualHost virtualHost) + '' + </Host> + ''; + innerElementsForVirtualHost = virtualHost: + (map (alias: '' + <Alias>${alias}</Alias> + '') virtualHost.aliases) + ++ (optional cfg.logPerVirtualHost '' + <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs/${virtualHost.name}" + prefix="${virtualHost.name}_access_log." pattern="combined" resolveHosts="false"/> + ''); + hostElementsString = concatMapStringsSep "\n" hostElementForVirtualHost cfg.virtualHosts; + hostElementsSedString = replaceStrings ["\n"] ["\\\n"] hostElementsString; + in '' + # Create a modified server.xml which also includes all virtual hosts + sed -e "/<Engine name=\"Catalina\" defaultHost=\"localhost\">/a\\"${escapeShellArg hostElementsSedString} \ + ${tomcat}/conf/server.xml > ${cfg.baseDir}/conf/server.xml + '' + } + ${optionalString (cfg.logDirs != []) '' + for i in ${toString cfg.logDirs}; do + mkdir -p ${cfg.baseDir}/logs/$i + chown ${cfg.user}:${cfg.group} ${cfg.baseDir}/logs/$i + done + ''} + ${optionalString cfg.logPerVirtualHost (toString (map (h: '' + mkdir -p ${cfg.baseDir}/logs/${h.name} + chown ${cfg.user}:${cfg.group} ${cfg.baseDir}/logs/${h.name} + '') cfg.virtualHosts))} + + # Symlink all the given common libs files or paths into the lib/ directory + for i in ${tomcat} ${toString cfg.commonLibs}; do + if [ -f $i ]; then + # If the given web application is a file, symlink it into the common/lib/ directory + ln -sfn $i ${cfg.baseDir}/lib/`basename $i` + elif [ -d $i ]; then + # If the given web application is a directory, then iterate over the files + # in the special purpose directories and symlink them into the tomcat tree + + for j in $i/lib/*; do + ln -sfn $j ${cfg.baseDir}/lib/`basename $j` + done + fi + done + + # Symlink all the given shared libs files or paths into the shared/lib/ directory + for i in ${toString cfg.sharedLibs}; do + if [ -f $i ]; then + # If the given web application is a file, symlink it into the common/lib/ directory + ln -sfn $i ${cfg.baseDir}/shared/lib/`basename $i` + elif [ -d $i ]; then + # If the given web application is a directory, then iterate over the files + # in the special purpose directories and symlink them into the tomcat tree + + for j in $i/shared/lib/*; do + ln -sfn $j ${cfg.baseDir}/shared/lib/`basename $j` + done + fi + done + + # Symlink all the given web applications files or paths into the webapps/ directory + for i in ${toString cfg.webapps}; do + if [ -f $i ]; then + # If the given web application is a file, symlink it into the webapps/ directory + ln -sfn $i ${cfg.baseDir}/webapps/`basename $i` + elif [ -d $i ]; then + # If the given web application is a directory, then iterate over the files + # in the special purpose directories and symlink them into the tomcat tree + + for j in $i/webapps/*; do + ln -sfn $j ${cfg.baseDir}/webapps/`basename $j` + done + + # Also symlink the configuration files if they are included + if [ -d $i/conf/Catalina ]; then + for j in $i/conf/Catalina/*; do + mkdir -p ${cfg.baseDir}/conf/Catalina/localhost + ln -sfn $j ${cfg.baseDir}/conf/Catalina/localhost/`basename $j` + done + fi + fi + done + + ${toString (map (virtualHost: '' + # Create webapps directory for the virtual host + mkdir -p ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps + + # Modify ownership + chown ${cfg.user}:${cfg.group} ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps + + # Symlink all the given web applications files or paths into the webapps/ directory + # of this virtual host + for i in "${if virtualHost ? webapps then toString virtualHost.webapps else ""}"; do + if [ -f $i ]; then + # If the given web application is a file, symlink it into the webapps/ directory + ln -sfn $i ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $i` + elif [ -d $i ]; then + # If the given web application is a directory, then iterate over the files + # in the special purpose directories and symlink them into the tomcat tree + + for j in $i/webapps/*; do + ln -sfn $j ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $j` + done + + # Also symlink the configuration files if they are included + if [ -d $i/conf/Catalina ]; then + for j in $i/conf/Catalina/*; do + mkdir -p ${cfg.baseDir}/conf/Catalina/${virtualHost.name} + ln -sfn $j ${cfg.baseDir}/conf/Catalina/${virtualHost.name}/`basename $j` + done + fi + fi + done + '') cfg.virtualHosts)} + + ${optionalString cfg.axis2.enable '' + # Copy the Axis2 web application + cp -av ${pkgs.axis2}/webapps/axis2 ${cfg.baseDir}/webapps + + # Turn off addressing, which causes many errors + sed -i -e 's%<module ref="addressing"/>%<!-- <module ref="addressing"/> -->%' ${cfg.baseDir}/webapps/axis2/WEB-INF/conf/axis2.xml + + # Modify permissions on the Axis2 application + chown -R ${cfg.user}:${cfg.group} ${cfg.baseDir}/webapps/axis2 + + # Symlink all the given web service files or paths into the webapps/axis2/WEB-INF/services directory + for i in ${toString cfg.axis2.services}; do + if [ -f $i ]; then + # If the given web service is a file, symlink it into the webapps/axis2/WEB-INF/services + ln -sfn $i ${cfg.baseDir}/webapps/axis2/WEB-INF/services/`basename $i` + elif [ -d $i ]; then + # If the given web application is a directory, then iterate over the files + # in the special purpose directories and symlink them into the tomcat tree + + for j in $i/webapps/axis2/WEB-INF/services/*; do + ln -sfn $j ${cfg.baseDir}/webapps/axis2/WEB-INF/services/`basename $j` + done + + # Also symlink the configuration files if they are included + if [ -d $i/conf/Catalina ]; then + for j in $i/conf/Catalina/*; do + ln -sfn $j ${cfg.baseDir}/conf/Catalina/localhost/`basename $j` + done + fi + fi + done + ''} + ''; + + serviceConfig = { + Type = "forking"; + PermissionsStartOnly = true; + PIDFile="/run/tomcat/tomcat.pid"; + RuntimeDirectory = "tomcat"; + User = cfg.user; + Environment=[ + "CATALINA_BASE=${cfg.baseDir}" + "CATALINA_PID=/run/tomcat/tomcat.pid" + "JAVA_HOME='${cfg.jdk}'" + "JAVA_OPTS='${builtins.toString cfg.javaOpts}'" + "CATALINA_OPTS='${builtins.toString cfg.catalinaOpts}'" + ] ++ cfg.extraEnvironment; + ExecStart = "${tomcat}/bin/startup.sh"; + ExecStop = "${tomcat}/bin/shutdown.sh"; + }; + }; + }; +} diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix new file mode 100644 index 00000000000..eb7fd0995de --- /dev/null +++ b/nixos/modules/services/web-servers/traefik.nix @@ -0,0 +1,170 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.traefik; + jsonValue = with types; + let + valueType = nullOr (oneOf [ + bool + int + float + str + (lazyAttrsOf valueType) + (listOf valueType) + ]) // { + description = "JSON value"; + emptyValue.value = { }; + }; + in valueType; + dynamicConfigFile = if cfg.dynamicConfigFile == null then + pkgs.runCommand "config.toml" { + buildInputs = [ pkgs.remarshal ]; + preferLocalBuild = true; + } '' + remarshal -if json -of toml \ + < ${ + pkgs.writeText "dynamic_config.json" + (builtins.toJSON cfg.dynamicConfigOptions) + } \ + > $out + '' + else + cfg.dynamicConfigFile; + staticConfigFile = if cfg.staticConfigFile == null then + pkgs.runCommand "config.toml" { + buildInputs = [ pkgs.yj ]; + preferLocalBuild = true; + } '' + yj -jt -i \ + < ${ + pkgs.writeText "static_config.json" (builtins.toJSON + (recursiveUpdate cfg.staticConfigOptions { + providers.file.filename = "${dynamicConfigFile}"; + })) + } \ + > $out + '' + else + cfg.staticConfigFile; +in { + options.services.traefik = { + enable = mkEnableOption "Traefik web server"; + + staticConfigFile = mkOption { + default = null; + example = literalExpression "/path/to/static_config.toml"; + type = types.nullOr types.path; + description = '' + Path to traefik's static configuration to use. + (Using that option has precedence over <literal>staticConfigOptions</literal> and <literal>dynamicConfigOptions</literal>) + ''; + }; + + staticConfigOptions = mkOption { + description = '' + Static configuration for Traefik. + ''; + type = jsonValue; + default = { entryPoints.http.address = ":80"; }; + example = { + entryPoints.web.address = ":8080"; + entryPoints.http.address = ":80"; + + api = { }; + }; + }; + + dynamicConfigFile = mkOption { + default = null; + example = literalExpression "/path/to/dynamic_config.toml"; + type = types.nullOr types.path; + description = '' + Path to traefik's dynamic configuration to use. + (Using that option has precedence over <literal>dynamicConfigOptions</literal>) + ''; + }; + + dynamicConfigOptions = mkOption { + description = '' + Dynamic configuration for Traefik. + ''; + type = jsonValue; + default = { }; + example = { + http.routers.router1 = { + rule = "Host(`localhost`)"; + service = "service1"; + }; + + http.services.service1.loadBalancer.servers = + [{ url = "http://localhost:8080"; }]; + }; + }; + + dataDir = mkOption { + default = "/var/lib/traefik"; + type = types.path; + description = '' + Location for any persistent data traefik creates, ie. acme + ''; + }; + + group = mkOption { + default = "traefik"; + type = types.str; + example = "docker"; + description = '' + Set the group that traefik runs under. + For the docker backend this needs to be set to <literal>docker</literal> instead. + ''; + }; + + package = mkOption { + default = pkgs.traefik; + defaultText = literalExpression "pkgs.traefik"; + type = types.package; + description = "Traefik package to use."; + }; + }; + + config = mkIf cfg.enable { + systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0700 traefik traefik - -" ]; + + systemd.services.traefik = { + description = "Traefik web server"; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + startLimitIntervalSec = 86400; + startLimitBurst = 5; + serviceConfig = { + ExecStart = + "${cfg.package}/bin/traefik --configfile=${staticConfigFile}"; + Type = "simple"; + User = "traefik"; + Group = cfg.group; + Restart = "on-failure"; + AmbientCapabilities = "cap_net_bind_service"; + CapabilityBoundingSet = "cap_net_bind_service"; + NoNewPrivileges = true; + LimitNPROC = 64; + LimitNOFILE = 1048576; + PrivateTmp = true; + PrivateDevices = true; + ProtectHome = true; + ProtectSystem = "full"; + ReadWriteDirectories = cfg.dataDir; + }; + }; + + users.users.traefik = { + group = "traefik"; + home = cfg.dataDir; + createHome = true; + isSystemUser = true; + }; + + users.groups.traefik = { }; + }; +} diff --git a/nixos/modules/services/web-servers/trafficserver/default.nix b/nixos/modules/services/web-servers/trafficserver/default.nix new file mode 100644 index 00000000000..b52087fa038 --- /dev/null +++ b/nixos/modules/services/web-servers/trafficserver/default.nix @@ -0,0 +1,310 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.trafficserver; + user = config.users.users.trafficserver.name; + group = config.users.groups.trafficserver.name; + + getManualUrl = name: "https://docs.trafficserver.apache.org/en/latest/admin-guide/files/${name}.en.html"; + + yaml = pkgs.formats.yaml { }; + + mkYamlConf = name: cfg: + if cfg != null then { + "trafficserver/${name}.yaml".source = yaml.generate "${name}.yaml" cfg; + } else { + "trafficserver/${name}.yaml".text = ""; + }; + + mkRecordLines = path: value: + if isAttrs value then + lib.mapAttrsToList (n: v: mkRecordLines (path ++ [ n ]) v) value + else if isInt value then + "CONFIG ${concatStringsSep "." path} INT ${toString value}" + else if isFloat value then + "CONFIG ${concatStringsSep "." path} FLOAT ${toString value}" + else + "CONFIG ${concatStringsSep "." path} STRING ${toString value}"; + + mkRecordsConfig = cfg: concatStringsSep "\n" (flatten (mkRecordLines [ ] cfg)); + mkPluginConfig = cfg: concatStringsSep "\n" (map (p: "${p.path} ${p.arg}") cfg); +in +{ + options.services.trafficserver = { + enable = mkEnableOption "Apache Traffic Server"; + + cache = mkOption { + type = types.lines; + default = ""; + example = "dest_domain=example.com suffix=js action=never-cache"; + description = '' + Caching rules that overrule the origin's caching policy. + + Consult the <link xlink:href="${getManualUrl "cache.config"}">upstream + documentation</link> for more details. + ''; + }; + + hosting = mkOption { + type = types.lines; + default = ""; + example = "domain=example.com volume=1"; + description = '' + Partition the cache according to origin server or domain + + Consult the <link xlink:href="${getManualUrl "hosting.config"}"> + upstream documentation</link> for more details. + ''; + }; + + ipAllow = mkOption { + type = types.nullOr yaml.type; + default = lib.importJSON ./ip_allow.json; + defaultText = literalDocBook "upstream defaults"; + example = literalExpression '' + { + ip_allow = [{ + apply = "in"; + ip_addrs = "127.0.0.1"; + action = "allow"; + methods = "ALL"; + }]; + } + ''; + description = '' + Control client access to Traffic Server and Traffic Server connections + to upstream servers. + + Consult the <link xlink:href="${getManualUrl "ip_allow.yaml"}">upstream + documentation</link> for more details. + ''; + }; + + logging = mkOption { + type = types.nullOr yaml.type; + default = lib.importJSON ./logging.json; + defaultText = literalDocBook "upstream defaults"; + example = { }; + description = '' + Configure logs. + + Consult the <link xlink:href="${getManualUrl "logging.yaml"}">upstream + documentation</link> for more details. + ''; + }; + + parent = mkOption { + type = types.lines; + default = ""; + example = '' + dest_domain=. method=get parent="p1.example:8080; p2.example:8080" round_robin=true + ''; + description = '' + Identify the parent proxies used in an cache hierarchy. + + Consult the <link xlink:href="${getManualUrl "parent.config"}">upstream + documentation</link> for more details. + ''; + }; + + plugins = mkOption { + default = [ ]; + + description = '' + Controls run-time loadable plugins available to Traffic Server, as + well as their configuration. + + Consult the <link xlink:href="${getManualUrl "plugin.config"}">upstream + documentation</link> for more details. + ''; + + type = with types; + listOf (submodule { + options.path = mkOption { + type = str; + example = "xdebug.so"; + description = '' + Path to plugin. The path can either be absolute, or relative to + the plugin directory. + ''; + }; + options.arg = mkOption { + type = str; + default = ""; + example = "--header=ATS-My-Debug"; + description = "arguments to pass to the plugin"; + }; + }); + }; + + records = mkOption { + type = with types; + let valueType = (attrsOf (oneOf [ int float str valueType ])) // { + description = "Traffic Server records value"; + }; + in + valueType; + default = { }; + example = { proxy.config.proxy_name = "my_server"; }; + description = '' + List of configurable variables used by Traffic Server. + + Consult the <link xlink:href="${getManualUrl "records.config"}"> + upstream documentation</link> for more details. + ''; + }; + + remap = mkOption { + type = types.lines; + default = ""; + example = "map http://from.example http://origin.example"; + description = '' + URL remapping rules used by Traffic Server. + + Consult the <link xlink:href="${getManualUrl "remap.config"}"> + upstream documentation</link> for more details. + ''; + }; + + splitDns = mkOption { + type = types.lines; + default = ""; + example = '' + dest_domain=internal.corp.example named="255.255.255.255:212 255.255.255.254" def_domain=corp.example search_list="corp.example corp1.example" + dest_domain=!internal.corp.example named=255.255.255.253 + ''; + description = '' + Specify the DNS server that Traffic Server should use under specific + conditions. + + Consult the <link xlink:href="${getManualUrl "splitdns.config"}"> + upstream documentation</link> for more details. + ''; + }; + + sslMulticert = mkOption { + type = types.lines; + default = ""; + example = "dest_ip=* ssl_cert_name=default.pem"; + description = '' + Configure SSL server certificates to terminate the SSL sessions. + + Consult the <link xlink:href="${getManualUrl "ssl_multicert.config"}"> + upstream documentation</link> for more details. + ''; + }; + + sni = mkOption { + type = types.nullOr yaml.type; + default = null; + example = literalExpression '' + { + sni = [{ + fqdn = "no-http2.example.com"; + https = "off"; + }]; + } + ''; + description = '' + Configure aspects of TLS connection handling for both inbound and + outbound connections. + + Consult the <link xlink:href="${getManualUrl "sni.yaml"}">upstream + documentation</link> for more details. + ''; + }; + + storage = mkOption { + type = types.lines; + default = "/var/cache/trafficserver 256M"; + example = "/dev/disk/by-id/XXXXX volume=1"; + description = '' + List all the storage that make up the Traffic Server cache. + + Consult the <link xlink:href="${getManualUrl "storage.config"}"> + upstream documentation</link> for more details. + ''; + }; + + strategies = mkOption { + type = types.nullOr yaml.type; + default = null; + description = '' + Specify the next hop proxies used in an cache hierarchy and the + algorithms used to select the next proxy. + + Consult the <link xlink:href="${getManualUrl "strategies.yaml"}"> + upstream documentation</link> for more details. + ''; + }; + + volume = mkOption { + type = types.nullOr yaml.type; + default = ""; + example = "volume=1 scheme=http size=20%"; + description = '' + Manage cache space more efficiently and restrict disk usage by + creating cache volumes of different sizes. + + Consult the <link xlink:href="${getManualUrl "volume.config"}"> + upstream documentation</link> for more details. + ''; + }; + }; + + config = mkIf cfg.enable { + environment.etc = { + "trafficserver/cache.config".text = cfg.cache; + "trafficserver/hosting.config".text = cfg.hosting; + "trafficserver/parent.config".text = cfg.parent; + "trafficserver/plugin.config".text = mkPluginConfig cfg.plugins; + "trafficserver/records.config".text = mkRecordsConfig cfg.records; + "trafficserver/remap.config".text = cfg.remap; + "trafficserver/splitdns.config".text = cfg.splitDns; + "trafficserver/ssl_multicert.config".text = cfg.sslMulticert; + "trafficserver/storage.config".text = cfg.storage; + "trafficserver/volume.config".text = cfg.volume; + } // (mkYamlConf "ip_allow" cfg.ipAllow) + // (mkYamlConf "logging" cfg.logging) + // (mkYamlConf "sni" cfg.sni) + // (mkYamlConf "strategies" cfg.strategies); + + environment.systemPackages = [ pkgs.trafficserver ]; + systemd.packages = [ pkgs.trafficserver ]; + + # Traffic Server does privilege handling independently of systemd, and + # therefore should be started as root + systemd.services.trafficserver = { + enable = true; + wantedBy = [ "multi-user.target" ]; + }; + + # These directories can't be created by systemd because: + # + # 1. Traffic Servers starts as root and switches to an unprivileged user + # afterwards. The runtime directories defined below are assumed to be + # owned by that user. + # 2. The bin/trafficserver script assumes these directories exist. + systemd.tmpfiles.rules = [ + "d '/run/trafficserver' - ${user} ${group} - -" + "d '/var/cache/trafficserver' - ${user} ${group} - -" + "d '/var/lib/trafficserver' - ${user} ${group} - -" + "d '/var/log/trafficserver' - ${user} ${group} - -" + ]; + + services.trafficserver = { + records.proxy.config.admin.user_id = user; + records.proxy.config.body_factory.template_sets_dir = + "${pkgs.trafficserver}/etc/trafficserver/body_factory"; + }; + + users.users.trafficserver = { + description = "Apache Traffic Server"; + isSystemUser = true; + inherit group; + }; + users.groups.trafficserver = { }; + }; +} diff --git a/nixos/modules/services/web-servers/trafficserver/ip_allow.json b/nixos/modules/services/web-servers/trafficserver/ip_allow.json new file mode 100644 index 00000000000..fc2db803728 --- /dev/null +++ b/nixos/modules/services/web-servers/trafficserver/ip_allow.json @@ -0,0 +1,36 @@ +{ + "ip_allow": [ + { + "apply": "in", + "ip_addrs": "127.0.0.1", + "action": "allow", + "methods": "ALL" + }, + { + "apply": "in", + "ip_addrs": "::1", + "action": "allow", + "methods": "ALL" + }, + { + "apply": "in", + "ip_addrs": "0/0", + "action": "deny", + "methods": [ + "PURGE", + "PUSH", + "DELETE" + ] + }, + { + "apply": "in", + "ip_addrs": "::/0", + "action": "deny", + "methods": [ + "PURGE", + "PUSH", + "DELETE" + ] + } + ] +} diff --git a/nixos/modules/services/web-servers/trafficserver/logging.json b/nixos/modules/services/web-servers/trafficserver/logging.json new file mode 100644 index 00000000000..81e7ba0186c --- /dev/null +++ b/nixos/modules/services/web-servers/trafficserver/logging.json @@ -0,0 +1,37 @@ +{ + "logging": { + "formats": [ + { + "name": "welf", + "format": "id=firewall time=\"%<cqtd> %<cqtt>\" fw=%<phn> pri=6 proto=%<cqus> duration=%<ttmsf> sent=%<psql> rcvd=%<cqhl> src=%<chi> dst=%<shi> dstname=%<shn> user=%<caun> op=%<cqhm> arg=\"%<cqup>\" result=%<pssc> ref=\"%<{Referer}cqh>\" agent=\"%<{user-agent}cqh>\" cache=%<crc>" + }, + { + "name": "squid_seconds_only_timestamp", + "format": "%<cqts> %<ttms> %<chi> %<crc>/%<pssc> %<psql> %<cqhm> %<cquc> %<caun> %<phr>/%<shn> %<psct>" + }, + { + "name": "squid", + "format": "%<cqtq> %<ttms> %<chi> %<crc>/%<pssc> %<psql> %<cqhm> %<cquc> %<caun> %<phr>/%<shn> %<psct>" + }, + { + "name": "common", + "format": "%<chi> - %<caun> [%<cqtn>] \"%<cqtx>\" %<pssc> %<pscl>" + }, + { + "name": "extended", + "format": "%<chi> - %<caun> [%<cqtn>] \"%<cqtx>\" %<pssc> %<pscl> %<sssc> %<sscl> %<cqcl> %<pqcl> %<cqhl> %<pshl> %<pqhl> %<sshl> %<tts>" + }, + { + "name": "extended2", + "format": "%<chi> - %<caun> [%<cqtn>] \"%<cqtx>\" %<pssc> %<pscl> %<sssc> %<sscl> %<cqcl> %<pqcl> %<cqhl> %<pshl> %<pqhl> %<sshl> %<tts> %<phr> %<cfsc> %<pfsc> %<crc>" + } + ], + "logs": [ + { + "filename": "squid", + "format": "squid", + "mode": "binary" + } + ] + } +} diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix new file mode 100644 index 00000000000..431509f7fd5 --- /dev/null +++ b/nixos/modules/services/web-servers/ttyd.nix @@ -0,0 +1,196 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.ttyd; + + # Command line arguments for the ttyd daemon + args = [ "--port" (toString cfg.port) ] + ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ] + ++ optionals (cfg.interface != null) [ "--interface" cfg.interface ] + ++ [ "--signal" (toString cfg.signal) ] + ++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions)) + ++ [ "--terminal-type" cfg.terminalType ] + ++ optionals cfg.checkOrigin [ "--check-origin" ] + ++ [ "--max-clients" (toString cfg.maxClients) ] + ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ] + ++ optionals cfg.enableIPv6 [ "--ipv6" ] + ++ optionals cfg.enableSSL [ "--ssl-cert" cfg.certFile + "--ssl-key" cfg.keyFile + "--ssl-ca" cfg.caFile ] + ++ [ "--debug" (toString cfg.logLevel) ]; + +in + +{ + + ###### interface + + options = { + services.ttyd = { + enable = mkEnableOption "ttyd daemon"; + + port = mkOption { + type = types.port; + default = 7681; + description = "Port to listen on (use 0 for random port)"; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/run/ttyd.sock"; + description = "UNIX domain socket path to bind."; + }; + + interface = mkOption { + type = types.nullOr types.str; + default = null; + example = "eth0"; + description = "Network interface to bind."; + }; + + username = mkOption { + type = types.nullOr types.str; + default = null; + description = "Username for basic authentication."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + apply = value: if value == null then null else toString value; + description = '' + File containing the password to use for basic authentication. + For insecurely putting the password in the globally readable store use + <literal>pkgs.writeText "ttydpw" "MyPassword"</literal>. + ''; + }; + + signal = mkOption { + type = types.ints.u8; + default = 1; + description = "Signal to send to the command on session close."; + }; + + clientOptions = mkOption { + type = types.attrsOf types.str; + default = {}; + example = literalExpression ''{ + fontSize = "16"; + fontFamily = "Fira Code"; + + }''; + description = '' + Attribute set of client options for xtermjs. + <link xlink:href="https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/"/> + ''; + }; + + terminalType = mkOption { + type = types.str; + default = "xterm-256color"; + description = "Terminal type to report."; + }; + + checkOrigin = mkOption { + type = types.bool; + default = false; + description = "Whether to allow a websocket connection from a different origin."; + }; + + maxClients = mkOption { + type = types.int; + default = 0; + description = "Maximum clients to support (0, no limit)"; + }; + + indexFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Custom index.html path"; + }; + + enableIPv6 = mkOption { + type = types.bool; + default = false; + description = "Whether or not to enable IPv6 support."; + }; + + enableSSL = mkOption { + type = types.bool; + default = false; + description = "Whether or not to enable SSL (https) support."; + }; + + certFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "SSL certificate file path."; + }; + + keyFile = mkOption { + type = types.nullOr types.path; + default = null; + apply = value: if value == null then null else toString value; + description = '' + SSL key file path. + For insecurely putting the keyFile in the globally readable store use + <literal>pkgs.writeText "ttydKeyFile" "SSLKEY"</literal>. + ''; + }; + + caFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "SSL CA file path for client certificate verification."; + }; + + logLevel = mkOption { + type = types.int; + default = 7; + description = "Set log level."; + }; + }; + }; + + ###### implementation + + config = mkIf cfg.enable { + + assertions = + [ { assertion = cfg.enableSSL + -> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null; + message = "SSL is enabled for ttyd, but no certFile, keyFile or caFile has been specefied."; } + { assertion = ! (cfg.interface != null && cfg.socket != null); + message = "Cannot set both interface and socket for ttyd."; } + { assertion = (cfg.username != null) == (cfg.passwordFile != null); + message = "Need to set both username and passwordFile for ttyd"; } + ]; + + systemd.services.ttyd = { + description = "ttyd Web Server Daemon"; + + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + # Runs login which needs to be run as root + # login: Cannot possibly work without effective root + User = "root"; + }; + + script = if cfg.passwordFile != null then '' + PASSWORD=$(cat ${escapeShellArg cfg.passwordFile}) + ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ + --credential ${escapeShellArg cfg.username}:"$PASSWORD" \ + ${pkgs.shadow}/bin/login + '' + else '' + ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ + ${pkgs.shadow}/bin/login + ''; + }; + }; +} diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix new file mode 100644 index 00000000000..b2eecdbb53e --- /dev/null +++ b/nixos/modules/services/web-servers/unit/default.nix @@ -0,0 +1,155 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.unit; + + configFile = pkgs.writeText "unit.json" cfg.config; + +in { + options = { + services.unit = { + enable = mkEnableOption "Unit App Server"; + package = mkOption { + type = types.package; + default = pkgs.unit; + defaultText = literalExpression "pkgs.unit"; + description = "Unit package to use."; + }; + user = mkOption { + type = types.str; + default = "unit"; + description = "User account under which unit runs."; + }; + group = mkOption { + type = types.str; + default = "unit"; + description = "Group account under which unit runs."; + }; + stateDir = mkOption { + type = types.path; + default = "/var/spool/unit"; + description = "Unit data directory."; + }; + logDir = mkOption { + type = types.path; + default = "/var/log/unit"; + description = "Unit log directory."; + }; + config = mkOption { + type = types.str; + default = '' + { + "listeners": {}, + "applications": {} + } + ''; + example = '' + { + "listeners": { + "*:8300": { + "application": "example-php-72" + } + }, + "applications": { + "example-php-72": { + "type": "php 7.2", + "processes": 4, + "user": "nginx", + "group": "nginx", + "root": "/var/www", + "index": "index.php", + "options": { + "file": "/etc/php.d/default.ini", + "admin": { + "max_execution_time": "30", + "max_input_time": "30", + "display_errors": "off", + "display_startup_errors": "off", + "open_basedir": "/dev/urandom:/proc/cpuinfo:/proc/meminfo:/etc/ssl/certs:/var/www", + "disable_functions": "exec,passthru,shell_exec,system" + } + } + } + } + } + ''; + description = "Unit configuration in JSON format. More details here https://unit.nginx.org/configuration"; + }; + }; + }; + + config = mkIf cfg.enable { + + environment.systemPackages = [ cfg.package ]; + + systemd.tmpfiles.rules = [ + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.logDir}' 0750 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.unit = { + description = "Unit App Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + preStart = '' + [ ! -e '${cfg.stateDir}/conf.json' ] || rm -f '${cfg.stateDir}/conf.json' + ''; + postStart = '' + ${pkgs.curl}/bin/curl -X PUT --data-binary '@${configFile}' --unix-socket '/run/unit/control.unit.sock' 'http://localhost/config' + ''; + serviceConfig = { + Type = "forking"; + PIDFile = "/run/unit/unit.pid"; + ExecStart = '' + ${cfg.package}/bin/unitd --control 'unix:/run/unit/control.unit.sock' --pid '/run/unit/unit.pid' \ + --log '${cfg.logDir}/unit.log' --state '${cfg.stateDir}' --tmp '/tmp' \ + --user ${cfg.user} --group ${cfg.group} + ''; + ExecStop = '' + ${pkgs.curl}/bin/curl -X DELETE --unix-socket '/run/unit/control.unit.sock' 'http://localhost/config' + ''; + # Runtime directory and mode + RuntimeDirectory = "unit"; + RuntimeDirectoryMode = "0750"; + # Access write directories + ReadWritePaths = [ cfg.stateDir cfg.logDir ]; + # Security + NoNewPrivileges = true; + # Sandboxing + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateUsers = false; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + PrivateMounts = true; + # System Call Filtering + SystemCallArchitectures = "native"; + }; + }; + + users.users = optionalAttrs (cfg.user == "unit") { + unit = { + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = optionalAttrs (cfg.group == "unit") { + unit = { }; + }; + + }; +} diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix new file mode 100644 index 00000000000..1b3474f2f52 --- /dev/null +++ b/nixos/modules/services/web-servers/uwsgi.nix @@ -0,0 +1,229 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.uwsgi; + + isEmperor = cfg.instance.type == "emperor"; + + imperialPowers = + [ + # spawn other user processes + "CAP_SETUID" "CAP_SETGID" + "CAP_SYS_CHROOT" + # transfer capabilities + "CAP_SETPCAP" + # create other user sockets + "CAP_CHOWN" + ]; + + buildCfg = name: c: + let + plugins' = + if any (n: !any (m: m == n) cfg.plugins) (c.plugins or []) + then throw "`plugins` attribute in uWSGI configuration contains plugins not in config.services.uwsgi.plugins" + else c.plugins or cfg.plugins; + plugins = unique plugins'; + + hasPython = v: filter (n: n == "python${v}") plugins != []; + hasPython2 = hasPython "2"; + hasPython3 = hasPython "3"; + + python = + if hasPython2 && hasPython3 then + throw "`plugins` attribute in uWSGI configuration shouldn't contain both python2 and python3" + else if hasPython2 then cfg.package.python2 + else if hasPython3 then cfg.package.python3 + else null; + + pythonEnv = python.withPackages (c.pythonPackages or (self: [])); + + uwsgiCfg = { + uwsgi = + if c.type == "normal" + then { + inherit plugins; + } // removeAttrs c [ "type" "pythonPackages" ] + // optionalAttrs (python != null) { + pyhome = "${pythonEnv}"; + env = + # Argh, uwsgi expects list of key-values there instead of a dictionary. + let envs = partition (hasPrefix "PATH=") (c.env or []); + oldPaths = map (x: substring (stringLength "PATH=") (stringLength x) x) envs.right; + paths = oldPaths ++ [ "${pythonEnv}/bin" ]; + in [ "PATH=${concatStringsSep ":" paths}" ] ++ envs.wrong; + } + else if isEmperor + then { + emperor = if builtins.typeOf c.vassals != "set" then c.vassals + else pkgs.buildEnv { + name = "vassals"; + paths = mapAttrsToList buildCfg c.vassals; + }; + } // removeAttrs c [ "type" "vassals" ] + else throw "`type` attribute in uWSGI configuration should be either 'normal' or 'emperor'"; + }; + + in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg); + +in { + + options = { + services.uwsgi = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Enable uWSGI"; + }; + + runDir = mkOption { + type = types.path; + default = "/run/uwsgi"; + description = "Where uWSGI communication sockets can live"; + }; + + package = mkOption { + type = types.package; + internal = true; + }; + + instance = mkOption { + type = with types; let + valueType = nullOr (oneOf [ + bool + int + float + str + (lazyAttrsOf valueType) + (listOf valueType) + (mkOptionType { + name = "function"; + description = "function"; + check = x: isFunction x; + merge = mergeOneOption; + }) + ]) // { + description = "Json value or lambda"; + emptyValue.value = {}; + }; + in valueType; + default = { + type = "normal"; + }; + example = literalExpression '' + { + type = "emperor"; + vassals = { + moin = { + type = "normal"; + pythonPackages = self: with self; [ moinmoin ]; + socket = "''${config.services.uwsgi.runDir}/uwsgi.sock"; + }; + }; + } + ''; + description = '' + uWSGI configuration. It awaits an attribute <literal>type</literal> inside which can be either + <literal>normal</literal> or <literal>emperor</literal>. + + For <literal>normal</literal> mode you can specify <literal>pythonPackages</literal> as a function + from libraries set into a list of libraries. <literal>pythonpath</literal> will be set accordingly. + + For <literal>emperor</literal> mode, you should use <literal>vassals</literal> attribute + which should be either a set of names and configurations or a path to a directory. + + Other attributes will be used in configuration file as-is. Notice that you can redefine + <literal>plugins</literal> setting here. + ''; + }; + + plugins = mkOption { + type = types.listOf types.str; + default = []; + description = "Plugins used with uWSGI"; + }; + + user = mkOption { + type = types.str; + default = "uwsgi"; + description = "User account under which uWSGI runs."; + }; + + group = mkOption { + type = types.str; + default = "uwsgi"; + description = "Group account under which uWSGI runs."; + }; + + capabilities = mkOption { + type = types.listOf types.str; + apply = caps: caps ++ optionals isEmperor imperialPowers; + default = [ ]; + example = literalExpression '' + [ + "CAP_NET_BIND_SERVICE" # bind on ports <1024 + "CAP_NET_RAW" # open raw sockets + ] + ''; + description = '' + Grant capabilities to the uWSGI instance. See the + <literal>capabilities(7)</literal> for available values. + <note> + <para> + uWSGI runs as an unprivileged user (even as Emperor) with the minimal + capabilities required. This option can be used to add fine-grained + permissions without running the service as root. + </para> + <para> + When in Emperor mode, any capability to be inherited by a vassal must + be specified again in the vassal configuration using <literal>cap</literal>. + See the uWSGI <link + xlink:href="https://uwsgi-docs.readthedocs.io/en/latest/Capabilities.html">docs</link> + for more information. + </para> + </note> + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") '' + d ${cfg.runDir} 775 ${cfg.user} ${cfg.group} + ''; + + systemd.services.uwsgi = { + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + Type = "notify"; + ExecStart = "${cfg.package}/bin/uwsgi --json ${buildCfg "server" cfg.instance}/server.json"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; + NotifyAccess = "main"; + KillSignal = "SIGQUIT"; + AmbientCapabilities = cfg.capabilities; + CapabilityBoundingSet = cfg.capabilities; + RuntimeDirectory = mkIf (cfg.runDir == "/run/uwsgi") "uwsgi"; + }; + }; + + users.users = optionalAttrs (cfg.user == "uwsgi") { + uwsgi = { + group = cfg.group; + uid = config.ids.uids.uwsgi; + }; + }; + + users.groups = optionalAttrs (cfg.group == "uwsgi") { + uwsgi.gid = config.ids.gids.uwsgi; + }; + + services.uwsgi.package = pkgs.uwsgi.override { + plugins = unique cfg.plugins; + }; + }; +} diff --git a/nixos/modules/services/web-servers/varnish/default.nix b/nixos/modules/services/web-servers/varnish/default.nix new file mode 100644 index 00000000000..fe817313a99 --- /dev/null +++ b/nixos/modules/services/web-servers/varnish/default.nix @@ -0,0 +1,115 @@ +{ config, lib, pkgs, ...}: + +with lib; + +let + cfg = config.services.varnish; + + commandLine = "-f ${pkgs.writeText "default.vcl" cfg.config}" + + optionalString (cfg.extraModules != []) " -p vmod_path='${makeSearchPathOutput "lib" "lib/varnish/vmods" ([cfg.package] ++ cfg.extraModules)}' -r vmod_path"; +in +{ + options = { + services.varnish = { + enable = mkEnableOption "Varnish Server"; + + enableConfigCheck = mkEnableOption "checking the config during build time" // { default = true; }; + + package = mkOption { + type = types.package; + default = pkgs.varnish; + defaultText = literalExpression "pkgs.varnish"; + description = '' + The package to use + ''; + }; + + http_address = mkOption { + type = types.str; + default = "*:6081"; + description = " + HTTP listen address and port. + "; + }; + + config = mkOption { + type = types.lines; + description = " + Verbatim default.vcl configuration. + "; + }; + + stateDir = mkOption { + type = types.path; + default = "/var/spool/varnish/${config.networking.hostName}"; + defaultText = literalExpression ''"/var/spool/varnish/''${config.networking.hostName}"''; + description = " + Directory holding all state for Varnish to run. + "; + }; + + extraModules = mkOption { + type = types.listOf types.package; + default = []; + example = literalExpression "[ pkgs.varnishPackages.geoip ]"; + description = " + Varnish modules (except 'std'). + "; + }; + + extraCommandLine = mkOption { + type = types.str; + default = ""; + example = "-s malloc,256M"; + description = " + Command line switches for varnishd (run 'varnishd -?' to get list of options) + "; + }; + }; + + }; + + config = mkIf cfg.enable { + + systemd.services.varnish = { + description = "Varnish"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + preStart = '' + mkdir -p ${cfg.stateDir} + chown -R varnish:varnish ${cfg.stateDir} + ''; + postStop = '' + rm -rf ${cfg.stateDir} + ''; + serviceConfig = { + Type = "simple"; + PermissionsStartOnly = true; + ExecStart = "${cfg.package}/sbin/varnishd -a ${cfg.http_address} -n ${cfg.stateDir} -F ${cfg.extraCommandLine} ${commandLine}"; + Restart = "always"; + RestartSec = "5s"; + User = "varnish"; + Group = "varnish"; + AmbientCapabilities = "cap_net_bind_service"; + NoNewPrivileges = true; + LimitNOFILE = 131072; + }; + }; + + environment.systemPackages = [ cfg.package ]; + + # check .vcl syntax at compile time (e.g. before nixops deployment) + system.extraDependencies = mkIf cfg.enableConfigCheck [ + (pkgs.runCommand "check-varnish-syntax" {} '' + ${cfg.package}/bin/varnishd -C ${commandLine} 2> $out || (cat $out; exit 1) + '') + ]; + + users.users.varnish = { + group = "varnish"; + uid = config.ids.uids.varnish; + }; + + users.groups.varnish.gid = config.ids.uids.varnish; + }; +} diff --git a/nixos/modules/services/web-servers/zope2.nix b/nixos/modules/services/web-servers/zope2.nix new file mode 100644 index 00000000000..92210916022 --- /dev/null +++ b/nixos/modules/services/web-servers/zope2.nix @@ -0,0 +1,262 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.zope2; + + zope2Opts = { name, ... }: { + options = { + + name = mkOption { + default = "${name}"; + type = types.str; + description = "The name of the zope2 instance. If undefined, the name of the attribute set will be used."; + }; + + threads = mkOption { + default = 2; + type = types.int; + description = "Specify the number of threads that Zope's ZServer web server will use to service requests. "; + }; + + http_address = mkOption { + default = "localhost:8080"; + type = types.str; + description = "Give a port and address for the HTTP server."; + }; + + user = mkOption { + default = "zope2"; + type = types.str; + description = "The name of the effective user for the Zope process."; + }; + + clientHome = mkOption { + default = "/var/lib/zope2/${name}"; + type = types.path; + description = "Home directory of zope2 instance."; + }; + extra = mkOption { + default = + '' + <zodb_db main> + mount-point / + cache-size 30000 + <blobstorage> + blob-dir /var/lib/zope2/${name}/blobstorage + <filestorage> + path /var/lib/zope2/${name}/filestorage/Data.fs + </filestorage> + </blobstorage> + </zodb_db> + ''; + type = types.lines; + description = "Extra zope.conf"; + }; + + packages = mkOption { + type = types.listOf types.package; + description = "The list of packages you want to make available to the zope2 instance."; + }; + + }; + }; + +in + +{ + + ###### interface + + options = { + + services.zope2.instances = mkOption { + default = {}; + type = with types; attrsOf (submodule zope2Opts); + example = literalExpression '' + { + plone01 = { + http_address = "127.0.0.1:8080"; + extra = + ''' + <zodb_db main> + mount-point / + cache-size 30000 + <blobstorage> + blob-dir /var/lib/zope2/plone01/blobstorage + <filestorage> + path /var/lib/zope2/plone01/filestorage/Data.fs + </filestorage> + </blobstorage> + </zodb_db> + '''; + }; + } + ''; + description = "zope2 instances to be created automaticaly by the system."; + }; + }; + + ###### implementation + + config = mkIf (cfg.instances != {}) { + + users.users.zope2 = { + isSystemUser = true; + group = "zope2"; + }; + users.groups.zope2 = {}; + + systemd.services = + let + + createZope2Instance = opts: name: + let + interpreter = pkgs.writeScript "interpreter" + '' + import sys + + _interactive = True + if len(sys.argv) > 1: + _options, _args = __import__("getopt").getopt(sys.argv[1:], 'ic:m:') + _interactive = False + for (_opt, _val) in _options: + if _opt == '-i': + _interactive = True + elif _opt == '-c': + exec _val + elif _opt == '-m': + sys.argv[1:] = _args + _args = [] + __import__("runpy").run_module( + _val, {}, "__main__", alter_sys=True) + + if _args: + sys.argv[:] = _args + __file__ = _args[0] + del _options, _args + execfile(__file__) + + if _interactive: + del _interactive + __import__("code").interact(banner="", local=globals()) + ''; + env = pkgs.buildEnv { + name = "zope2-${name}-env"; + paths = [ + pkgs.python27 + pkgs.python27Packages.recursivePthLoader + pkgs.python27Packages."plone.recipe.zope2instance" + ] ++ attrValues pkgs.python27.modules + ++ opts.packages; + postBuild = + '' + echo "#!$out/bin/python" > $out/bin/interpreter + cat ${interpreter} >> $out/bin/interpreter + ''; + }; + conf = pkgs.writeText "zope2-${name}-conf" + '' + %define INSTANCEHOME ${env} + instancehome $INSTANCEHOME + %define CLIENTHOME ${opts.clientHome}/${opts.name} + clienthome $CLIENTHOME + + debug-mode off + security-policy-implementation C + verbose-security off + default-zpublisher-encoding utf-8 + zserver-threads ${toString opts.threads} + effective-user ${opts.user} + + pid-filename ${opts.clientHome}/${opts.name}/pid + lock-filename ${opts.clientHome}/${opts.name}/lock + python-check-interval 1000 + enable-product-installation off + + <environment> + zope_i18n_compile_mo_files false + </environment> + + <eventlog> + level INFO + <logfile> + path /var/log/zope2/${name}.log + level INFO + </logfile> + </eventlog> + + <logger access> + level WARN + <logfile> + path /var/log/zope2/${name}-Z2.log + format %(message)s + </logfile> + </logger> + + <http-server> + address ${opts.http_address} + </http-server> + + <zodb_db temporary> + <temporarystorage> + name temporary storage for sessioning + </temporarystorage> + mount-point /temp_folder + container-class Products.TemporaryFolder.TemporaryContainer + </zodb_db> + + ${opts.extra} + ''; + ctlScript = pkgs.writeScript "zope2-${name}-ctl-script" + '' + #!${env}/bin/python + + import sys + import plone.recipe.zope2instance.ctl + + if __name__ == '__main__': + sys.exit(plone.recipe.zope2instance.ctl.main( + ["-C", "${conf}"] + + sys.argv[1:])) + ''; + + ctl = pkgs.writeScript "zope2-${name}-ctl" + '' + #!${pkgs.bash}/bin/bash -e + export PYTHONHOME=${env} + exec ${ctlScript} "$@" + ''; + in { + #description = "${name} instance"; + after = [ "network.target" ]; # with RelStorage also add "postgresql.service" + wantedBy = [ "multi-user.target" ]; + path = opts.packages; + preStart = + '' + mkdir -p /var/log/zope2/ + touch /var/log/zope2/${name}.log + touch /var/log/zope2/${name}-Z2.log + chown ${opts.user} /var/log/zope2/${name}.log + chown ${opts.user} /var/log/zope2/${name}-Z2.log + + mkdir -p ${opts.clientHome}/filestorage ${opts.clientHome}/blobstorage + mkdir -p ${opts.clientHome}/${opts.name} + chown ${opts.user} ${opts.clientHome} -R + + ${ctl} adduser admin admin + ''; + + serviceConfig.Type = "forking"; + serviceConfig.ExecStart = "${ctl} start"; + serviceConfig.ExecStop = "${ctl} stop"; + serviceConfig.ExecReload = "${ctl} restart"; + }; + + in listToAttrs (map (name: { name = "zope2-${name}"; value = createZope2Instance (builtins.getAttr name cfg.instances) name; }) (builtins.attrNames cfg.instances)); + + }; + +} |