Merge pull request #15862 from mayflower/nginx-module

Declarative nginx module with ACME support
This commit is contained in:
Rok Garbas 2016-08-01 13:10:06 +02:00 committed by GitHub
commit 34237beca6
3 changed files with 455 additions and 10 deletions

View File

@ -4,31 +4,222 @@ with lib;
let
cfg = config.services.nginx;
nginx = cfg.package;
virtualHosts = mapAttrs (vhostName: vhostConfig:
vhostConfig // (optionalAttrs vhostConfig.enableACME {
sslCertificate = "/var/lib/acme/${vhostName}/fullchain.pem";
sslCertificateKey = "/var/lib/acme/${vhostName}/key.pem";
})
) cfg.virtualHosts;
configFile = pkgs.writeText "nginx.conf" ''
user ${cfg.user} ${cfg.group};
error_log stderr;
daemon off;
${cfg.config}
${optionalString (cfg.httpConfig == "") ''
http {
include ${cfg.package}/conf/mime.types;
include ${cfg.package}/conf/fastcgi.conf;
${optionalString (cfg.recommendedOptimisation) ''
# optimisation
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
''}
ssl_protocols ${cfg.sslProtocols};
ssl_ciphers ${cfg.sslCiphers};
${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
${optionalString (cfg.recommendedTlsSettings) ''
ssl_session_cache shared:SSL:42m;
ssl_session_timeout 23m;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
''}
${optionalString (cfg.recommendedGzipSettings) ''
gzip on;
gzip_disable "msie6";
gzip_proxied any;
gzip_comp_level 9;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
''}
${optionalString (cfg.recommendedProxySettings) ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header Accept-Encoding "";
proxy_redirect off;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_http_version 1.0;
''}
client_max_body_size ${cfg.clientMaxBodySize};
server_tokens ${if cfg.serverTokens then "on" else "off"};
${vhosts}
${optionalString cfg.statusPage ''
server {
listen 80;
listen [::]:80;
server_name localhost;
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
allow ::1;
deny all;
}
}
''}
${cfg.appendHttpConfig}
}''}
${optionalString (cfg.httpConfig != "") ''
http {
include ${cfg.package}/conf/mime.types;
include ${cfg.package}/conf/fastcgi.conf;
${cfg.httpConfig}
}
''}
}''}
${cfg.appendConfig}
'';
vhosts = concatStringsSep "\n" (mapAttrsToList (serverName: vhost:
let
ssl = vhost.enableSSL || vhost.forceSSL;
port = if vhost.port != null then vhost.port else (if ssl then 443 else 80);
listenString = toString port + optionalString ssl " ssl http2"
+ optionalString vhost.default " default";
acmeLocation = optionalString vhost.enableACME ''
location /.well-known/acme-challenge {
try_files $uri @acme-fallback;
root ${vhost.acmeRoot};
auth_basic off;
}
location @acme-fallback {
auth_basic off;
proxy_pass http://${vhost.acmeFallbackHost};
}
'';
in ''
${optionalString vhost.forceSSL ''
server {
listen 80 ${optionalString vhost.default "default"};
listen [::]:80 ${optionalString vhost.default "default"};
server_name ${serverName} ${concatStringsSep " " vhost.serverAliases};
${acmeLocation}
location / {
return 301 https://$host${optionalString (port != 443) ":${port}"}$request_uri;
}
}
''}
server {
listen ${listenString};
listen [::]:${listenString};
server_name ${serverName} ${concatStringsSep " " vhost.serverAliases};
${acmeLocation}
${optionalString (vhost.root != null) "root ${vhost.root};"}
${optionalString (vhost.globalRedirect != null) ''
return 301 http${optionalString ssl "s"}://${vhost.globalRedirect}$request_uri;
''}
${optionalString ssl ''
ssl_certificate ${vhost.sslCertificate};
ssl_certificate_key ${vhost.sslCertificateKey};
''}
${optionalString (vhost.basicAuth != {}) (mkBasicAuth serverName vhost.basicAuth)}
${mkLocations vhost.locations}
${vhost.extraConfig}
}
''
) virtualHosts);
mkLocations = locations: concatStringsSep "\n" (mapAttrsToList (location: config: ''
location ${location} {
${optionalString (config.proxyPass != null) "proxy_pass ${config.proxyPass};"}
${optionalString (config.root != null) "root ${config.root};"}
${config.extraConfig}
}
'') locations);
mkBasicAuth = serverName: authDef: let
htpasswdFile = pkgs.writeText "${serverName}.htpasswd" (
concatStringsSep "\n" (mapAttrsToList (user: password: ''
${user}:{PLAIN}${password}
'') authDef)
);
in ''
auth_basic secured;
auth_basic_user_file ${htpasswdFile};
'';
in
{
options = {
services.nginx = {
enable = mkOption {
enable = mkEnableOption "Nginx Web Server";
statusPage = mkOption {
default = false;
type = types.bool;
description = "
Enable the nginx Web Server.
Enable status page reachable from localhost on http://127.0.0.1/nginx_status.
";
};
recommendedTlsSettings = mkOption {
default = false;
type = types.bool;
description = "
Enable recommended TLS settings.
";
};
recommendedOptimisation = mkOption {
default = false;
type = types.bool;
description = "
Enable recommended optimisation settings.
";
};
recommendedGzipSettings = mkOption {
default = false;
type = types.bool;
description = "
Enable recommended gzip settings.
";
};
recommendedProxySettings = mkOption {
default = false;
type = types.bool;
description = "
Enable recommended proxy settings.
";
};
@ -64,7 +255,22 @@ in
httpConfig = mkOption {
type = types.lines;
default = "";
description = "Configuration lines to be appended inside of the http {} block.";
description = "
Configuration lines to be set inside the http block.
This is mutually exclusive with the structured configuration
via virtualHosts and the recommendedXyzSettings configuration
options. See appendHttpConfig for appending to the generated http block.
";
};
appendHttpConfig = mkOption {
type = types.lines;
default = "";
description = "
Configuration lines to be appended to the generated http block.
This is mutually exclusive with using httpConfig for specifying the whole
http block verbatim.
";
};
stateDir = mkOption {
@ -86,8 +292,59 @@ in
description = "Group account under which nginx runs.";
};
};
serverTokens = mkOption {
type = types.bool;
default = false;
description = "Show nginx version in headers and error pages.";
};
clientMaxBodySize = mkOption {
type = types.string;
default = "10m";
description = "Set nginx global client_max_body_size.";
};
sslCiphers = mkOption {
type = types.str;
default = "EECDH+aRSA+AESGCM:EDH+aRSA:EECDH+aRSA:+AES256:+AES128:+SHA1:!CAMELLIA:!SEED:!3DES:!DES:!RC4:!eNULL";
description = "Ciphers to choose from when negotiating tls handshakes.";
};
sslProtocols = mkOption {
type = types.str;
default = "TLSv1.2";
example = "TLSv1 TLSv1.1 TLSv1.2";
description = "Allowed TLS protocol versions.";
};
sslDhparam = mkOption {
type = types.nullOr types.path;
default = null;
example = "/path/to/dhparams.pem";
description = "Path to DH parameters file.";
};
virtualHosts = mkOption {
type = types.attrsOf (types.submodule (import ./vhost-options.nix {
inherit lib;
}));
default = {
localhost = {};
};
example = literalExample ''
{
"hydra.example.com" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:3000";
};
};
};
'';
description = "Declarative vhost config";
};
};
};
config = mkIf cfg.enable {
@ -97,7 +354,6 @@ in
description = "Nginx Web Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ nginx ];
preStart =
''
mkdir -p ${cfg.stateDir}/logs
@ -105,14 +361,23 @@ in
chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}
'';
serviceConfig = {
ExecStart = "${nginx}/bin/nginx -c ${configFile} -p ${cfg.stateDir}";
ExecStart = "${cfg.package}/bin/nginx -c ${configFile} -p ${cfg.stateDir}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Restart = "on-failure";
Restart = "always";
RestartSec = "10s";
StartLimitInterval = "1min";
};
};
security.acme.certs = filterAttrs (n: v: v != {}) (
mapAttrs (vhostName: vhostConfig:
optionalAttrs vhostConfig.enableACME {
webroot = vhostConfig.acmeRoot;
extraDomains = genAttrs vhostConfig.serverAliases (alias: null);
}
) virtualHosts
);
users.extraUsers = optionalAttrs (cfg.user == "nginx") (singleton
{ name = "nginx";
group = cfg.group;

View File

@ -0,0 +1,40 @@
# This file defines the options that can be used both for the Apache
# main server configuration, and for the virtual hosts. (The latter
# has additional options that affect the web server as a whole, like
# the user/group to run under.)
{ lib }:
with lib;
{
options = {
proxyPass = mkOption {
type = types.nullOr types.str;
default = null;
example = "http://www.example.org/";
description = ''
Adds proxy_pass directive and sets default proxy headers Host, X-Real-Ip
and X-Forwarded-For.
'';
};
root = mkOption {
type = types.nullOr types.path;
default = null;
example = /your/root/directory;
description = ''
Root directory for requests.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
These lines go to the end of the location verbatim.
'';
};
};
}

View File

@ -0,0 +1,140 @@
# This file defines the options that can be used both for the Apache
# main server configuration, and for the virtual hosts. (The latter
# has additional options that affect the web server as a whole, like
# the user/group to run under.)
{ lib }:
with lib;
{
options = {
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.
'';
};
port = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
Port for the server. Defaults to 80 for http
and 443 for https (i.e. when enableSSL is set).
'';
};
enableACME = mkOption {
type = types.bool;
default = false;
description = "Whether to ask Let's Encrypt to sign a certificate for this vhost.";
};
acmeRoot = mkOption {
type = types.str;
default = "/var/lib/acme/acme-challenge";
description = "Directory to store certificates and keys managed by the ACME service.";
};
acmeFallbackHost = mkOption {
type = types.str;
default = "0.0.0.0";
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.
'';
};
enableSSL = mkOption {
type = types.bool;
default = false;
description = "Whether to enable SSL (https) support.";
};
forceSSL = mkOption {
type = types.bool;
default = false;
description = "Whether to always redirect to https.";
};
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.";
};
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 = http://newserver.example.org/;
description = ''
If set, all requests for this host are redirected permanently to
the given URL.
'';
};
basicAuth = mkOption {
type = types.attrsOf types.str;
default = {};
example = literalExample ''
{
user = "password";
};
'';
description = ''
Basic Auth protection for a vhost.
WARNING: This is implemented to store the password in plain text in the
nix store.
'';
};
locations = mkOption {
type = types.attrsOf (types.submodule (import ./location-options.nix {
inherit lib;
}));
default = {};
example = literalExample ''
{
"/" = {
proxyPass = "http://localhost:3000";
};
};
'';
description = "Declarative location config";
};
};
}