Nginx

From NixOS Wiki
Revision as of 17:53, 15 November 2019 by Bachp (talk | contribs) (Update phpfpm config to use settings instead of extraConfig)
Jump to: navigation, search

Nginx is a lightweight webserver. Configuration is handled using the services.nginx. options.

Let's Encrypt certificates

The nginx module for NixOS has native support for Let's encrypt certificates; services.nginx.+acme. The NixOS Manual, Chapter 20. SSL/TLS Certificates with ACME explains it in detail.

Minimal Example

Assuming that myhost.org resolves to the ip address of your host and port 80 and 443 has been opened.

services.nginx.enable = true;
services.nginx.virtualHosts."myhost.org" = {
    addSSL = true;
    enableACME = true;
    root = "/var/www/myhost.org";
};

This will set up nginx to serve files for myhost.org, automatically request an ACME SSL Certificate and will configure systemd timers to renew the certificate if required.

Troubleshooting

Rate limiting

The ACME server for Let's encrypt has rate limits. There is a known issue[1] with how NixOS handles automatic certificate generation wherein it is trivial to hit the limits when enabling multiple domains or sub-domains at once.

When hitting the limit, the logs will show as follows:

Mar 30 14:07:38 HOSTNAME systemd[1]: Failed to start Renew ACME Certificate for example.com.
...
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]: 2018-03-30 18:08:10,566:DEBUG:acme.client:540: JWS payload:
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]: {
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]:   "resource": "new-reg"
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]: }
...
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]: Connection: close
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]: {
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]:   "type": "urn:acme:error:rateLimited",
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]:   "detail": "Error creating new registration :: too many registrations for this IP: see https://letsencrypt.org/docs/rate-limits/",
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]:   "status": 429
Mar 30 14:08:10 HOSTNAME acme-example.com-start[25915]: }

See Issue.png#38144 for the current status.

Sample setups

Static blog with ssl enforced in configuration.nix

services.nginx = {
  enable = true;
  virtualHosts."blog.example.com" = {
    enableACME = true;
    forceSSL = true;
    root = "/var/www/blog";
  };
};
# Optional: You can configure the email address used with Let's Encrypt.
# This way you get renewal reminders (automated by NixOS) as well as expiration emails.
security.acme.certs = {
  "blog.example.com".email = "youremail@address.com";
};

LEMP stack (Nginx/MySQL/PHP) in configuration.nix

{ config, ...}: {
services.nginx = {
  enable = true;
  virtualHosts."blog.example.com" = {
    enableACME = true;
    forceSSL = true;
    root = "/var/www/blog";
    locations."~ \.php$".extraConfig = ''
      fastcgi_pass  unix:${config.services.phpfpm.pools.mypool.socket};
      fastcgi_index index.php;
    '';
  };
};
services.mysql = {
  enable = true;
  package = pkgs.mariadb;
};
services.phpfpm.pools.mypool = {                                                                                                                                                                                                             
  user = "nobody";                                                                                                                                                                                                                           
  settings = {                                                                                                                                                                                                                               
    pm = "dynamic";                                                                                                                                                                                                                          
    "pm.max_children" = 5;                                                                                                                                                                                                                   
    "pm.start_servers" = 2;                                                                                                                                                                                                                  
    "pm.min_spare_servers" = 1;                                                                                                                                                                                                              
    "pm.max_spare_servers" = 3;                                                                                                                                                                                                              
    "pm.max_requests" = 500;                                                                                                                                                                                                                 
  };                                                                                                                                                                                                                                         
};


Hardened setup with TLS and HSTS preloading:

For testing your TLS configuration, you might want to visit [1]. If you configured preloading and want to apply for being included in the preloading list, check out [2]. Please read enough about preloading to understand the consequences, as it takes some effort to be removed from the list.

services.nginx = {
    enable = true;

    # Use recommended settings
    recommendedGzipSettings = true;
    recommendedOptimisation = true;
    recommendedProxySettings = true;
    recommendedTlsSettings = true;

    # Only allow PFS-enabled ciphers with AES256
    sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";
    
    commonHttpConfig = ''
      # Add HSTS header with preloading to HTTPS requests.
      # Adding this header to HTTP requests is discouraged
      map $scheme $hsts_header {
          https   "max-age=31536000; includeSubdomains; preload";
      }
      add_header Strict-Transport-Security $hsts_header;

      # Enable CSP for your services.
      #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;

      # Minimize information leaked to other domains
      add_header 'Referrer-Policy' 'origin-when-cross-origin';

      # Disable embedding as a frame
      add_header X-Frame-Options DENY;

      # Prevent injection of code in other mime types (XSS Attacks)
      add_header X-Content-Type-Options nosniff;

      # Enable XSS protection of the browser.
      # May be unnecessary when CSP is configured properly (see above)
      add_header X-XSS-Protection "1; mode=block";

      # This might create errors
      proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
    '';

    # Add any further config to match your needs, e.g.:
    virtualHosts = let
      base = locations: {
        inherit locations;

        forceSSL = true;
        enableACME = true;
      };
      proxy = port: base {
        "/".proxyPass = "http://127.0.0.1:" + toString(port) + "/";
      };
    in {
      # Define example.com as reverse-proxied service on 127.0.0.1:3000
      "example.com" = proxy 3000 // { default = true; };
    };
};

Correct Caching when Serving Static Files from /nix/store

Since the dates for all files in /nix/store are set to 1 second after the unix epoch, attempting to serve them over nginx can result in caching issues. etags can be used to resolve this, but nginx's built-in etags depend on the file modification time and size, which isn't good enough for us. To have caching work reliably, we'll construct our own etag:

locations."/my/static/file.txt" =
  let file = pkgs.writeText "file.txt" "this is a static file!";
  in 
  { alias = file;
    extraConfig = ''
      etag off;
      add_header etag "\"${builtins.substring 11 32 file.outPath}\"";
      '';
  }