uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead

https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948
this can do it nicely.

Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
Anton Arapov 2021-04-03 12:58:10 +02:00 committed by Alan Daniels
commit 56de2bcd43
30691 changed files with 3076956 additions and 0 deletions

View file

@ -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"
];
};
};
};
}

View file

@ -0,0 +1,834 @@
{ 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;
settings.httpd = {
files = "${cfg.logDir}/*.log";
su = "${cfg.user} ${cfg.group}";
frequency = "daily";
rotate = 28;
sharedscripts = true;
compress = true;
delaycompress = true;
postrotate = "systemctl reload httpd.service > /dev/null 2>/dev/null || true";
};
};
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";
};
};
};
}

View file

@ -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.
'';
};
};
}

View file

@ -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.&lt;name&gt;.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);
};
}

View file

@ -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;
};
}

View file

@ -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>.
'';
};
};
}

View file

@ -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";
};
};
};
}

View file

@ -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 { };
};
}

View file

@ -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 = {};
};
}

View file

@ -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; [ Madouura ];
}

View file

@ -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/*

View file

@ -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" ];
};
};
}

View file

@ -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
'';
};
}

View file

@ -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}"
)
}
'';
};
}

View file

@ -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;
};
}

View file

@ -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}"
)
}
'';
};
}

View file

@ -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 ];
}

View file

@ -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;
};
}

View file

@ -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";
};
};
};
}

File diff suppressed because it is too large Load diff

View file

@ -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; [ ];
}

View file

@ -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 &lt;filename&gt; &lt;username&gt;</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.
'';
};
};
}

View file

@ -0,0 +1,297 @@
# 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 = [ "backlog=1024" "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/
'';
};
reuseport = mkOption {
type = types.bool;
default = false;
description = ''
Create an individual listening socket .
It is required to specify only once on one of the hosts.
'';
};
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 &lt;filename&gt; &lt;username&gt;</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";
};
};
}

View file

@ -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;
};
}

View file

@ -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";
};
};
});
}

View file

@ -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";
};
};
};
}

View file

@ -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 = { };
};
}

View file

@ -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 = { };
};
}

View file

@ -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"
]
}
]
}

View file

@ -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"
}
]
}
}

View file

@ -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
'';
};
};
}

View file

@ -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 = { };
};
};
}

View file

@ -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;
};
};
}

View file

@ -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;
};
}

View file

@ -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));
};
}