push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.agate;
in
{
options = {
services.agate = {
enable = mkEnableOption "Agate Server";
package = mkPackageOption pkgs "agate" { };
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 occurrences 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" ];
wants = [ "network-online.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,990 @@
{
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 {
apxs2Support = true;
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);
vhostCertNames = 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}
${optionalString (adminAddr != null) "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}
${optionalString (adminAddr != null) "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 lib;
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 = mkPackageOption pkgs "apacheHttpd" { };
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` is overridden.
'';
};
extraModules = mkOption {
type = types.listOf types.unspecified;
default = [ ];
example = literalExpression ''
[
"proxy_connect"
{ name = "jk"; path = "''${pkgs.apacheHttpdPackages.mod_jk}/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
{var}`name` and {var}`path` of the
module.
'';
};
adminAddr = mkOption {
type = types.nullOr types.str;
example = "admin@example.org";
default = null;
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 <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
{file}`access.log` and
{file}`error.log`, namely suffixed by the
{option}`hostName` 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
`root` add the following configuration:
```
systemd.services.httpd.serviceConfig.User = lib.mkForce "root";
```
'';
};
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 = mkEnableOption "the mod_auth_mellon module";
enablePHP = mkEnableOption "the PHP module";
phpPackage = mkPackageOption pkgs "php" { };
enablePerl = mkEnableOption "the Perl module (mod_perl)";
phpOptions = mkOption {
type = types.lines;
default = "";
example = ''
date.timezone = "CET"
'';
description = ''
Options appended to the PHP configuration file {file}`php.ini`.
'';
};
mpm = mkOption {
type = types.enum [
"event"
"prefork"
"worker"
];
default = "event";
example = "worker";
description = ''
Multi-processing module to be used by Apache. Available
modules are `prefork` (handles each
request in a separate child process), `worker`
(hybrid approach that starts a number of child processes
each running a number of threads) and `event`
(the default; a recent variant of `worker`
that handles persistent connections more efficiently).
'';
};
maxClients = mkOption {
type = types.ints.positive;
default = 150;
example = 8;
description = "Maximum number of httpd processes (prefork)";
};
maxRequestsPerChild = mkOption {
type = types.ints.unsigned;
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.
'';
}
{
assertion = cfg.enablePHP -> php.ztsSupport;
message = ''
The php package provided by `services.httpd.phpPackage` is not built with zts support. Please
ensure the php has zts support by settings `services.httpd.phpPackage = php.override { ztsSupport = true; }`
'';
}
]
++ map (
name:
mkCertOwnershipAssertion {
cert = config.security.acme.certs.${name};
groups = config.users.groups;
services = [
config.systemd.services.httpd
]
++ lib.optional (vhostCertNames != [ ]) config.systemd.services.httpd-config-reload;
}
) vhostCertNames;
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-${certName}.service" ]) vhostCertNames);
after = [
"network.target"
]
# Ensure httpd runs with baseline certificates in place.
++ map (certName: "acme-${certName}.service") vhostCertNames;
# Ensure httpd runs (with current config) before the actual ACME jobs run
before = map (certName: "acme-order-renew-${certName}.service") vhostCertNames;
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-order-renew-$cert.service to signify the successful updating
# of certs end-to-end.
systemd.services.httpd-config-reload =
let
sslServices = map (certName: "acme-order-renew-${certName}.service") vhostCertNames;
in
mkIf (vhostCertNames != [ ]) {
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.
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"
) vhostCertNames;
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,59 @@
{
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 <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 <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 <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,321 @@
{
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}
This option overrides `addSSL`, `forceSSL` and `onlySSL`.
If you only want to set the addresses manually and not the ports, take a look at `listenAddresses`.
:::
'';
};
listenAddresses = mkOption {
type = with types; nonEmptyListOf str;
description = ''
Listen addresses for this virtual host.
Compared to `listen` this only sets the addresses
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
`listen` 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 `listen` 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
`listen` 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`.
'';
};
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
[rate limit](https://letsencrypt.org/docs/rate-limits).
Alternately, you can generate a certificate through {option}`enableACME`.
*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 [](#opt-security.acme.certs).*
'';
};
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. *However, if you use the prefork mpm, there will
be severe restrictions.* Refer to <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}
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 `locations.<name>.alias` option.
:::
'';
};
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 {file}`~/public_html` as
`/~«username»`.
'';
};
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 <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 <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,484 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.caddy;
certs = config.security.acme.certs;
virtualHosts = attrValues cfg.virtualHosts;
acmeEnabledVhosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
vhostCertNames = unique (map (hostOpts: hostOpts.useACMEHost) acmeEnabledVhosts);
mkVHostConf =
hostOpts:
let
sslCertDir = certs.${hostOpts.useACMEHost}.directory;
in
''
${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
${optionalString (
hostOpts.listenAddresses != [ ]
) "bind ${concatStringsSep " " hostOpts.listenAddresses}"}
${optionalString (
hostOpts.useACMEHost != null
) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
${optionalString (hostOpts.logFormat != null) ''
log {
${hostOpts.logFormat}
}
''}
${hostOpts.extraConfig}
}
'';
settingsFormat = pkgs.formats.json { };
configFile =
if cfg.settings != { } then
settingsFormat.generate "caddy.json" cfg.settings
else
let
Caddyfile = pkgs.writeTextDir "Caddyfile" ''
{
${cfg.globalConfig}
}
${cfg.extraConfig}
${concatMapStringsSep "\n" mkVHostConf virtualHosts}
'';
Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { } ''
mkdir -p $out
cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
${lib.getExe cfg.package} fmt --overwrite $out/Caddyfile
'';
in
"${
if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile
}/Caddyfile";
etcConfigFile = "caddy/caddy_config";
configPath = "/etc/${etcConfigFile}";
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
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}
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.
:::
'';
};
group = mkOption {
default = "caddy";
type = types.str;
description = ''
Group under which caddy runs.
::: {.note}
If left as the default value this group will automatically be created
on system activation, otherwise you are responsible for
ensuring the group exists before the Caddy service starts.
:::
'';
};
package = mkPackageOption pkgs "caddy" { };
dataDir = mkOption {
type = types.path;
default = "/var/lib/caddy";
description = ''
The data directory for caddy.
::: {.note}
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.
Caddy v2 replaced `CADDYPATH` with XDG directories.
See <https://caddyserver.com/docs/conventions#file-locations>.
:::
'';
};
logDir = mkOption {
type = types.path;
default = "/var/log/caddy";
description = ''
Directory for storing Caddy access logs.
::: {.note}
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.
:::
'';
};
logFormat = mkOption {
type = types.lines;
default = ''
level ERROR
'';
example = literalExpression ''
mkForce "level INFO";
'';
description = ''
Configuration for the default logger. See
<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.
The configuration file is exposed at {file}`${configPath}`.
'';
};
adapter = mkOption {
default =
if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then
"caddyfile"
else
null;
defaultText = literalExpression ''
if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then "caddyfile" else null
'';
example = literalExpression "nginx";
type = with types; nullOr str;
description = ''
Name of the config adapter to use.
See <https://caddyserver.com/docs/config-adapters>
for the full list.
If `null` is specified, the `--adapter` argument is omitted when
starting or restarting Caddy. Notably, this allows specification of a
configuration file in Caddy's native JSON format, as long as the
filename does not start with `Caddyfile` (in which case the `caddyfile`
adapter is implicitly enabled). See
<https://caddyserver.com/docs/command-line#caddy-run> for details.
::: {.note}
Any value other than `null` or `caddyfile` is only valid when providing
your own `configFile`.
:::
'';
};
resume = mkOption {
default = false;
type = types.bool;
description = ''
Use saved config, if any (and prefer over any specified configuration passed with `--config`).
'';
};
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 `Caddyfile`.
Refer to <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 `Caddyfile`.
'';
};
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 = null;
example = "https://acme-v02.api.letsencrypt.org/directory";
type = with types; nullOr str;
description = ''
::: {.note}
Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca)
in the global options block of the resulting Caddyfile.
:::
The URL to the ACME CA's directory. It is strongly recommended to set
this to `https://acme-staging-v02.api.letsencrypt.org/directory` for
Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/)
while testing or in development.
Value `null` should be prefered for production setups,
as it omits the `acme_ca` option to enable
[automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback).
'';
};
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.
'';
};
enableReload = mkOption {
default = true;
type = types.bool;
description = ''
Reload Caddy instead of restarting it when configuration file changes.
Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin)
to not be turned off.
If you enable this option, consider setting [`grace_period`](https://caddyserver.com/docs/caddyfile/options#grace-period)
to a non-infinite value in {option}`services.caddy.globalConfig`
to prevent Caddy waiting for active connections to finish,
which could delay the reload essentially indefinitely.
'';
};
settings = mkOption {
type = settingsFormat.type;
default = { };
description = ''
Structured configuration for Caddy to generate a Caddy JSON configuration file.
See <https://caddyserver.com/docs/json/> for available options.
::: {.warning}
Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream.
There are only very few exception to this.
Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or
{option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead.
:::
::: {.note}
Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified.
:::
'';
};
environmentFile = mkOption {
type = with types; nullOr path;
default = null;
example = "/run/secrets/caddy.env";
description = ''
Environment file as defined in {manpage}`systemd.exec(5)`.
You can use environment variables to pass secrets to the service without adding
them to the world-redable nix store.
```
# in configuration.nix
services.caddy.environmentFile = "/run/secrets/caddy.env";
services.caddy.globalConfig = '''
{
acme_ca https://acme.zerossl.com/v2/DV90
acme_eab {
key_id {$EAB_KEY_ID}
mac_key {$EAB_MAC_KEY}
}
}
''';
```
```
# in /run/secrets/caddy.env
EAB_KEY_ID=secret
EAB_MAC_KEY=secret
```
Find more examples
[here](https://caddyserver.com/docs/caddyfile/concepts#environment-variables)
'';
};
};
# implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null;
message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`";
}
]
++ map (
name:
mkCertOwnershipAssertion {
cert = certs.${name};
groups = config.users.groups;
services = [ config.systemd.services.caddy ];
}
) vhostCertNames;
services.caddy.globalConfig = ''
${optionalString (cfg.email != null) "email ${cfg.email}"}
${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
log {
${cfg.logFormat}
}
'';
# https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes
boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
boot.kernel.sysctl."net.core.wmem_max" = mkDefault 2500000;
systemd.packages = [ cfg.package ];
systemd.services.caddy = {
wants = map (certName: "acme-${certName}.service") vhostCertNames;
after = map (certName: "acme-${certName}.service") vhostCertNames;
wantedBy = [ "multi-user.target" ];
startLimitIntervalSec = 14400;
startLimitBurst = 10;
reloadTriggers = optional cfg.enableReload cfg.configFile;
restartTriggers = optional (!cfg.enableReload) cfg.configFile;
serviceConfig =
let
runOptions = ''--config ${configPath} ${
optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"
}'';
in
{
# Override the `ExecStart` line from upstream's systemd unit file by our own:
# 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 = [
""
''${lib.getExe cfg.package} run ${runOptions} ${optionalString cfg.resume "--resume"}''
];
# Validating the configuration before applying it ensures well get a proper error that will be reported when switching to the configuration
ExecReload = [
""
]
++ lib.optional cfg.enableReload "${lib.getExe cfg.package} reload ${runOptions} --force";
User = cfg.user;
Group = cfg.group;
ReadWritePaths = [ cfg.dataDir ];
StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
Restart = "on-failure";
RestartPreventExitStatus = 1;
RestartSec = "5s";
EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile;
# 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
certCfg = map (
certName:
nameValuePair certName {
group = mkDefault cfg.group;
reloadServices = [ "caddy.service" ];
}
) vhostCertNames;
in
listToAttrs certCfg;
environment.etc.${etcConfigFile}.source = cfg.configFile;
};
}

View File

@@ -0,0 +1,88 @@
{ 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.
*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 [](#opt-security.acme.certs).*
'';
};
logFormat = mkOption {
type = types.nullOr types.lines;
default = ''
output file ${cfg.logDir}/access-${lib.replaceStrings [ "/" " " ] [ "_" "_" ] 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
<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 `Caddyfile`.
'';
};
};
}

View File

@@ -0,0 +1,92 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkIf mkOption optional;
inherit (lib.types)
path
bool
listOf
str
port
;
cfg = config.services.darkhttpd;
args = lib.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 = {
enable = lib.mkEnableOption "DarkHTTPd web server";
port = mkOption {
default = 80;
type = 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,191 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
forEachInstance =
f:
flip mapAttrs' config.services.fcgiwrap.instances (
name: cfg: nameValuePair "fcgiwrap-${name}" (f cfg)
);
in
{
imports =
forEach
[
"enable"
"user"
"group"
"socketType"
"socketAddress"
"preforkProcesses"
]
(
attr:
mkRemovedOptionModule [ "services" "fcgiwrap" attr ] ''
The global shared fcgiwrap instance is no longer supported due to
security issues.
Isolated instances should instead be configured through
`services.fcgiwrap.instances.*'.
''
);
options.services.fcgiwrap.instances = mkOption {
description = "Configuration for fcgiwrap instances.";
default = { };
type = types.attrsOf (
types.submodule (
{ config, ... }:
{
options = {
process.prefork = mkOption {
type = types.ints.positive;
default = 1;
description = "Number of processes to prefork.";
};
process.user = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
User as which this instance of fcgiwrap will be run.
Set to `null` (the default) to use a dynamically allocated user.
'';
};
process.group = mkOption {
type = types.nullOr types.str;
default = null;
description = "Group as which this instance of fcgiwrap will be run.";
};
socket.type = mkOption {
type = types.enum [
"unix"
"tcp"
"tcp6"
];
default = "unix";
description = "Socket type: 'unix', 'tcp' or 'tcp6'.";
};
socket.address = mkOption {
type = types.str;
default = "/run/fcgiwrap-${config._module.args.name}.sock";
example = "1.2.3.4:5678";
description = ''
Socket address.
In case of a UNIX socket, this should be its filesystem path.
'';
};
socket.user = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
User to be set as owner of the UNIX socket.
'';
};
socket.group = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Group to be set as owner of the UNIX socket.
'';
};
socket.mode = mkOption {
type = types.nullOr types.str;
default = if config.socket.type == "unix" then "0600" else null;
defaultText = literalExpression ''
if config.socket.type == "unix" then "0600" else null
'';
description = ''
Mode to be set on the UNIX socket.
Defaults to private to the socket's owner.
'';
};
};
}
)
);
};
config = {
assertions = concatLists (
mapAttrsToList (name: cfg: [
{
assertion = cfg.socket.type == "unix" -> cfg.socket.user != null;
message = "Socket owner is required for the UNIX socket type.";
}
{
assertion = cfg.socket.type == "unix" -> cfg.socket.group != null;
message = "Socket owner is required for the UNIX socket type.";
}
{
assertion = cfg.socket.user != null -> cfg.socket.type == "unix";
message = "Socket owner can only be set for the UNIX socket type.";
}
{
assertion = cfg.socket.group != null -> cfg.socket.type == "unix";
message = "Socket owner can only be set for the UNIX socket type.";
}
{
assertion = cfg.socket.mode != null -> cfg.socket.type == "unix";
message = "Socket mode can only be set for the UNIX socket type.";
}
]) config.services.fcgiwrap.instances
);
systemd.services = forEachInstance (cfg: {
after = [ "nss-user-lookup.target" ];
wantedBy = optional (cfg.socket.type != "unix") "multi-user.target";
serviceConfig = {
ExecStart = ''
${pkgs.fcgiwrap}/sbin/fcgiwrap ${
cli.toGNUCommandLineShell { } (
{
c = cfg.process.prefork;
}
// (optionalAttrs (cfg.socket.type != "unix") {
s = "${cfg.socket.type}:${cfg.socket.address}";
})
)
}
'';
}
// (
if cfg.process.user != null then
{
User = cfg.process.user;
Group = cfg.process.group;
}
else
{
DynamicUser = true;
}
);
});
systemd.sockets = forEachInstance (
cfg:
mkIf (cfg.socket.type == "unix") {
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = cfg.socket.address;
SocketUser = cfg.socket.user;
SocketGroup = cfg.socket.group;
SocketMode = cfg.socket.mode;
};
}
);
};
}

View File

@@ -0,0 +1,87 @@
# Garage {#module-services-garage}
[Garage](https://garagehq.deuxfleurs.fr/)
is an open-source, self-hostable S3 store, simpler than MinIO, for geodistributed stores.
The server setup can be automated using
[services.garage](#opt-services.garage.enable). A
client configured to your local Garage instance is available in
the global environment as `garage-manage`.
## General considerations on upgrades {#module-services-garage-upgrade-scenarios}
Garage provides a cookbook documentation on how to upgrade:
<https://garagehq.deuxfleurs.fr/documentation/cookbook/upgrading/>
::: {.warning}
Garage has two types of upgrades: patch-level upgrades and minor/major version upgrades.
In all cases, you should read the changelog and ideally test the upgrade on a staging cluster.
Checking the health of your cluster can be achieved using `garage-manage repair`.
:::
- **Straightforward upgrades (patch-level upgrades).**
Upgrades must be performed one by one, i.e. for each node, stop it, upgrade it : change [stateVersion](#opt-system.stateVersion) or [services.garage.package](#opt-services.garage.package), restart it if it was not already by switching.
- **Multiple version upgrades.**
Garage do not provide any guarantee on moving more than one major-version forward.
E.g., if you're on `0.9`, you cannot upgrade to `2.0`.
You need to upgrade to `1.2` first.
As long as [stateVersion](#opt-system.stateVersion) is declared properly,
this is enforced automatically. The module will issue a warning to remind the user to upgrade to latest
Garage *after* that deploy.
## Advanced upgrades (minor/major version upgrades) {#module-services-garage-advanced-upgrades}
Here are some baseline instructions to handle advanced upgrades in Garage, when in doubt, please refer to upstream instructions.
- Disable API and web access to Garage.
- Perform `garage-manage repair --all-nodes --yes tables` and `garage-manage repair --all-nodes --yes blocks`.
- Verify the resulting logs and check that data is synced properly between all nodes.
If you have time, do additional checks (`scrub`, `block_refs`, etc.).
- Check if queues are empty by `garage-manage stats` or through monitoring tools.
- Run `systemctl stop garage` to stop the actual Garage version.
- Backup the metadata folder of ALL your nodes, e.g. for a metadata directory (the default one) in `/var/lib/garage/meta`,
you can run `pushd /var/lib/garage; tar -acf meta-v0.7.tar.zst meta/; popd`.
- Run the offline migration: `nix-shell -p garage_1 --run "garage offline-repair --yes"`, this can take some time depending on how many objects are stored in your cluster.
- Bump Garage version in your NixOS configuration, either by changing [stateVersion](#opt-system.stateVersion) or bumping [services.garage.package](#opt-services.garage.package), this should restart Garage automatically.
- Perform `garage-manage repair --all-nodes --yes tables` and `garage-manage repair --all-nodes --yes blocks`.
- Wait for a full table sync to run.
Your upgraded cluster should be in a working state, re-enable API and web access.
## Maintainer information {#module-services-garage-maintainer-info}
As stated in the previous paragraph, we must provide a clean upgrade-path for Garage
since it cannot move more than one major version forward on a single upgrade. This chapter
adds some notes how Garage updates should be rolled out in the future.
This is inspired from how Nextcloud does it.
While patch-level updates are no problem and can be done directly in the
package-expression (and should be backported to supported stable branches after that),
major-releases should be added in a new attribute (e.g. Garage `v3.0.0`
should be available in `nixpkgs` as `pkgs.garage_3`).
To provide simple upgrade paths it's generally useful to backport those as well to stable
branches. As long as the package-default isn't altered, this won't break existing setups.
After that, the versioning-warning in the `garage`-module should be
updated to make sure that the
[package](#opt-services.garage.package)-option selects the latest version
on fresh setups.
If major-releases will be abandoned by upstream, we should check first if those are needed
in NixOS for a safe upgrade-path before removing those. In that case we should keep those
packages, but mark them as insecure in an expression like this (in
`<nixpkgs/pkgs/tools/filesystem/garage/default.nix>`):
```nix
# ...
{
garage_1_2_0 = generic {
version = "1.2.0";
sha256 = "0000000000000000000000000000000000000000000000000000";
eol = true;
};
}
```
Ideally we should make sure that it's possible to jump two NixOS versions forward:
i.e. the warnings and the logic in the module should guard a user to upgrade from a
Garage on e.g. 22.11 to a Garage on 23.11.

View File

@@ -0,0 +1,159 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.garage;
toml = pkgs.formats.toml { };
configFile = toml.generate "garage.toml" cfg.settings;
in
{
meta = {
doc = ./garage.md;
maintainers = with lib.maintainers; [
mjm
cything
];
};
options.services.garage = {
enable = mkEnableOption "Garage Object Storage (S3 compatible)";
extraEnvironment = mkOption {
type = types.attrsOf types.str;
description = "Extra environment variables to pass to the Garage server.";
default = { };
example = {
RUST_BACKTRACE = "yes";
};
};
environmentFile = mkOption {
type = types.nullOr types.path;
description = "File containing environment variables to be passed to the Garage server.";
default = null;
};
logLevel = mkOption {
type = types.enum [
"error"
"warn"
"info"
"debug"
"trace"
];
default = "info";
example = "debug";
description = "Garage log level, see <https://garagehq.deuxfleurs.fr/documentation/quick-start/#launching-the-garage-server> for examples.";
};
settings = mkOption {
type = types.submodule {
freeformType = toml.type;
options = {
metadata_dir = mkOption {
default = "/var/lib/garage/meta";
type = types.path;
description = "The metadata directory, put this on a fast disk (e.g. SSD) if possible.";
};
data_dir = mkOption {
default = "/var/lib/garage/data";
example = [
{
path = "/var/lib/garage/data";
capacity = "2T";
}
];
type = with types; either path (listOf attrs);
description = ''
The directory in which Garage will store the data blocks of objects. This folder can be placed on an HDD.
Since v0.9.0, Garage supports multiple data directories, refer to <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#data_dir> for the exact format.
'';
};
};
};
description = "Garage configuration, see <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/> for reference.";
};
package = mkOption {
type = types.package;
description = "Garage package to use, needs to be set explicitly. If you are upgrading from a major version, please read NixOS and Garage release notes for upgrade instructions.";
};
};
config = mkIf cfg.enable {
environment.etc."garage.toml" = {
source = configFile;
};
# For administration
environment.systemPackages = [
(pkgs.writeScriptBin "garage" ''
# make it so all future variables set are automatically exported as environment variables
set -a
# source the set environmentFile (since systemd EnvironmentFile is supposed to be a minor subset of posix sh parsing) (with shell arg escaping to avoid quoting issues)
[ -f ${lib.escapeShellArg cfg.environmentFile} ] && . ${lib.escapeShellArg cfg.environmentFile}
# exec the program with quoted args (also with shell arg escaping for the program path to avoid quoting issues there)
exec ${lib.escapeShellArg (lib.getExe cfg.package)} "$@"
'')
];
systemd.services.garage = {
description = "Garage Object Storage (S3 compatible)";
after = [
"network.target"
"network-online.target"
];
wants = [
"network.target"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
configFile
]
++ (lib.optional (cfg.environmentFile != null) cfg.environmentFile);
serviceConfig =
let
paths = lib.flatten (
with cfg.settings;
[
metadata_dir
]
# data_dir can either be a string or a list of attrs
# if data_dir is a list, the actual path will in in the `path` attribute of each item
# see https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#data_dir
++ lib.optional (lib.isList data_dir) (map (item: item.path) data_dir)
++ lib.optional (lib.isString data_dir) [ data_dir ]
);
isDefault = lib.hasPrefix "/var/lib/garage";
isDefaultStateDirectory = lib.any isDefault paths;
in
{
ExecStart = "${cfg.package}/bin/garage server";
StateDirectory = lib.mkIf isDefaultStateDirectory "garage";
DynamicUser = lib.mkDefault true;
ProtectHome = true;
NoNewPrivileges = true;
EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
ReadWritePaths = lib.filter (x: !(isDefault x)) (lib.flatten [ paths ]);
# Upstream recommendation https://garagehq.deuxfleurs.fr/documentation/cookbook/systemd/
LimitNOFILE = 42000;
};
environment = {
RUST_LOG = lib.mkDefault "garage=${cfg.logLevel}";
}
// cfg.extraEnvironment;
};
};
}

View File

@@ -0,0 +1,46 @@
{ lib }:
{
tlsRecommendationsOption = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"modern"
"intermediate"
"old"
]
);
default = null;
example = "intermediate";
description = ''
By default, H2O, without prejudice, will use as many TLS versions &
cipher suites as it & the TLS library (OpenSSL) can support. The user is
expected to hone settings for the security of their server. Setting some
constraints is recommended, & if unsure about what TLS settings to use,
this option gives curated TLS settings recommendations from Mozillas
SSL Configuration Generator project (see
<https://ssl-config.mozilla.org>) or read more at Mozillas Wiki (see
<https://wiki.mozilla.org/Security/Server_Side_TLS>).
modern
: Services with clients that support TLS 1.3 & dont need backward
compatibility
intermediate
: General-purpose servers with a variety of clients, recommended for
almost all systems
old
: Compatible with a number of very old clients, & should be used only as
a last resort
The default for all virtual hosts can be set with
services.h2o.defaultTLSRecommendations, but this value can be overridden
on a per-host basis using services.h2o.hosts.<name>.tls.recommmendations.
The settings will also be overidden by manual values set with
services.settings.h2o.hosts.<name>.tls.extraSettings.
NOTE: older/weaker ciphers might require overriding the OpenSSL version
of H2O (such as `openssl_legacy`). This can be done with
sevices.settings.h2o.package.
'';
};
}

View File

@@ -0,0 +1,549 @@
{
config,
lib,
pkgs,
...
}:
# TODO: Gems includes for Mruby
let
cfg = config.services.h2o;
inherit (config.security.acme) certs;
inherit (lib)
literalExpression
mkDefault
mkEnableOption
mkIf
mkOption
types
;
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption;
settingsFormat = pkgs.formats.yaml { };
getNames = name: vhostSettings: rec {
server = if vhostSettings.serverName != null then vhostSettings.serverName else name;
cert =
if lib.attrByPath [ "acme" "useHost" ] null vhostSettings == null then
server
else
vhostSettings.acme.useHost;
};
# Attrset with the virtual hosts relevant to ACME configuration
acmeEnabledHostsConfigs = lib.foldlAttrs (
acc: name: value:
if value.acme == null || (!value.acme.enable && value.acme.useHost == null) then
acc
else
let
names = getNames name value;
virtualHostConfig = value // {
serverName = names.server;
certName = names.cert;
};
in
acc ++ [ virtualHostConfig ]
) [ ] cfg.hosts;
# Attrset with the ACME certificate names split by whether or not they depend
# on H2O serving challenges.
acmeCertNames =
let
partition =
acc: vhostSettings:
let
inherit (vhostSettings) certName;
isDependent = certs.${certName}.dnsProvider == null;
in
if isDependent && !(builtins.elem certName acc.dependent) then
acc // { dependent = acc.dependent ++ [ certName ]; }
else if !isDependent && !(builtins.elem certName acc.independent) then
acc // { independent = acc.independent ++ [ certName ]; }
else
acc;
certNames = lib.lists.foldl partition {
dependent = [ ];
independent = [ ];
} acmeEnabledHostsConfigs;
in
certNames
// {
all = certNames.dependent ++ certNames.independent;
};
mozTLSRecs =
if cfg.defaultTLSRecommendations != null then
let
# NOTE: if updating, *do* verify the changes then adjust ciphers &
# other settings with the tests @
# `nixos/tests/web-servers/h2o/tls-recommendations.nix`
# & run with `nix-build -A nixosTests.h2o.tls-recommendations`
version = "5.7";
git_tag = "v5.7.1";
guidelinesJSON =
lib.pipe
{
urls = [
"https://ssl-config.mozilla.org/guidelines/${version}.json"
"https://raw.githubusercontent.com/mozilla/ssl-config-generator/refs/tags/${git_tag}/src/static/guidelines/${version}.json"
];
sha256 = "sha256:1mj2pcb1hg7q2wpgdq3ac8pc2q64wvwvwlkb9xjmdd9jm4hiyny7";
}
[
pkgs.fetchurl
builtins.readFile
builtins.fromJSON
];
in
guidelinesJSON.configurations
else
null;
hostsConfig = lib.concatMapAttrs (
name: value:
let
port = {
HTTP = lib.attrByPath [ "http" "port" ] cfg.defaultHTTPListenPort value;
TLS = lib.attrByPath [ "tls" "port" ] cfg.defaultTLSListenPort value;
};
names = getNames name value;
acmeSettings = lib.optionalAttrs (builtins.elem names.cert acmeCertNames.dependent) (
let
acmePort = 80;
acmeChallengePath = "/.well-known/acme-challenge";
in
{
"${names.server}:${builtins.toString acmePort}" = {
listen.port = acmePort;
paths."${acmeChallengePath}/" = {
"file.dir" = value.acme.root + acmeChallengePath;
};
};
}
);
httpSettings =
lib.optionalAttrs (value.tls == null || value.tls.policy == "add") {
"${names.server}:${builtins.toString port.HTTP}" = value.settings // {
listen.port = port.HTTP;
};
}
// lib.optionalAttrs (value.tls != null && value.tls.policy == "force") {
"${names.server}:${builtins.toString port.HTTP}" = {
listen.port = port.HTTP;
paths."/" = {
redirect = {
status = value.tls.redirectCode;
url = "https://${names.server}:${builtins.toString port.TLS}";
};
};
};
};
tlsSettings =
lib.optionalAttrs
(
value.tls != null
&& builtins.elem value.tls.policy [
"add"
"only"
"force"
]
)
{
"${names.server}:${builtins.toString port.TLS}" =
let
tlsRecommendations = lib.attrByPath [ "tls" "recommendations" ] cfg.defaultTLSRecommendations value;
hasTLSRecommendations = tlsRecommendations != null && mozTLSRecs != null;
# ATTENTION: Lets Encrypt has sunset OCSP stapling.
tlsRecAttrs =
# If using ACME, this module will disable H2Os default OCSP
# stapling.
#
# See: https://letsencrypt.org/2024/12/05/ending-ocsp/
lib.optionalAttrs (builtins.elem names.cert acmeCertNames.all) {
ocsp-update-interval = 0;
}
# Mozillas ssl-config-generator is at present still
# recommending this setting as well, but this module will
# skip setting a stapling value as Lets Encrypt + ACME is
# the most likely use case.
#
# See: https://github.com/mozilla/ssl-config-generator/issues/323
// lib.optionalAttrs hasTLSRecommendations (
let
recs = mozTLSRecs.${tlsRecommendations};
in
{
min-version = builtins.head recs.tls_versions;
cipher-preference = "server";
"cipher-suite-tls1.3" = recs.ciphersuites;
}
// lib.optionalAttrs (recs.ciphers.openssl != [ ]) {
cipher-suite = lib.concatStringsSep ":" recs.ciphers.openssl;
}
);
headerRecAttrs =
lib.optionalAttrs
(
hasTLSRecommendations
&& value.tls != null
&& builtins.elem value.tls.policy [
"force"
"only"
]
)
(
let
headerSet = value.settings."header.set" or [ ];
recs = mozTLSRecs.${tlsRecommendations};
hsts = "Strict-Transport-Security: max-age=${builtins.toString recs.hsts_min_age}; includeSubDomains; preload";
in
{
"header.set" =
if builtins.isString headerSet then
[
headerSet
hsts
]
else
headerSet ++ [ hsts ];
}
);
listen =
let
identity =
value.tls.identity
++ lib.optional (builtins.elem names.cert acmeCertNames.all) {
key-file = "${certs.${names.cert}.directory}/key.pem";
certificate-file = "${certs.${names.cert}.directory}/fullchain.pem";
};
baseListen = {
port = port.TLS;
ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // {
inherit identity;
};
}
// lib.optionalAttrs (value.host != null) {
host = value.host;
};
# QUIC, if used, will duplicate the TLS over TCP directive, but
# append some extra QUIC-related settings
quicListen = lib.optional (value.tls.quic != null) (baseListen // { inherit (value.tls) quic; });
in
{
listen = [ baseListen ] ++ quicListen;
};
in
value.settings // headerRecAttrs // listen;
};
in
# With a high likelihood of HTTP & ACME challenges being on the same port,
# 80, do a recursive update to merge the 2 settings together
(lib.recursiveUpdate acmeSettings httpSettings) // tlsSettings
) cfg.hosts;
h2oConfig = settingsFormat.generate "h2o.yaml" (
lib.recursiveUpdate { hosts = hostsConfig; } cfg.settings
);
# Executing H2O with our generated configuration; `mode` added as needed
h2oExe = ''${lib.getExe cfg.package} ${
lib.strings.escapeShellArgs [
"--conf"
"${h2oConfig}"
]
}'';
in
{
options = {
services.h2o = {
enable = mkEnableOption "H2O web server";
user = mkOption {
type = types.nonEmptyStr;
default = "h2o";
description = "User running H2O service";
};
group = mkOption {
type = types.nonEmptyStr;
default = "h2o";
description = "Group running H2O services";
};
package = lib.mkPackageOption pkgs "h2o" {
example = # nix
''
pkgs.h2o.override {
withMruby = false;
openssl = pkgs.openssl_legacy;
}
'';
};
defaultHTTPListenPort = mkOption {
type = types.port;
default = 80;
description = ''
If hosts do not specify listen.port, use these ports for HTTP by default.
'';
example = 8080;
};
defaultTLSListenPort = mkOption {
type = types.port;
default = 443;
description = ''
If hosts do not specify listen.port, use these ports for SSL by default.
'';
example = 8443;
};
defaultTLSRecommendations = tlsRecommendationsOption;
settings = mkOption {
type = settingsFormat.type;
default = { };
description = "Configuration for H2O (see <https://h2o.examp1e.net/configure.html>)";
example =
literalExpression
# nix
''
{
compress = "ON";
ssl-offload = "kernel";
http2-reprioritize-blocking-assets = "ON";
"file.mime.addtypes" = {
"text/x-rst" = {
extensions = [ ".rst" ];
is_compressible = "YES";
};
};
}
'';
};
hosts = mkOption {
type = types.attrsOf (types.submodule (import ./vhost-options.nix { inherit config lib; }));
default = { };
description = ''
The `hosts` config to be merged with the settings.
Note that unlike YAML used for H2O, Nix will not support duplicate
keys to, for instance, have multiple listens in a host block; use the
virtual host options in like `http` & `tls` or use `$HOST:$PORT`
keys if manually specifying config.
'';
example =
literalExpression
# nix
''
{
"hydra.example.com" = {
tls = {
policy = "force";
identity = [
{
key-file = "/path/to/key";
certificate-file = "/path/to/cert";
};
];
extraSettings = {
minimum-version = "TLSv1.3";
};
};
settings = {
paths."/" = {
"file:dir" = "/var/www/default";
};
};
};
}
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion =
!(builtins.hasAttr "hosts" h2oConfig)
|| builtins.all (
host:
let
hasKeyPlusCert = attrs: (attrs.key-file or "") != "" && (attrs.certificate-file or "") != "";
in
# TLS not used
(lib.attrByPath [ "listen" "ssl" ] null host == null)
# TLS identity property
|| (
builtins.hasAttr "identity" host
&& builtins.length host.identity > 0
&& builtins.all hasKeyPlusCert host.listen.ssl.identity
)
# TLS short-hand (was manually specified)
|| (hasKeyPlusCert host.listen.ssl)
) (lib.attrValues h2oConfig.hosts);
message = ''
TLS support will require at least one non-empty certificate & key
file. Use services.h2o.hosts.<name>.acme.enable,
services.h2o.hosts.<name>.acme.useHost,
services.h2o.hosts.<name>.tls.identity, or
services.h2o.hosts.<name>.tls.extraSettings.
'';
}
]
++ builtins.map (
name:
mkCertOwnershipAssertion {
cert = certs.${name};
groups = config.users.groups;
services = [
config.systemd.services.h2o
]
++ lib.optional (acmeCertNames.all != [ ]) config.systemd.services.h2o-config-reload;
}
) acmeCertNames.all;
users = {
users.${cfg.user} = {
group = cfg.group;
}
// lib.optionalAttrs (cfg.user == "h2o") {
isSystemUser = true;
};
groups.${cfg.group} = { };
};
systemd.services.h2o = {
description = "H2O HTTP server";
wantedBy = [ "multi-user.target" ];
wants = lib.concatLists (map (certName: [ "acme-${certName}.service" ]) acmeCertNames.all);
# Since H2O will be hosting the challenges, H2O must be started
before = builtins.map (certName: "acme-order-renew-${certName}.service") acmeCertNames.all;
after = [
"network.target"
]
++ builtins.map (certName: "acme-${certName}.service") acmeCertNames.all;
serviceConfig = {
ExecStart = "${h2oExe} --mode 'master'";
ExecReload = [
"${h2oExe} --mode 'test'"
"${pkgs.coreutils}/bin/kill -HUP $MAINPID"
];
ExecStop = "${pkgs.coreutils}/bin/kill -s QUIT $MAINPID";
User = cfg.user;
Group = cfg.group;
Restart = "always";
RestartSec = "10s";
RuntimeDirectory = "h2o";
RuntimeDirectoryMode = "0750";
CacheDirectory = "h2o";
CacheDirectoryMode = "0750";
LogsDirectory = "h2o";
LogsDirectoryMode = "0750";
ProtectSystem = "strict";
ProtectHome = mkDefault true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
LockPersonality = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
PrivateMounts = true;
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilitiesBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
};
preStart = "${h2oExe} --mode 'test'";
};
# This service waits for all certificates to be available before reloading
# H2O configuration. `tlsTargets` are added to `wantedBy` + `before` which
# allows the `acme-order-renew-$cert.service` to signify the successful updating
# of certs end-to-end.
systemd.services.h2o-config-reload =
let
tlsServices = map (certName: "acme-order-renew-${certName}.service") acmeCertNames.all;
in
mkIf (acmeCertNames.all != [ ]) {
wantedBy = tlsServices ++ [ "multi-user.target" ];
after = tlsServices;
unitConfig = {
ConditionPathExists = map (
certName: "${certs.${certName}.directory}/fullchain.pem"
) acmeCertNames.all;
# Disable rate limiting for this since it may be triggered quickly
# a bunch of times if a lot of certificates are renewed in quick
# succession. The reload itself is cheap, so even doing a lot of them
# in a short burst is fine.
#
# FIXME: like Nginxs FIXME, theres probably a better way to do
# this.
StartLimitIntervalSec = 0;
};
serviceConfig = {
Type = "oneshot";
TimeoutSec = 60;
ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active h2o.service";
ExecStart = "/run/current-system/systemd/bin/systemctl reload h2o.service";
};
};
security.acme.certs =
let
mkCerts =
acc: vhostSettings:
if vhostSettings.acme.useHost == null then
let
hasRoot = vhostSettings.acme.root != null;
in
acc
// {
"${vhostSettings.serverName}" = {
group = mkDefault cfg.group;
# If `acme.root` 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 = lib.mkOverride (if hasRoot then 1000 else 2000) vhostSettings.acme.root;
# Also nudge dnsProvider to null in case it is inherited
dnsProvider = lib.mkOverride (if hasRoot then 1000 else 2000) null;
extraDomainNames = vhostSettings.serverAliases;
};
}
else
acc;
in
lib.lists.foldl mkCerts { } acmeEnabledHostsConfigs;
};
}

View File

@@ -0,0 +1,250 @@
{
config,
lib,
...
}:
let
inherit (lib)
literalExpression
mkOption
types
;
inherit (import ./common.nix { inherit lib; }) tlsRecommendationsOption;
in
{
options = {
serverName = mkOption {
type = types.nullOr types.nonEmptyStr;
default = null;
description = ''
Server name to be used for this virtual host. Defaults to attribute
name in hosts.
'';
example = "example.org";
};
serverAliases = mkOption {
type = types.listOf types.nonEmptyStr;
default = [ ];
example = [
"www.example.org"
"example.org"
];
description = ''
Additional names of virtual hosts served by this virtual host
configuration.
'';
};
host = mkOption {
type = types.nullOr types.nonEmptyStr;
default = null;
example = "127.0.0.1";
description = ''
Set the host address for this virtual host. If unset, the default is to
listen on all network interfaces.
'';
};
http = mkOption {
type = types.nullOr (
types.submodule {
options = {
port = mkOption {
type = types.port;
default = config.services.h2o.defaultHTTPListenPort;
defaultText = literalExpression ''
config.services.h2o.defaultHTTPListenPort
'';
description = ''
Override the default HTTP port for this virtual host.
'';
example = literalExpression "8080";
};
};
}
);
default = null;
description = "HTTP options for virtual host";
};
tls = mkOption {
type = types.nullOr (
types.submodule {
options = {
port = mkOption {
type = types.port;
default = config.services.h2o.defaultTLSListenPort;
defaultText = literalExpression ''
config.services.h2o.defaultTLSListenPort
'';
description = ''
Override the default TLS port for this virtual host.
'';
example = 8443;
};
policy = mkOption {
type = types.enum [
"add"
"only"
"force"
];
description = ''
`add` will additionally listen for TLS connections. `only` will
disable TLS connections. `force` will redirect non-TLS traffic
to the TLS connection.
'';
example = "force";
};
redirectCode = mkOption {
type = types.ints.between 300 399;
default = 301;
example = 308;
description = ''
HTTP status used by `globalRedirect` & `forceSSL`. Possible
usecases include temporary (302, 307) redirects, keeping the
request method & body (307, 308), or explicitly resetting the
method to GET (303). See
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections>.
'';
};
identity = mkOption {
type = types.listOf (
types.submodule {
options = {
key-file = mkOption {
type = types.path;
description = ''
Path to key file. See
<https://h2o.examp1e.net/configure/base_directives.html#key-file>.
'';
};
certificate-file = mkOption {
type = types.path;
description = ''
Path to certificate file. See
<https://h2o.examp1e.net/configure/base_directives.html#certificate-file>.
'';
};
};
}
);
default = [ ];
description = ''
Key / certificate pairs for the virtual host.
'';
example =
literalExpression
# nix
''
[
{
key-file = "/path/to/rsa.key";
certificate-file = "/path/to/rsa.crt";
}
{
key-file = "/path/to/ecdsa.key";
certificate-file = "/path/to/ecdsa.crt";
}
]
'';
};
recommendations = tlsRecommendationsOption;
quic = mkOption {
type = types.nullOr types.attrs;
default = null;
description = ''
Enables HTTP/3 over QUIC on the UDP port for TLS. The attrset
provides fine-turning for QUIC behavior, but can be empty. See
<https://h2o.examp1e.net/configure/http3_directives.html#quic-attributes>.
'';
example =
literalExpression
# nix
''
{
amp-limit = 2;
handshake-timeout-rtt-multiplier = 300;
retry = "ON";
}
'';
};
extraSettings = mkOption {
type = types.attrs;
default = { };
description = ''
Additional TLS/SSL-related configuration options. See
<https://h2o.examp1e.net/configure/base_directives.html#listen-ssl>.
'';
example =
literalExpression
# nix
''
{
minimum-version = "TLSv1.3";
}
'';
};
};
}
);
default = null;
description = "TLS options for virtual host";
};
acme = mkOption {
type = types.nullOr (
types.addCheck (types.submodule {
options = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to ask Lets Encrypt to sign a certificate for this
virtual host. Alternatively, an existing host can be used thru
{option}`acme.useHost`.
'';
};
useHost = mkOption {
type = types.nullOr types.nonEmptyStr;
default = null;
description = ''
An existing Lets Encrypt certificate to use for this virtual
host. This is useful if you have many subdomains and want to
avoid hitting the [rate
limit](https://letsencrypt.org/docs/rate-limits). Alternately,
you can generate a certificate through {option}`acme.enable`.
Note that this option neither creates any certificates nor does
it add subdomains to existing onesyou will need to create
them manually using [](#opt-security.acme.certs).
'';
};
root = mkOption {
type = types.nullOr types.path;
default = "/var/lib/acme/acme-challenge";
description = ''
Directory for the ACME challenge, which is **public**. Dont put
certs or keys in here. Set to `null` to inherit from
config.security.acme.
'';
};
};
}) (a: (a.enable || a.useHost != null) && !(a.enable && a.useHost != null))
);
default = null;
description = "ACME options for virtual host.";
};
settings = mkOption {
type = types.attrs;
default = { };
description = ''
Attrset to be transformed into YAML for host config. Note that the HTTP
/ TLS configurations will override these config values. See
<https://h2o.examp1e.net/configure/base_directives.html#hosts>.
'';
};
};
}

View File

@@ -0,0 +1,121 @@
{
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,70 @@
set -e
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,100 @@
{
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,46 @@
/*
This makes a keter bundle as described on the github page:
https://github.com/snoyberg/keter#bundling-your-app-for-keter
*/
{
keterDomain,
keterExecutable,
gnutar,
writeTextFile,
lib,
stdenv,
...
}:
let
str.stanzas = [
{
# we just use nix as an absolute path so we're not bundling any binaries
type = "webapp";
/*
Note that we're not actually putting the executable in the bundle,
we already can use the nix store for copying, so we just
symlink to the app.
*/
exec = keterExecutable;
host = keterDomain;
}
];
configFile = writeTextFile {
name = "keter.yml";
text = (lib.generators.toYAML { } str);
};
in
stdenv.mkDerivation {
name = "keter-bundle";
buildCommand = ''
mkdir -p config
cp ${configFile} config/keter.yaml
echo 'create a gzipped tarball'
mkdir -p $out
tar -zcvf $out/bundle.tar.gz.keter ./.
'';
buildInputs = [ gnutar ];
}

View File

@@ -0,0 +1,205 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.keter;
yaml = pkgs.formats.yaml { };
in
{
meta = {
maintainers = with lib.maintainers; [ jappie ];
};
imports = [
(lib.mkRenamedOptionModule [ "services" "keter" "keterRoot" ] [ "services" "keter" "root" ])
(lib.mkRenamedOptionModule [ "services" "keter" "keterPackage" ] [ "services" "keter" "package" ])
];
options.services.keter = {
enable = lib.mkEnableOption ''
keter, a web app deployment manager.
Note that this module only support loading of webapps:
Keep an old app running and swap the ports when the new one is booted
'';
root = lib.mkOption {
type = lib.types.str;
default = "/var/lib/keter";
description = "Mutable state folder for keter";
};
package = lib.mkPackageOption pkgs [ "haskellPackages" "keter" ] { };
globalKeterConfig = lib.mkOption {
type = lib.types.submodule {
freeformType = yaml.type;
options = {
ip-from-header = lib.mkOption {
default = true;
type = lib.types.bool;
description = "You want that ip-from-header in the nginx setup case. It allows nginx setting the original ip address rather then it being localhost (due to reverse proxying)";
};
listeners = lib.mkOption {
default = [
{
host = "*";
port = 6981;
}
];
type = lib.types.listOf (
lib.types.submodule {
options = {
host = lib.mkOption {
type = lib.types.str;
description = "host";
};
port = lib.mkOption {
type = lib.types.port;
description = "port";
};
};
}
);
description = ''
You want that ip-from-header in
the nginx setup case.
It allows nginx setting the original ip address rather
then it being localhost (due to reverse proxying).
However if you configure keter to accept connections
directly you may want to set this to false.'';
};
rotate-logs = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
emits keter logs and it's applications to stderr.
which allows journald to capture them.
Set to true to let keter put the logs in files
(useful on non systemd systems, this is the old approach
where keter handled log management)'';
};
};
};
description = "Global config for keter, see <https://github.com/snoyberg/keter/blob/master/etc/keter-config.yaml> for reference";
};
bundle = {
appName = lib.mkOption {
type = lib.types.str;
default = "myapp";
description = "The name keter assigns to this bundle";
};
executable = lib.mkOption {
type = lib.types.path;
description = "The executable to be run";
};
domain = lib.mkOption {
type = lib.types.str;
default = "example.com";
description = "The domain keter will bind to";
};
publicScript = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Allows loading of public environment variables,
these are emitted to the log so it shouldn't contain secrets.
'';
example = "ADMIN_EMAIL=hi@example.com";
};
secretScript = lib.mkOption {
type = lib.types.str;
default = "";
description = "Allows loading of private environment variables";
example = "MY_AWS_KEY=$(cat /run/keys/AWS_ACCESS_KEY_ID)";
};
};
};
config = lib.mkIf cfg.enable (
let
incoming = "${cfg.root}/incoming";
globalKeterConfigFile = pkgs.writeTextFile {
name = "keter-config.yml";
text = (lib.generators.toYAML { } (cfg.globalKeterConfig // { root = cfg.root; }));
};
# If things are expected to change often, put it in the bundle!
bundle = pkgs.callPackage ./bundle.nix (
cfg.bundle
// {
keterExecutable = executable;
keterDomain = cfg.bundle.domain;
}
);
# This indirection is required to ensure the nix path
# gets copied over to the target machine in remote deployments.
# Furthermore, it's important that we use exec to
# run the binary otherwise we get process leakage due to this
# being executed on every change.
executable = pkgs.writeShellScript "bundle-wrapper" ''
set -e
${cfg.bundle.secretScript}
set -xe
${cfg.bundle.publicScript}
exec ${cfg.bundle.executable}
'';
in
{
systemd.services.keter = {
description = "keter app loader";
script = ''
set -xe
mkdir -p ${incoming}
${lib.getExe cfg.package} ${globalKeterConfigFile};
'';
wantedBy = [
"multi-user.target"
"nginx.service"
];
serviceConfig = {
Restart = "always";
RestartSec = "10s";
};
after = [
"network.target"
"local-fs.target"
"postgresql.target"
];
};
# On deploy this will load our app, by moving it into the incoming dir
# If the bundle content changes, this will run again.
# Because the bundle content contains the nix path to the executable,
# we inherit nix based cache busting.
systemd.services.load-keter-bundle = {
description = "load keter bundle into incoming folder";
after = [ "keter.service" ];
wantedBy = [ "multi-user.target" ];
# we can't override keter bundles because it'll stop the previous app
# https://github.com/snoyberg/keter#deploying
script = ''
set -xe
cp ${bundle}/bundle.tar.gz.keter ${incoming}/${cfg.bundle.appName}.keter
'';
path = [
executable
cfg.bundle.executable
]; # this is a hack to get the executable copied over to the machine.
};
}
);
}

View File

@@ -0,0 +1,101 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.lighttpd.cgit;
pathPrefix = optionalString (stringLength cfg.subdir != 0) ("/" + 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,80 @@
{
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 = literalMD ''
`config.${options.services.collectd.package}` 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,273 @@
# 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:
# https://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: optionalString (elem moduleName cfg.enableModules) ''"${moduleName}"'';
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:
#
# https://redmine.lighttpd.net/projects/1/wiki/Server_modulesDetails
# https://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" )
${optionalString cfg.mod_userdir ''
userdir.path = "public_html"
''}
${optionalString cfg.mod_status ''
status.status-url = "/server-status"
status.statistics-url = "/server-statistics"
status.config-url = "/server-config"
''}
${cfg.extraConfig}
'';
in
{
options = {
services.lighttpd = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Enable the lighttpd web server.
'';
};
package = mkPackageOption pkgs "lighttpd" { };
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` 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`.
'';
};
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 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}";
serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -SIGUSR1 $MAINPID";
# 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,64 @@
{
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,60 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.merecat;
format = pkgs.formats.keyValue {
mkKeyValue = generators.mkKeyValueDefault {
mkValueString =
v:
# In merecat.conf, booleans are "true" and "false"
if builtins.isBool v then if v then "true" else "false" else generators.mkValueStringDefault { } v;
} "=";
};
configFile = format.generate "merecat.conf" cfg.settings;
in
{
options.services.merecat = {
enable = mkEnableOption "Merecat HTTP server";
settings = mkOption {
inherit (format) type;
default = { };
description = ''
Merecat configuration. Refer to {manpage}`merecat(8)` for details on supported values.
'';
example = {
hostname = "localhost";
port = 8080;
virtual-host = true;
directory = "/srv/www";
};
};
};
config = mkIf cfg.enable {
systemd.services.merecat = {
description = "Merecat HTTP server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${pkgs.merecat}/bin/merecat -n -f ${configFile}";
AmbientCapabilities = lib.mkIf ((cfg.settings.port or 80) < 1024) [ "CAP_NET_BIND_SERVICE" ];
};
};
};
}

View File

@@ -0,0 +1,140 @@
{
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 <https://kazu-yamamoto.github.io/mighttpd2/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 <https://kazu-yamamoto.github.io/mighttpd2/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";
wants = [ "network-online.target" ];
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,221 @@
{
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 = with maintainers; [
bachp
ryan4yin
];
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.either types.path types.str);
description = "The list of data directories or nodes 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.";
};
certificatesDir = mkOption {
default = "/var/lib/minio/certs";
type = types.path;
description = "The directory where TLS certificates are stored.";
};
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
`configDir` 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
`configDir` 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 {manpage}`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 = mkPackageOption pkgs "minio" { };
};
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 = lib.mkMerge [
{
tmpfiles.rules = [
"d '${cfg.configDir}' - minio minio - -"
]
++ (map (x: "d '" + x + "' - minio minio - - ") (builtins.filter lib.types.path.check cfg.dataDir));
services.minio = {
description = "Minio Object Storage";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --console-address ${cfg.consoleAddress} --config-dir=${cfg.configDir} --certs-dir=${cfg.certificatesDir} ${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;
# hardening
DevicePolicy = "closed";
CapabilityBoundingSet = "";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
"AF_UNIX"
];
DeviceAllow = "";
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
MemoryDenyWriteExecute = true;
LockPersonality = true;
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
ProtectProc = "invisible";
ProtectHostname = true;
UMask = "0077";
# minio opens /proc/mounts on startup
ProcSubset = "all";
};
environment = {
MINIO_REGION = "${cfg.region}";
MINIO_BROWSER = "${if cfg.browser then "on" else "off"}";
};
};
}
(lib.mkIf (cfg.rootCredentialsFile != null) {
# The service will fail if the credentials file is missing
services.minio.unitConfig.ConditionPathExists = cfg.rootCredentialsFile;
# The service will not restart if the credentials file has
# been changed. This can cause stale root credentials.
paths.minio-root-credentials = {
wantedBy = [ "multi-user.target" ];
pathConfig = {
PathChanged = [ cfg.rootCredentialsFile ];
Unit = "minio-restart.service";
};
};
services.minio-restart = {
description = "Restart MinIO";
script = ''
systemctl restart minio.service
'';
serviceConfig = {
Type = "oneshot";
Restart = "on-failure";
RestartSec = 5;
};
};
})
];
users.users.minio = {
group = "minio";
uid = config.ids.uids.minio;
};
users.groups.minio.gid = config.ids.uids.minio;
};
}

View File

@@ -0,0 +1,110 @@
{
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:
```
systemd.services.molly-brown.serviceConfig.SupplementaryGroups =
[ config.security.acme.certs."example.com".group ];
```
'';
};
keyPath = mkOption {
type = types.path;
example = "/var/lib/acme/example.com/key.pem";
description = "Path to TLS key. See {option}`CertPath`.";
};
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
<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,101 @@
{
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 `config.services.gitolite.group` 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 = [ ];
}

View File

@@ -0,0 +1,162 @@
# 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, config }:
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 by running {command}`nix-shell --packages apacheHttpd --run 'htpasswd -B -c FILENAME USERNAME'`.
'';
};
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.
'';
};
uwsgiPass = mkOption {
type = types.nullOr types.str;
default = null;
example = "unix:/run/example/example.sock";
description = ''
Adds uwsgi_pass directive and sets recommended proxy headers if
recommendedUwsgiSettings is enabled.
'';
};
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 =
with types;
nullOr (oneOf [
str
int
]);
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.
'';
};
recommendedProxySettings = mkOption {
type = types.bool;
default = config.services.nginx.recommendedProxySettings;
defaultText = literalExpression "config.services.nginx.recommendedProxySettings";
description = ''
Enable recommended proxy settings.
'';
};
recommendedUwsgiSettings = mkOption {
type = types.bool;
default = config.services.nginx.recommendedUwsgiSettings;
defaultText = literalExpression "config.services.nginx.recommendedUwsgiSettings";
description = ''
Enable recommended uwsgi settings.
'';
};
};
}

View File

@@ -0,0 +1,110 @@
{ config, lib, ... }:
let
inherit (lib)
genAttrs
maintainers
mkAliasOptionModule
mkEnableOption
mkIf
mkOption
types
;
cfg = config.services.nginx.tailscaleAuth;
cfgAuth = config.services.tailscaleAuth;
in
{
imports = [
(mkAliasOptionModule
[ "services" "nginx" "tailscaleAuth" "package" ]
[ "services" "tailscaleAuth" "package" ]
)
(mkAliasOptionModule
[ "services" "nginx" "tailscaleAuth" "user" ]
[ "services" "tailscaleAuth" "user" ]
)
(mkAliasOptionModule
[ "services" "nginx" "tailscaleAuth" "group" ]
[ "services" "tailscaleAuth" "group" ]
)
(mkAliasOptionModule
[ "services" "nginx" "tailscaleAuth" "socketPath" ]
[ "services" "tailscaleAuth" "socketPath" ]
)
];
options.services.nginx.tailscaleAuth = {
enable = mkEnableOption "tailscale.nginx-auth, to authenticate nginx users via tailscale";
expectedTailnet = mkOption {
default = "";
type = types.nullOr types.str;
example = "tailnet012345.ts.net";
description = ''
If you want to prevent node sharing from allowing users to access services
across tailnets, declare your expected tailnets domain here.
'';
};
virtualHosts = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
A list of nginx virtual hosts to put behind tailscale.nginx-auth
'';
};
};
config = mkIf cfg.enable {
services.tailscaleAuth.enable = true;
services.nginx.enable = true;
users.users.${config.services.nginx.user}.extraGroups = [ cfgAuth.group ];
systemd.services.tailscale-nginx-auth = {
after = [ "nginx.service" ];
wants = [ "nginx.service" ];
};
services.nginx.virtualHosts = genAttrs cfg.virtualHosts (vhost: {
locations."/auth" = {
extraConfig = ''
internal;
proxy_pass http://unix:${cfgAuth.socketPath};
proxy_pass_request_body off;
# Upstream uses $http_host here, but we are using gixy to check nginx configurations
# gixy wants us to use $host: https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md
proxy_set_header Host $host;
proxy_set_header Remote-Addr $remote_addr;
proxy_set_header Remote-Port $remote_port;
proxy_set_header Original-URI $request_uri;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
'';
};
locations."/".extraConfig = ''
auth_request /auth;
auth_request_set $auth_user $upstream_http_tailscale_user;
auth_request_set $auth_name $upstream_http_tailscale_name;
auth_request_set $auth_login $upstream_http_tailscale_login;
auth_request_set $auth_tailnet $upstream_http_tailscale_tailnet;
auth_request_set $auth_profile_picture $upstream_http_tailscale_profile_picture;
proxy_set_header X-Webauth-User "$auth_user";
proxy_set_header X-Webauth-Name "$auth_name";
proxy_set_header X-Webauth-Login "$auth_login";
proxy_set_header X-Webauth-Tailnet "$auth_tailnet";
proxy_set_header X-Webauth-Profile-Picture "$auth_profile_picture";
${lib.optionalString (
cfg.expectedTailnet != ""
) ''proxy_set_header Expected-Tailnet "${cfg.expectedTailnet}";''}
'';
});
};
meta.maintainers = with maintainers; [ phaer ];
}

View File

@@ -0,0 +1,389 @@
# 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 = "Listen address.";
};
port = mkOption {
type = types.nullOr port;
description = ''
Port number to listen on.
If unset and the listen address is not a socket then nginx defaults to 80.
'';
default = null;
};
ssl = mkOption {
type = bool;
description = "Enable SSL.";
default = false;
};
proxyProtocol = mkOption {
type = bool;
description = "Enable PROXY protocol.";
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;
}
{ addr = "unix:/var/run/nginx.sock"; }
];
description = ''
Listen addresses and ports for this virtual host.
IPv6 addresses must be enclosed in square brackets.
Note: this option overrides `addSSL`
and `onlySSL`.
If you only want to set the addresses manually and not
the ports, take a look at `listenAddresses`.
'';
};
listenAddresses = mkOption {
type = with types; listOf str;
description = ''
Listen addresses for this virtual host.
Compared to `listen` this only sets the addresses
and the ports are chosen automatically.
Note: This option overrides `enableIPv6`
'';
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`.
'';
};
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
[rate limit](https://letsencrypt.org/docs/rate-limits).
Alternately, you can generate a certificate through {option}`enableACME`.
*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 [](#opt-security.acme.certs).*
'';
};
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.
With this option, you could request certificates for the present domain
with an ACME client that is running on another host, which you would
specify here.
'';
};
addSSL = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable HTTPS in addition to plain HTTP. This will set defaults for
`listen` 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 `listen` 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 redirects (defaults
to 301, configurable with `redirectCode`) all plain HTTP traffic to
HTTPS. This will set defaults for `listen` 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
[default](#opt-services.nginx.virtualHosts._name_.default)
server blocks to avoid serving the certificate for another vhost. Uses the
`ssl_reject_handshake` 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 the HTTP/2 protocol.
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 = true;
description = ''
Whether to enable the HTTP/3 protocol.
This requires using `pkgs.nginxQuic` package
which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`
and activate the QUIC transport protocol
`services.nginx.virtualHosts.<name>.quic = true;`.
Note that HTTP/3 support is experimental and *not* yet recommended for production.
Read more at <https://quic.nginx.org/>
HTTP/3 availability must be manually advertised, preferably in each location block.
'';
};
http3_hq = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the HTTP/0.9 protocol negotiation used in QUIC interoperability tests.
This requires using `pkgs.nginxQuic` package
which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`
and activate the QUIC transport protocol
`services.nginx.virtualHosts.<name>.quic = true;`.
Note that special application protocol support is experimental and *not* yet recommended for production.
Read more at <https://quic.nginx.org/>
'';
};
quic = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the QUIC transport protocol.
This requires using `pkgs.nginxQuic` package
which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`.
Note that QUIC 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 (defaults to 301,
configurable with `redirectCode`) to the given hostname.
'';
};
redirectCode = mkOption {
type = types.ints.between 300 399;
default = 301;
example = 308;
description = ''
HTTP status used by `globalRedirect` and `forceSSL`. Possible usecases
include temporary (302, 307) redirects, keeping the request method and
body (307, 308), or explicitly resetting the method to GET (303).
See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections>.
'';
};
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 by running {command}`nix-shell --packages apacheHttpd --run 'htpasswd -B -c FILENAME USERNAME'`.
'';
};
locations = mkOption {
type = types.attrsOf (
types.submodule (
import ./location-options.nix {
inherit lib config;
}
)
);
default = { };
example = literalExpression ''
{
"/" = {
proxyPass = "http://localhost:3000";
};
};
'';
description = "Declarative location config";
};
};
}

View File

@@ -0,0 +1,310 @@
{
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}
This option is read-only and managed by NixOS.
:::
'';
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 {file}`php.ini` 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
<https://www.php.net/manual/en/install.fpm.configuration.php>
for details. Note that settings names must be enclosed in quotes (e.g.
`"pm.max_children"` instead of `pm.max_children`).
'';
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 `php-fpm.conf` 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
<https://www.php.net/manual/en/install.fpm.configuration.php>
for details. Note that settings names must be enclosed in quotes (e.g.
`"pm.max_children"` instead of `pm.max_children`).
You need not specify the options `error_log` or
`daemonize` 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
`error_log` or
`daemonize` here, since they are generated by
NixOS.
'';
};
phpPackage = mkPackageOption pkgs "php" { };
phpOptions = mkOption {
type = types.lines;
default = "";
example = ''
date.timezone = "CET"
'';
description = ''
Options appended to the PHP configuration file {file}`php.ini`.
'';
};
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.system-phpfpm = {
description = "PHP FastCGI Process Manager 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" ];
documentation = [ "man:php-fpm(8)" ];
serviceConfig =
let
cfgFile = fpmCfgFile pool poolOpts;
iniFile = phpIni poolOpts;
in
{
Slice = "system-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";
WatchdogSec = 15;
};
}
) cfg.pools;
};
}

View File

@@ -0,0 +1,152 @@
{
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 [the Pomerium
configuration reference](https://pomerium.io/reference/) 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 {manpage}`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-${cfg.useACMEHost}.service");
after = [
"network.target"
]
++ (optional (cfg.useACMEHost != null) "acme-${cfg.useACMEHost}.service");
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-order-renew-$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-order-renew-${cfg.useACMEHost}.service"
"multi-user.target"
];
after = [ "acme-order-renew-${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,273 @@
{
lib,
pkgs,
config,
...
}:
with lib;
let
cfg = config.services.rustus;
in
{
meta.maintainers = with maintainers; [ happysalada ];
options.services.rustus = {
enable = mkEnableOption "TUS protocol implementation in Rust";
host = mkOption {
type = types.str;
description = ''
The host that rustus will connect to.
'';
default = "127.0.0.1";
example = "127.0.0.1";
};
port = mkOption {
type = types.port;
description = ''
The port that rustus will connect to.
'';
default = 1081;
example = 1081;
};
log_level = mkOption {
type = types.enum [
"DEBUG"
"INFO"
"ERROR"
];
description = ''
Desired log level
'';
default = "INFO";
example = "ERROR";
};
max_body_size = mkOption {
type = types.str;
description = ''
Maximum body size in bytes
'';
default = "10000000"; # 10 mb
example = "100000000";
};
url = mkOption {
type = types.str;
description = ''
url path for uploads
'';
default = "/files";
};
disable_health_access_logs = mkOption {
type = types.bool;
description = ''
disable access log for /health endpoint
'';
default = false;
};
cors = mkOption {
type = types.listOf types.str;
description = ''
list of origins allowed to upload
'';
default = [ "*" ];
example = [
"*.staging.domain"
"*.prod.domain"
];
};
tus_extensions = mkOption {
type = types.listOf (
types.enum [
"getting"
"creation"
"termination"
"creation-with-upload"
"creation-defer-length"
"concatenation"
"checksum"
]
);
description = ''
Since TUS protocol offers extensibility you can turn off some protocol extensions.
'';
default = [
"getting"
"creation"
"termination"
"creation-with-upload"
"creation-defer-length"
"concatenation"
"checksum"
];
};
remove_parts = mkOption {
type = types.bool;
description = ''
remove parts files after successful concatenation
'';
default = true;
example = false;
};
storage = lib.mkOption {
description = ''
Storages are used to actually store your files. You can configure where you want to store files.
'';
default = { };
example = lib.literalExpression ''
{
type = "hybrid-s3"
s3_access_key_file = konfig.age.secrets.R2_ACCESS_KEY.path;
s3_secret_key_file = konfig.age.secrets.R2_SECRET_KEY.path;
s3_bucket = "my_bucket";
s3_url = "https://s3.example.com";
}
'';
type = lib.types.submodule {
options = {
type = lib.mkOption {
type = lib.types.enum [
"file-storage"
"hybrid-s3"
];
description = "Type of storage to use";
};
s3_access_key_file = lib.mkOption {
type = lib.types.str;
description = "File path that contains the S3 access key.";
};
s3_secret_key_file = lib.mkOption {
type = lib.types.path;
description = "File path that contains the S3 secret key.";
};
s3_region = lib.mkOption {
type = lib.types.str;
default = "us-east-1";
description = "S3 region name.";
};
s3_bucket = lib.mkOption {
type = lib.types.str;
description = "S3 bucket.";
};
s3_url = lib.mkOption {
type = lib.types.str;
description = "S3 url.";
};
force_sync = lib.mkOption {
type = lib.types.bool;
description = "calls fsync system call after every write to disk in local storage";
default = true;
};
data_dir = lib.mkOption {
type = lib.types.str;
description = "path to the local directory where all files are stored";
default = "/var/lib/rustus";
};
dir_structure = lib.mkOption {
type = lib.types.str;
description = "pattern of a directory structure locally and on s3";
default = "{year}/{month}/{day}";
};
};
};
};
info_storage = lib.mkOption {
description = ''
Info storages are used to store information about file uploads. These storages must be persistent, because every time chunk is uploaded rustus updates information about upload. And when someone wants to download file, information about it requested from storage to get actual path of an upload.
'';
default = { };
type = lib.types.submodule {
options = {
type = lib.mkOption {
type = lib.types.enum [ "file-info-storage" ];
description = "Type of info storage to use";
default = "file-info-storage";
};
dir = lib.mkOption {
type = lib.types.str;
description = "directory to store info about uploads";
default = "/var/lib/rustus";
};
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.rustus =
let
isHybridS3 = cfg.storage.type == "hybrid-s3";
in
{
description = "Rustus server";
documentation = [ "https://s3rius.github.io/rustus/" ];
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
RUSTUS_SERVER_HOST = cfg.host;
RUSTUS_SERVER_PORT = toString cfg.port;
RUSTUS_LOG_LEVEL = cfg.log_level;
RUSTUS_MAX_BODY_SIZE = cfg.max_body_size;
RUSTUS_URL = cfg.url;
RUSTUS_DISABLE_HEALTH_ACCESS_LOG = lib.mkIf cfg.disable_health_access_logs "true";
RUSTUS_CORS = lib.concatStringsSep "," cfg.cors;
RUSTUS_TUS_EXTENSIONS = lib.concatStringsSep "," cfg.tus_extensions;
RUSTUS_REMOVE_PARTS = if cfg.remove_parts then "true" else "false";
RUSTUS_STORAGE = cfg.storage.type;
RUSTUS_DATA_DIR = cfg.storage.data_dir;
RUSTUS_DIR_STRUCTURE = cfg.storage.dir_structure;
RUSTUS_FORCE_FSYNC = if cfg.storage.force_sync then "true" else "false";
RUSTUS_S3_URL = mkIf isHybridS3 cfg.storage.s3_url;
RUSTUS_S3_BUCKET = mkIf isHybridS3 cfg.storage.s3_bucket;
RUSTUS_S3_REGION = mkIf isHybridS3 cfg.storage.s3_region;
RUSTUS_S3_ACCESS_KEY_PATH = mkIf isHybridS3 "%d/S3_ACCESS_KEY_PATH";
RUSTUS_S3_SECRET_KEY_PATH = mkIf isHybridS3 "%d/S3_SECRET_KEY_PATH";
RUSTUS_INFO_STORAGE = cfg.info_storage.type;
RUSTUS_INFO_DIR = cfg.info_storage.dir;
};
serviceConfig = {
ExecStart = "${pkgs.rustus}/bin/rustus";
StateDirectory = "rustus";
# User name is defined here to enable restoring a backup for example
# You will run the backup restore command as sudo -u rustus in order
# to have write permissions to /var/lib
User = "rustus";
DynamicUser = true;
LoadCredential = lib.optionals isHybridS3 [
"S3_ACCESS_KEY_PATH:${cfg.storage.s3_access_key_file}"
"S3_SECRET_KEY_PATH:${cfg.storage.s3_secret_key_file}"
];
# hardening
RestrictRealtime = true;
RestrictNamespaces = true;
LockPersonality = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
ProtectHostUserNamespaces = true;
ProtectClock = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
CapabilityBoundingSet = "";
ProtectProc = "invisible";
# TODO consider SystemCallFilter LimitAS ProcSubset
};
};
};
}

View File

@@ -0,0 +1,226 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkOption types;
cfg = config.services.send;
in
{
options = {
services.send = {
enable = lib.mkEnableOption "Send, a file sharing web sevice for ffsend.";
package = lib.mkPackageOption pkgs "send" { };
environment = mkOption {
type =
with types;
attrsOf (
nullOr (oneOf [
bool
int
str
(listOf int)
])
);
description = ''
All the available config options and their defaults can be found here: <https://github.com/timvisee/send/blob/master/server/config.js>,
some descriptions can found here: <https://github.com/timvisee/send/blob/master/docs/docker.md#environment-variables>
Values under {option}`services.send.environment` will override the predefined values in the Send service.
- Time/duration should be in seconds
- Filesize values should be in bytes
'';
example = {
DEFAULT_DOWNLOADS = 1;
DETECT_BASE_URL = true;
EXPIRE_TIMES_SECONDS = [
300
3600
86400
604800
];
};
};
dataDir = lib.mkOption {
type = types.path;
readOnly = true;
default = "/var/lib/send";
description = ''
Directory for uploaded files.
Due to limitations in {option}`systemd.services.send.serviceConfig.DynamicUser`, this item is read only.
'';
};
baseUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Base URL for the Send service.
Leave it blank to automatically detect the base url.
'';
};
host = lib.mkOption {
type = types.str;
default = "127.0.0.1";
description = "The hostname or IP address for Send to bind to.";
};
port = lib.mkOption {
type = types.port;
default = 1443;
description = "Port the Send service listens on.";
};
openFirewall = lib.mkOption {
type = types.bool;
default = false;
description = "Whether to open firewall ports for send";
};
redis = {
createLocally = lib.mkOption {
type = types.bool;
default = true;
description = "Whether to create a local redis automatically.";
};
name = lib.mkOption {
type = types.str;
default = "send";
description = ''
Name of the redis server.
Only used if {option}`services.send.redis.createLocally` is set to true.
'';
};
host = lib.mkOption {
type = types.str;
default = "localhost";
description = "Redis server address.";
};
port = lib.mkOption {
type = types.port;
default = 6379;
description = "Port of the redis server.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/agenix/send-redis-password";
description = ''
The path to the file containing the Redis password.
If {option}`services.send.redis.createLocally` is set to true,
the content of this file will be used as the password for the locally created Redis instance.
Leave it blank if no password is required.
'';
};
};
};
};
config = lib.mkIf cfg.enable {
services.send.environment.DETECT_BASE_URL = cfg.baseUrl == null;
assertions = [
{
assertion = cfg.redis.createLocally -> cfg.redis.host == "localhost";
message = "the redis host must be localhost if services.send.redis.createLocally is set to true";
}
];
networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.port;
services.redis = lib.optionalAttrs cfg.redis.createLocally {
servers."${cfg.redis.name}" = {
enable = true;
bind = "localhost";
port = cfg.redis.port;
};
};
systemd.services.send = {
serviceConfig = {
Type = "simple";
Restart = "always";
StateDirectory = "send";
WorkingDirectory = cfg.dataDir;
ReadWritePaths = cfg.dataDir;
LoadCredential = lib.optionalString (
cfg.redis.passwordFile != null
) "redis-password:${cfg.redis.passwordFile}";
# Hardening
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
AmbientCapabilities = lib.optionalString (cfg.port < 1024) "cap_net_bind_service";
DynamicUser = true;
CapabilityBoundingSet = "";
NoNewPrivileges = true;
RemoveIPC = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "full";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
UMask = "0077";
};
environment = {
IP_ADDRESS = cfg.host;
PORT = toString cfg.port;
BASE_URL = if (cfg.baseUrl == null) then "http://${cfg.host}:${toString cfg.port}" else cfg.baseUrl;
FILE_DIR = cfg.dataDir + "/uploads";
REDIS_HOST = cfg.redis.host;
REDIS_PORT = toString cfg.redis.port;
}
// (lib.mapAttrs (
name: value:
if lib.isList value then
"[" + lib.concatStringsSep ", " (map (x: toString x) value) + "]"
else if lib.isBool value then
lib.boolToString value
else
toString value
) cfg.environment);
after = [
"network.target"
]
++ lib.optionals cfg.redis.createLocally [
"redis-${cfg.redis.name}.service"
];
description = "Send web service";
wantedBy = [ "multi-user.target" ];
script = ''
${lib.optionalString (cfg.redis.passwordFile != null) ''
export REDIS_PASSWORD="$(cat $CREDENTIALS_DIRECTORY/redis-password)"
''}
${lib.getExe cfg.package}
'';
};
};
meta.maintainers = with lib.maintainers; [ moraxyc ];
}

View File

@@ -0,0 +1,304 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.stargazer;
globalSection = ''
listen = ${lib.concatStringsSep " " cfg.listen}
connection-logging = ${lib.boolToString cfg.connectionLogging}
log-ip = ${lib.boolToString cfg.ipLog}
log-ip-partial = ${lib.boolToString cfg.ipLogPartial}
request-timeout = ${toString cfg.requestTimeout}
response-timeout = ${toString cfg.responseTimeout}
[:tls]
store = ${toString cfg.store}
organization = ${cfg.certOrg}
gen-certs = ${lib.boolToString cfg.genCerts}
regen-certs = ${lib.boolToString cfg.regenCerts}
${lib.optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"}
'';
genINI = lib.generators.toINI { };
configFile = pkgs.writeText "config.ini" (
lib.strings.concatStrings (
[ globalSection ]
++ (lib.lists.forEach cfg.routes (
section:
let
name = section.route;
params = builtins.removeAttrs section [ "route" ];
in
genINI {
"${name}" = params;
}
+ "\n"
))
)
);
in
{
options.services.stargazer = {
enable = lib.mkEnableOption "Stargazer Gemini server";
listen = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]";
defaultText = lib.literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
example = lib.literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
description = ''
Address and port to listen on.
'';
};
connectionLogging = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether or not to log connections to stdout.";
};
ipLog = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Log client IP addresses in the connection log.";
};
ipLogPartial = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Log partial client IP addresses in the connection log.";
};
requestTimeout = lib.mkOption {
type = lib.types.ints.unsigned;
default = 5;
description = ''
Number of seconds to wait for the client to send a complete
request. Set to 0 to disable.
'';
};
responseTimeout = lib.mkOption {
type = lib.types.ints.unsigned;
default = 0;
description = ''
Number of seconds to wait for the client to send a complete
request and for stargazer to finish sending the response.
Set to 0 to disable.
'';
};
allowCgiUser = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
When enabled, the stargazer process will be given `CAP_SETGID`
and `CAP_SETUID` so that it can run cgi processes as a different
user. This is required if the `cgi-user` option is used for a route.
Note that these capabilities could allow privilege escalation so be
careful. For that reason, this is disabled by default.
You will need to create the user mentioned `cgi-user` if it does not
already exist.
'';
};
store = lib.mkOption {
type = lib.types.path;
default = /var/lib/gemini/certs;
description = ''
Path to the certificate store on disk. This should be a
persistent directory writable by Stargazer.
'';
};
certOrg = lib.mkOption {
type = lib.types.str;
default = "stargazer";
description = ''
The name of the organization responsible for the X.509
certificate's /O name.
'';
};
genCerts = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Set to false to disable automatic certificate generation.
Use if you want to provide your own certs.
'';
};
regenCerts = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Set to false to turn off automatic regeneration of expired certificates.
Use if you want to provide your own certs.
'';
};
certLifetime = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
How long certs generated by Stargazer should live for.
Certs live forever by default.
'';
example = lib.literalExpression "\"1y\"";
};
debugMode = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Run Stargazer in debug mode.";
};
routes = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
freeformType =
with lib.types;
attrsOf (
nullOr (oneOf [
bool
int
float
str
])
// {
description = "INI atom (null, bool, int, float or string)";
}
);
options.route = lib.mkOption {
type = lib.types.str;
description = "Route section name";
};
}
);
default = [ ];
description = ''
Routes that Stargazer should server.
Expressed as a list of attribute sets. Each set must have a key `route`
that becomes the section name for that route in the stargazer ini cofig.
The remaining keys and values become the parameters for that route.
[Refer to upstream docs for other params](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt)
'';
example = lib.literalExpression ''
[
{
route = "example.com";
root = "/srv/gemini/example.com"
}
{
route = "example.com:/man";
root = "/cgi-bin";
cgi = true;
}
{
route = "other.org~(.*)";
redirect = "gemini://example.com";
rewrite = "\1";
}
]
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "stargazer";
description = "User account under which stargazer runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "stargazer";
description = "Group account under which stargazer runs.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.stargazer = {
description = "Stargazer gemini server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile} ${lib.optionalString cfg.debugMode "-D"}";
Restart = "always";
# User and group
User = cfg.user;
Group = cfg.group;
AmbientCapabilities = lib.mkIf cfg.allowCgiUser [
"CAP_SETGID"
"CAP_SETUID"
];
# Hardening
UMask = "0077";
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "full";
ProtectClock = true;
ProtectHostname = true;
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
PrivateDevices = true;
NoNewPrivileges = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
MemoryDenyWriteExecute = true;
LockPersonality = true;
RestrictRealtime = true;
RemoveIPC = true;
CapabilityBoundingSet = [
"~CAP_SYS_PTRACE"
"~CAP_SYS_ADMIN"
"~CAP_SETPCAP"
"~CAP_SYS_TIME"
"~CAP_SYS_PACCT"
"~CAP_SYS_TTY_CONFIG "
"~CAP_SYS_CHROOT"
"~CAP_SYS_BOOT"
"~CAP_NET_ADMIN"
]
++ lib.lists.optional (!cfg.allowCgiUser) [
"~CAP_SETGID"
"~CAP_SETUID"
];
SystemCallArchitectures = "native";
SystemCallFilter = [
"~@cpu-emulation @debug @keyring @mount @obsolete"
]
++ lib.lists.optional (!cfg.allowCgiUser) [ "@privileged @setuid" ];
};
};
# Create default cert store
systemd.tmpfiles.rules = lib.mkIf (cfg.store == /var/lib/gemini/certs) [
''d /var/lib/gemini/certs - "${cfg.user}" "${cfg.group}" -''
];
users.users = lib.optionalAttrs (cfg.user == "stargazer") {
stargazer = {
group = cfg.group;
isSystemUser = true;
};
};
users.groups = lib.optionalAttrs (cfg.group == "stargazer") {
stargazer = { };
};
};
meta.maintainers = with lib.maintainers; [ gaykitty ];
}

View File

@@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.static-web-server;
toml = pkgs.formats.toml { };
configFilePath = toml.generate "config.toml" cfg.configuration;
in
{
options = {
services.static-web-server = {
enable = lib.mkEnableOption ''Static Web Server'';
listen = lib.mkOption {
default = "[::]:8787";
type = lib.types.str;
description = ''
The {manpage}`systemd.socket(5)` "ListenStream" used in static-web-server.socket.
This is equivalent to SWS's "host" and "port" options.
See here for specific syntax: <https://www.freedesktop.org/software/systemd/man/systemd.socket.html#ListenStream=>
'';
};
root = lib.mkOption {
type = lib.types.path;
description = ''
The location of files for SWS to serve. Equivalent to SWS's "root" config value.
NOTE: This folder must exist before starting SWS.
'';
};
configuration = lib.mkOption {
default = { };
type = toml.type;
example = {
general = {
log-level = "error";
directory-listing = true;
};
};
description = ''
Configuration for Static Web Server. See
<https://static-web-server.net/configuration/config-file/>.
NOTE: Don't set "host", "port", or "root" here. They will be ignored.
Use the top-level "listen" and "root" options instead.
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.static-web-server ];
systemd.packages = [ pkgs.static-web-server ];
# Have to set wantedBy since systemd.packages ignores the "Install" section
systemd.sockets.static-web-server = {
wantedBy = [ "sockets.target" ];
# Start with empty string to reset upstream option
listenStreams = [
""
cfg.listen
];
};
systemd.services.static-web-server = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
# Remove upstream sample environment file; use config.toml exclusively
EnvironmentFile = [ "" ];
ExecStart = [
""
"${pkgs.static-web-server}/bin/static-web-server --fd 0 --config-file ${configFilePath} --root ${cfg.root}"
];
# Supplementary groups doesn't work unless we create the group ourselves
SupplementaryGroups = [ "" ];
# If the user is serving files from their home dir, override ProtectHome to allow that
ProtectHome = if lib.hasPrefix "/home" cfg.root then "tmpfs" else "true";
BindReadOnlyPaths = cfg.root;
};
};
};
meta.maintainers = with lib.maintainers; [ mac-chaffee ];
}

View File

@@ -0,0 +1,437 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.tomcat;
tomcat = cfg.package;
in
{
meta = {
maintainers = with lib.maintainers; [
danbst
anthonyroussel
];
};
###### interface
options = {
services.tomcat = {
enable = lib.mkEnableOption "Apache Tomcat";
package = lib.mkPackageOption pkgs "tomcat9" {
example = "tomcat10";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = ''
The TCP port Tomcat should listen on.
'';
};
purifyOnStart = lib.mkOption {
type = lib.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 = lib.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 = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.path;
description = "Directories to create in baseDir/logs/";
};
extraConfigFiles = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.path;
description = "Extra configuration files to pull into the tomcat conf directory";
};
extraEnvironment = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "ENVIRONMENT=production" ];
description = "Environment Variables to pass to the tomcat service";
};
extraGroups = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
example = [ "users" ];
description = "Defines extra groups to which the tomcat user belongs.";
};
user = lib.mkOption {
type = lib.types.str;
default = "tomcat";
description = "User account under which Apache Tomcat runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "tomcat";
description = "Group account under which Apache Tomcat runs.";
};
javaOpts = lib.mkOption {
type = lib.types.either (lib.types.listOf lib.types.str) lib.types.str;
default = "";
description = "Parameters to pass to the Java Virtual Machine which spawns Apache Tomcat";
};
catalinaOpts = lib.mkOption {
type = lib.types.either (lib.types.listOf lib.types.str) lib.types.str;
default = "";
description = "Parameters to pass to the Java Virtual Machine which spawns the Catalina servlet container";
};
sharedLibs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List containing JAR files or directories with JAR files which are libraries shared by the web applications";
};
serverXml = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Verbatim server.xml configuration.
This is mutually exclusive with the virtualHosts options.
'';
};
commonLibs = lib.mkOption {
type = lib.types.listOf lib.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 = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ tomcat.webapps ];
defaultText = lib.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 = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "name of the virtualhost";
};
aliases = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "aliases of the virtualhost";
default = [ ];
};
webapps = lib.mkOption {
type = lib.types.listOf lib.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 = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable logging per virtual host.";
};
jdk = lib.mkPackageOption pkgs "jdk" { };
axis2 = {
enable = lib.mkEnableOption "Apache Axis2 container";
services = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
description = "List containing AAR files or directories with AAR files which are web services to be deployed on Axis2";
};
};
};
};
###### implementation
config = lib.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
${lib.optionalString (cfg.extraConfigFiles != [ ]) ''
for i in ${toString cfg.extraConfigFiles}; do
ln -sfn $i ${cfg.baseDir}/conf/`basename $i`
done
''}
# 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">
''
+ lib.concatStrings (innerElementsForVirtualHost virtualHost)
+ ''
</Host>
'';
innerElementsForVirtualHost =
virtualHost:
(map (alias: ''
<Alias>${alias}</Alias>
'') virtualHost.aliases)
++ (lib.optional cfg.logPerVirtualHost ''
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs/${virtualHost.name}"
prefix="${virtualHost.name}_access_log." pattern="combined" resolveHosts="false"/>
'');
hostElementsString = lib.concatMapStringsSep "\n" hostElementForVirtualHost cfg.virtualHosts;
hostElementsSedString = lib.replaceStrings [ "\n" ] [ "\\\n" ] hostElementsString;
in
''
# Create a modified server.xml which listens on the given port,
# and also includes all virtual hosts.
# The host modification must be last here,
# else if hostElementsSedString is empty sed gets confused as to what to append
sed -e 's/<Connector port="8080"/<Connector port="${toString cfg.port}"/' \
-e "/<Engine name=\"Catalina\" defaultHost=\"localhost\">/a\\"${lib.escapeShellArg hostElementsSedString} \
${tomcat}/conf/server.xml > ${cfg.baseDir}/conf/server.xml
''
}
${lib.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
''}
${lib.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 "${lib.optionalString (virtualHost ? webapps) (toString virtualHost.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}/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
)}
${lib.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,168 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.traefik;
format = pkgs.formats.toml { };
dynamicConfigFile =
if cfg.dynamicConfigFile == null then
format.generate "config.toml" cfg.dynamicConfigOptions
else
cfg.dynamicConfigFile;
staticConfigFile =
if cfg.staticConfigFile == null then
format.generate "config.toml" (
recursiveUpdate cfg.staticConfigOptions {
providers.file.filename = "${dynamicConfigFile}";
}
)
else
cfg.staticConfigFile;
finalStaticConfigFile =
if cfg.environmentFiles == [ ] then staticConfigFile else "/run/traefik/config.toml";
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 `staticConfigOptions` and `dynamicConfigOptions`)
'';
};
staticConfigOptions = mkOption {
description = ''
Static configuration for Traefik.
'';
type = format.type;
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 `dynamicConfigOptions`)
'';
};
dynamicConfigOptions = mkOption {
description = ''
Dynamic configuration for Traefik.
'';
type = format.type;
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 `docker` instead.
'';
};
package = mkPackageOption pkgs "traefik" { };
environmentFiles = mkOption {
default = [ ];
type = types.listOf types.path;
example = [ "/run/secrets/traefik.env" ];
description = ''
Files to load as environment file. Environment variables from this file
will be substituted into the static configuration file using envsubst.
'';
};
};
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0700 traefik traefik - -" ];
systemd.services.traefik = {
description = "Traefik web server";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
startLimitIntervalSec = 86400;
startLimitBurst = 5;
serviceConfig = {
EnvironmentFile = cfg.environmentFiles;
ExecStartPre = lib.optional (cfg.environmentFiles != [ ]) (
pkgs.writeShellScript "pre-start" ''
umask 077
${pkgs.envsubst}/bin/envsubst -i "${staticConfigFile}" > "${finalStaticConfigFile}"
''
);
ExecStart = "${cfg.package}/bin/traefik --configfile=${finalStaticConfigFile}";
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";
ReadWritePaths = [ cfg.dataDir ];
RuntimeDirectory = "traefik";
WorkingDirectory = cfg.dataDir;
};
};
users.users.traefik = {
group = "traefik";
home = cfg.dataDir;
createHome = true;
isSystemUser = true;
};
users.groups.traefik = { };
};
}

View File

@@ -0,0 +1,334 @@
{
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 [upstream
documentation](${getManualUrl "cache.config"}) 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 [
upstream documentation](${getManualUrl "hosting.config"}) for more details.
'';
};
ipAllow = mkOption {
type = types.nullOr yaml.type;
default = lib.importJSON ./ip_allow.json;
defaultText = literalMD "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 [upstream
documentation](${getManualUrl "ip_allow.yaml"}) for more details.
'';
};
logging = mkOption {
type = types.nullOr yaml.type;
default = lib.importJSON ./logging.json;
defaultText = literalMD "upstream defaults";
example = { };
description = ''
Configure logs.
Consult the [upstream
documentation](${getManualUrl "logging.yaml"}) 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 [upstream
documentation](${getManualUrl "parent.config"}) for more details.
'';
};
plugins = mkOption {
default = [ ];
description = ''
Controls run-time loadable plugins available to Traffic Server, as
well as their configuration.
Consult the [upstream
documentation](${getManualUrl "plugin.config"}) 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 [
upstream documentation](${getManualUrl "records.config"}) 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 [
upstream documentation](${getManualUrl "remap.config"}) 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 [
upstream documentation](${getManualUrl "splitdns.config"}) 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 [
upstream documentation](${getManualUrl "ssl_multicert.config"}) 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 [upstream
documentation](${getManualUrl "sni.yaml"}) 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 [
upstream documentation](${getManualUrl "storage.config"}) 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 [
upstream documentation](${getManualUrl "strategies.yaml"}) 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 [
upstream documentation](${getManualUrl "volume.config"}) 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,284 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ttyd;
inherit (lib)
optionals
types
mkOption
;
# 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)
]
++ (lib.concatLists (
lib.mapAttrsToList (_k: _v: [
"--client-option"
"${_k}=${_v}"
]) cfg.clientOptions
))
++ [
"--terminal-type"
cfg.terminalType
]
++ optionals cfg.checkOrigin [ "--check-origin" ]
++ optionals cfg.writeable [ "--writable" ] # the typo is correct
++ [
"--max-clients"
(toString cfg.maxClients)
]
++ optionals (cfg.indexFile != null) [
"--index"
cfg.indexFile
]
++ optionals cfg.enableIPv6 [ "--ipv6" ]
++ optionals cfg.enableSSL [
"--ssl"
"--ssl-cert"
cfg.certFile
"--ssl-key"
cfg.keyFile
]
++ optionals (cfg.enableSSL && cfg.caFile != null) [
"--ssl-ca"
cfg.caFile
]
++ [
"--debug"
(toString cfg.logLevel)
];
in
{
###### interface
options = {
services.ttyd = {
enable = lib.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 http 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 http authentication.
For insecurely putting the password in the globally readable store use
`pkgs.writeText "ttydpw" "MyPassword"`.
'';
};
signal = mkOption {
type = types.ints.u8;
default = 1;
description = "Signal to send to the command on session close.";
};
entrypoint = mkOption {
type = types.listOf types.str;
default = [ "${pkgs.shadow}/bin/login" ];
defaultText = lib.literalExpression ''
[ "''${pkgs.shadow}/bin/login" ]
'';
example = lib.literalExpression ''
[ (lib.getExe pkgs.htop) ]
'';
description = "Which command ttyd runs.";
apply = lib.escapeShellArgs;
};
user = mkOption {
type = types.str;
# `login` needs to be run as root
default = "root";
description = "Which unix user ttyd should run as.";
};
writeable = mkOption {
type = types.nullOr types.bool;
default = null; # null causes an eval error, forcing the user to consider attack surface
example = true;
description = "Allow clients to write to the TTY.";
};
clientOptions = mkOption {
type = types.attrsOf types.str;
default = { };
example = lib.literalExpression ''
{
fontSize = "16";
fontFamily = "Fira Code";
}
'';
description = ''
Attribute set of client options for xtermjs.
<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
`pkgs.writeText "ttydKeyFile" "SSLKEY"`.
'';
};
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 = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.enableSSL -> cfg.certFile != null && cfg.keyFile != null;
message = "SSL is enabled for ttyd, but no certFile or keyFile has been specified.";
}
{
assertion = cfg.writeable != null;
message = "services.ttyd.writeable must be set";
}
{
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 = {
User = cfg.user;
LoadCredential = lib.optionalString (
cfg.passwordFile != null
) "TTYD_PASSWORD_FILE:${cfg.passwordFile}";
};
script =
if cfg.passwordFile != null then
''
PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE")
${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
--credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \
${cfg.entrypoint}
''
else
''
${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
${cfg.entrypoint}
'';
};
};
}

View File

@@ -0,0 +1,163 @@
{
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 = mkPackageOption pkgs "unit" { };
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' --statedir '${cfg.stateDir}' --tmpdir '/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,246 @@
{
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';
hasPython3 = filter (n: n == "python3") plugins != [ ];
python = 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 `type` inside which can be either
`normal` or `emperor`.
For `normal` mode you can specify `pythonPackages` as a function
from libraries set into a list of libraries. `pythonpath` will be set accordingly.
For `emperor` mode, you should use `vassals` 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
`plugins` 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
{manpage}`capabilities(7)` for available values.
::: {.note}
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.
When in Emperor mode, any capability to be inherited by a vassal must
be specified again in the vassal configuration using `cap`.
See the uWSGI [docs](https://uwsgi-docs.readthedocs.io/en/latest/Capabilities.html)
for more information.
:::
'';
};
};
};
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,248 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
types
mkOption
hasPrefix
concatMapStringsSep
optionalString
concatMap
;
inherit (builtins) isNull;
cfg = config.services.varnish;
# Varnish has very strong opinions and very complicated code around handling
# the stateDir. After a lot of back and forth, we decided that we a)
# do not want a configurable option here, as most of the handling depends
# on the version and the compile time options. Putting everything into
# /var/run (RAM backed) is absolutely recommended by Varnish anyways.
# We do need to pay attention to the version-dependend variations, though!
stateDir =
if
(lib.versionOlder cfg.package.version "7")
# Remove after Varnish 6.0 is gone. In 6.0 varnishadm always appends the
# hostname (by default) and can't be nudged to not use any name. This has
# long changed by 7.5 and can be used without the host name.
then
"/var/run/varnish/${config.networking.hostName}"
# Newer varnish uses this:
else
"/var/run/varnishd";
# from --help:
# -a [<name>=]address[:port][,proto] # HTTP listen address and port
# [,user=<u>][,group=<g>] # Can be specified multiple times.
# [,mode=<m>] # default: ":80,HTTP"
# # Proto can be "PROXY" or "HTTP" (default)
# # user, group and mode set permissions for
# # a Unix domain socket.
commandLineAddresses =
(concatMapStringsSep " " (
a:
"-a "
+ optionalString (!isNull a.name) "${a.name}="
+ a.address
+ optionalString (!isNull a.port) ":${toString a.port}"
+ optionalString (!isNull a.proto) ",${a.proto}"
+ optionalString (!isNull a.user) ",user=${a.user}"
+ optionalString (!isNull a.group) ",group=${a.group}"
+ optionalString (!isNull a.mode) ",mode=${a.mode}"
) cfg.listen)
+ lib.optionalString (!isNull cfg.http_address) " -a ${cfg.http_address}";
addressSubmodule = types.submodule {
options = {
name = mkOption {
description = "Name is referenced in logs. If name is not specified, 'a0', 'a1', etc. is used.";
default = null;
type = with types; nullOr str;
};
address = mkOption {
description = ''
If given an IP address, it can be a host name ("localhost"), an IPv4 dotted-quad
("127.0.0.1") or an IPv6 address enclosed in square brackets ("[::1]").
(VCL4.1 and higher) If given an absolute Path ("/path/to/listen.sock") or "@"
followed by the name of an abstract socket ("@myvarnishd") accept connections
on a Unix domain socket.
The user, group and mode sub-arguments may be used to specify the permissions
of the socket file. These sub-arguments do not apply to abstract sockets.
'';
type = types.str;
};
port = mkOption {
description = "The port to use for IP sockets. If port is not specified, port 80 (http) is used.";
default = null;
type = with types; nullOr port;
};
proto = mkOption {
description = "PROTO can be 'HTTP' (the default) or 'PROXY'. Both version 1 and 2 of the proxy protocol can be used.";
type = types.enum [
"HTTP"
"PROXY"
];
default = "HTTP";
};
user = mkOption {
description = "User name who owns the socket file.";
default = null;
type = with lib.types; nullOr str;
};
group = mkOption {
description = "Group name who owns the socket file.";
default = null;
type = with lib.types; nullOr str;
};
mode = mkOption {
description = "Permission of the socket file (3-digit octal value).";
default = null;
type = with types; nullOr str;
};
};
};
checkedAddressModule = types.addCheck addressSubmodule (
m:
(
if ((hasPrefix "@" m.address) || (hasPrefix "/" m.address)) then
# this is a unix socket
(m.port != null)
else
# this is not a path-based unix socket
if !(hasPrefix "/" m.address) && (m.group != null) || (m.user != null) || (m.mode != null) then
false
else
true
)
);
commandLine =
"-f ${pkgs.writeText "default.vcl" cfg.config}"
+
lib.optionalString (cfg.extraModules != [ ])
" -p vmod_path='${
lib.makeSearchPathOutput "lib" "lib/varnish/vmods" ([ cfg.package ] ++ cfg.extraModules)
}' -r vmod_path";
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"varnish"
"stateDir"
] "The `stateDir` option never was functional or useful. varnish uses compile-time settings.")
];
options = {
services.varnish = {
enable = lib.mkEnableOption "Varnish Server";
enableConfigCheck = lib.mkEnableOption "checking the config during build time" // {
default = true;
};
package = lib.mkPackageOption pkgs "varnish" { };
http_address = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
HTTP listen address and port.
'';
};
listen = lib.mkOption {
description = "Accept for client requests on the specified listen addresses.";
type = lib.types.listOf checkedAddressModule;
defaultText = lib.literalExpression ''[ { address="*"; port=6081; } ]'';
default = lib.optional (isNull cfg.http_address) {
address = "*";
port = 6081;
};
};
config = lib.mkOption {
type = lib.types.lines;
description = ''
Verbatim default.vcl configuration.
'';
};
extraModules = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression "[ pkgs.varnishPackages.geoip ]";
description = ''
Varnish modules (except 'std').
'';
};
extraCommandLine = lib.mkOption {
type = lib.types.str;
default = "";
example = "-s malloc,256M";
description = ''
Command line switches for varnishd (run 'varnishd -?' to get list of options)
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.varnish = {
description = "Varnish";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
PermissionsStartOnly = true;
ExecStart = "${cfg.package}/sbin/varnishd ${commandLineAddresses} -n ${stateDir} -F ${cfg.extraCommandLine} ${commandLine}";
Restart = "always";
RestartSec = "5s";
User = "varnish";
Group = "varnish";
RuntimeDirectory = lib.removePrefix "/var/run/" stateDir;
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.checks = lib.mkIf cfg.enableConfigCheck [
(pkgs.runCommand "check-varnish-syntax" { } ''
${cfg.package}/bin/varnishd -C ${commandLine} 2> $out || (cat $out; exit 1)
'')
];
assertions = concatMap (m: [
{
assertion = (hasPrefix "/" m.address) || (hasPrefix "@" m.address) -> m.port == null;
message = "Listen ports must not be specified with UNIX sockets: ${builtins.toJSON m}";
}
{
assertion = !(hasPrefix "/" m.address) -> m.user == null && m.group == null && m.mode == null;
message = "Abstract UNIX sockets or IP sockets can not be used with user, group, and mode settings: ${builtins.toJSON m}";
}
]) cfg.listen;
warnings =
lib.optional (!isNull cfg.http_address)
"The option `services.varnish.http_address` is deprecated. Use `services.varnish.listen` instead.";
users.users.varnish = {
group = "varnish";
uid = config.ids.uids.varnish;
};
users.groups.varnish.gid = config.ids.uids.varnish;
};
}