summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorRok Garbas <rok@garbas.si>2016-08-01 13:10:06 +0200
committerGitHub <noreply@github.com>2016-08-01 13:10:06 +0200
commit34237beca6206475dec1f1c68edf227401793382 (patch)
tree7762e265bda491d501ad8671c70eaa70e7b1c29d /nixos
parentc91d07b6689c9f7bd90622a0d31530005c02256d (diff)
parenta193fecf0ef3fb2d048981217d6de7a051212e44 (diff)
downloadnixpkgs-34237beca6206475dec1f1c68edf227401793382.tar
nixpkgs-34237beca6206475dec1f1c68edf227401793382.tar.gz
nixpkgs-34237beca6206475dec1f1c68edf227401793382.tar.bz2
nixpkgs-34237beca6206475dec1f1c68edf227401793382.tar.lz
nixpkgs-34237beca6206475dec1f1c68edf227401793382.tar.xz
nixpkgs-34237beca6206475dec1f1c68edf227401793382.tar.zst
nixpkgs-34237beca6206475dec1f1c68edf227401793382.zip
Merge pull request #15862 from mayflower/nginx-module
Declarative nginx module with ACME support
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix285
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix40
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix140
3 files changed, 455 insertions, 10 deletions
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 27a33f33ff9..8385d8e6026 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -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 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 the nginx Web Server.
+          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;
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
new file mode 100644
index 00000000000..ce3462bed0a
--- /dev/null
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -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.
+      '';
+    };
+  };
+}
+
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
new file mode 100644
index 00000000000..ee3f68bf805
--- /dev/null
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -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";
+    };
+  };
+}