Nginx installation guide


Prerequisite

This guide is written for Debian Stretch. Other Debian based distributions should work as well.

While not mandatory, the guide makes use of the following programs to enhance the security of the installation

Install

Nginx

$ sudo apt install nginx-light libnginx-mod-http-headers-more-filter

nginx_modsite

nginx_modsite is a script that allows to activate or deactivate a site simply, without having to handle symlinks manually. In Debian, it is distributed in source form as part of the nginx-doc package. The easiest is to download it directly from the source repository:

$ sudo curl -o /usr/local/sbin/nginx_modsite "https://anonscm.debian.org/cgit/collab-maint/nginx.git/plain/debian/help/examples/nginx_modsite"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  4625  100  4625    0     0  12836      0 --:--:-- --:--:-- --:--:-- 12847
$ sudo chmod +x /usr/local/sbin/nginx_modsite

Configure

Replace file /etc/nginx/nginx.conf with:

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
  worker_connections 768;
  # multi_accept on;
}

http {
  ##
  # Basic Settings
  ##
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  ##
  # Logging Settings
  ##
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  ##
  # Virtual Host Configs
  ##
  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}

conf.d

The conf.d folder stores shared configuration shared between all the sites hosted on your server.

Create the following files:

  • /etc/nginx/conf.d/dns.conf
    # DNS resolver
    # It is required for OCSP Stapling. It might also be used if you use a hostname for upstream servers
    resolver 127.0.0.1;
    # If you don't have a DNS resolver on your machine you can use google public ones instead
    #resolver 8.8.8.8 8.8.4.4;
    
  • /etc/nginx/conf.d/gzip.conf
    gzip on;
    
    # Insert header "Vary: Accept-Encoding" in responses
    # https://www.maxcdn.com/blog/accept-encoding-its-vary-important/
    gzip_vary on;
    
    gzip_comp_level 6;
    
    gzip_proxied any;
    
    gzip_min_length 500;
    
    gzip_types
      application/atom+xml
      application/atom_xml
      application/javascript
      application/json
      application/ld+json
      application/manifest+json
      application/rss+xml
      application/text
      application/vnd.geo+json
      application/vnd.microsoft.icon
      application/vnd.ms-fontobject
      application/x-json
      application/x-font-opentype
      application/x-font-truetype
      application/x-font-ttf
      application/x-javascript
      application/x-web-app-manifest+json
      application/xhtml+xml
      application/xml
      application/xml+rss
      font/eot
      font/opentype
      font/otf
      image/bmp
      image/svg+xml
      image/vnd.microsoft.icon
      image/x-icon
      text/cache-manifest
      text/css
      text/javascript
      text/plain
      text/vcard
      text/vnd.rim.location.xloc
      text/vtt
      text/x-component
      text/x-cross-domain-policy
      text/xml;
    
  • /etc/nginx/conf.d/php.conf
    See documentation to install PHP.
  • /etc/nginx/conf.d/server_tokens.conf
    # Hide nginx version
    # This doesn't provides any real security but makes hackers life a bit more difficult
    server_tokens off;
    more_clear_headers Server;
    
  • /etc/nginx/conf.d/ssl.conf
    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;
    
    # Cipher list from https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
    ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256";
    
    # If you have a version of openssl < 1.1.0, you need to remove X25519 from the list
    ssl_ecdh_curve X25519:secp256k1:secp384r1;
    
    # Support OSCP Stapling. Check that resolver from in dns.conf is working
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    
    # Support SSL session cache
    ssl_session_timeout 1d;
    ssl_session_cache shared:NginxCache:50m;
    ssl_session_tickets off; # https://timtaubert.de/blog/2014/11/the-sad-state-of-server-side-tls-session-resumption-implementations/
    

snippets

The snippets folder allows you to store bits of configuration that you can later include in virtual hosts configuration.This saves a lot of typing and errors when creating a new site.

  • /etc/nging/conf.d/acme-challenge.conf
    See Let’s Encrypt
  • /etc/nging/conf.d/hsts.conf
    # The standard add_header from Nginx has two issues:
    #  - it will result in duplicate headers if the proxied content set it as well
    #  - if a subblock uses add_header as well, parent block headers are ignored
    # Using more_set_headers fixes both issues
    
    # WARNING
    # Make sure you have HTTPS correctly setup before including this file.
    # Failing to do so will render your site inaccessible.
    
    # Activate HTTP Strict Transport Security
    # max-age value is in seconds. 63072000 is 2 years
    more_set_headers "Strict-Transport-Security: max-age=63072000; includeSubDomains";
    
    # If subdomains are still using insecure HTTP, remove the includeSubDomains:
    # more_set_headers "Strict-Transport-Security: max-age=63072000";
    
  • /etc/nginx/snippets/security-headers.conf
    # Some safe security headers that can almost always be used
    
    # The standard add_header from Nginx has two issues:
    #  - it will result in duplicate headers if the proxied content set it as well
    #  - if a subblock uses add_header as well, parent block headers are ignored
    # Using more_set_headers fixes both issues
    
    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
    more_set_headers "X-XSS-Protection: 1; mode=block";
    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
    more_set_headers "X-Content-Type-Options: nosniff";
    # Prevent access from flash and PDF
    more_set_headers "X-Permitted-Cross-Domain-Policies: none";
    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
    more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
    
  • /etc/nginx/snippets/x-frame-options-deny.conf
    /etc/nginx/snippets/x-frame-options-sameorigin.conf
    # The standard add_header from Nginx has two issues:
    #  - it will result in duplicate headers if the proxied content set it as well
    #  - if a subblock uses add_header as well, parent block headers are ignored
    # Using more_set_headers fixes both issues
    
    # Prevent all usages of the website in an iframe.
    # Warning: This might break the site if it uses iframes for internal
    # functionalities. You might want to use the less strict
    # x-frame-options-sameorigin.conf in that case.
    more_set_headers "X-Frame-Options: DENY";
    
    # The standard add_header from Nginx has two issues:
    #  - it will result in duplicate headers if the proxied content set it as well
    #  - if a subblock uses add_header as well, parent block headers are ignored
    # Using more_set_headers fixes both issues
    
    # Prevent usage of the website in an iframe from other domains.
    # Warning: This will still allow iframe on your own domain.
    # For a more strict policy, use x-frame-options-deny.conf
    more_set_headers "X-Frame-Options: SAMEORIGIN";
    
  • /etc/nginx/snippets/https-permanent-redirect.conf
    # Reply to the browser with a permanent redirect to the secure version of the page
    # Wrapped in a location block so that other snippets (acme-challenge.conf) can override that.
    location / {
        return 301 https://$host$request_uri;
    }
    
  • /etc/nginx/snippets/listen-http.conf
    /etc/nginx/snippets/listen-https.conf
    Obviously, you need to replace the example IP addresses by the one of your server. You can get the IP of your server with the commands curl https://ipv6.meurisse.org and curl https://ipv4.meurisse.org.
    listen [2001:db8:3:47d0::2e:7]:80;
    listen 203.0.113.23:80;
    
    listen [2001:db8:3:47d0::2e:7]:443 ssl http2;
    listen 203.0.113.23:443 ssl http2;
    
  • /etc/nginx/snippets/ssl.conf
    ssl on;
    ssl_stapling on;
    
    # The standard add_header from Nginx has two issues:
    #  - it will result in duplicate headers if the proxied content set it as well
    #  - if a subblock uses add_header as well, parent block headers are ignored
    # Using more_set_headers fixes both issues
    
    more_set_headers 'Expect-CT: max-age=86400';
    

HTTP Auth

Install

$ sudo apt install apache2-utils

Create Password File

If the folder doesn't exist, you need to create it using

$ sudo mkdir /etc/nginx/htpasswd
$ sudo chmod 710 /etc/nginx/htpasswd
$ sudo chown root:www-data /etc/nginx/htpasswd

The create the user file

$ sudo touch /etc/nginx/htpasswd/generic.htpasswd

If you want different website to have different users, you can create as many password files as you want.

Add User

$ sudo htpasswd /etc/nginx/htpasswd/generic.htpasswd jdoe
New password: 
Re-type new password: 
Adding password for user jdoe

To update a password user, just run the same command.

Nginx will pick the modified file automatically. There is nothing to restart.

Use

To restrict access to a site or part of it, add the following lines to a server or location config

auth_basic "You shall not pass!";
auth_basic_user_file /etc/nginx/htpasswd/generic.htpasswd;

Firewall

You need to open TCP ports 80 and 443 in your firewall. Assuming that you configured nftables as described, you can edit file /etc/nftables/main_config.conf and add

# Web
add element  inet main  tcp_port_in { 80, 443 }

and activate it using

$ sudo /etc/nftables/reload_main.conf

httpoxy

The httpoxy security flow is a flow targeting CGI scripts using the Proxy HTTP header. It is possible to mitigate it by filtering out this header in fastcgi and proxy calls in Nginx.

Edit files /etc/nginx/fastcgi.conf and /etc/nginx/fastcgi_params and add these lines

# httpoxy.org
fastcgi_param HTTP_PROXY "";

Also edit file /etc/nginx/proxy_params add add these lines

# httpoxy.org
proxy_set_header Proxy "";

/var/www/ permissions

Setting the setgid bit on the /var/www/ allows to make sure that new files are readable by Nginx.

$ sudo chmod 2750 /var/www/
$ sudo chown root:www-data /var/www/

This also revoke the default read permission to user outside the www-data group. They don't need it and some data might not be public here.

New Site

This section shows how to create a new website in your Nginx server. Instructions here a very generic and will need to be adapted for your specific case.

In the following sections, we are showing the conf for a site called mysite.example.org. You need to replace all occurrences of mysite.example.org by the name of the site you want to create.

  1. Create the config file /etc/nginx/sites-available/mysite.example.org
    server {
        include snippets/listen-http.conf;
        server_name mysite.example.org;
    
        access_log /var/log/nginx/mysite.example.org.access.log;
        error_log /var/log/nginx/mysite.example.org.error.log info;
    
        include snippets/acme-challenge.conf;
        include snippets/https-permanent-redirect.conf;
    }
    
    server {
        include snippets/listen-https.conf;
        server_name mysite.example.org;
    
        access_log /var/log/nginx/mysite.example.org.access.log;
        error_log /var/log/nginx/mysite.example.org.error.log info;
    
        include snippets/acme-challenge.conf;
        #include snippets/ssl.conf;
        #ssl_certificate      /etc/letsencrypt/live/mysite.example.org/fullchain.pem;
        #ssl_certificate_key  /etc/letsencrypt/live/mysite.example.org/privkey.pem;
        #include snippets/hsts.conf;
    
        include snippets/security-headers.conf;
        include snippets/x-frame-options-deny.conf;
    
        root /var/www/mysite.example.org;
    }
    
  2. Activate the configuration with
    $ sudo nginx_modsite -e mysite.example.org
    Would you like to reload the Nginx configuration now? (Y/n) Y
    
  3. Edit file /usr/local/etc/certmanage/main.json and add the following to the list
    {
        "domains": ["mysite.example.org"],
        "reload": [["/bin/systemctl", "reload", "nginx.service"]]
    }
    
  4. Get your certificate
    $ sudo /usr/local/sbin/certmanage
    Renewing certificate for mysite.example.org that will expire on 0001-01-01
    
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
    Obtaining a new certificate
    Performing the following challenges:
    http-01 challenge for mysite.example.org
    Using the webroot path /var/www/acme-challenge for all unmatched domains.
    Waiting for verification...
    Cleaning up challenges
    Generating key (2048 bits): /etc/letsencrypt/keys/1764_key-certbot.pem
    Creating CSR: /etc/letsencrypt/csr/1764_csr-certbot.pem
    
    IMPORTANT NOTES:
     - Congratulations! Your certificate and chain have been saved at
       /etc/letsencrypt/live/mysite.example.org/fullchain.pem. Your cert
       will expire on 2019-03-11. To obtain a new or tweaked version of
       this certificate in the future, simply run certbot again. To
       non-interactively renew *all* of your certificates, run "certbot
       renew"
     - If you like Certbot, please consider supporting our work by:
    
       Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
       Donating to EFF:                    https://eff.org/donate-le
    
    Restarting services:
    systemctl reload nginx.service
    
  5. Uncomment the ssl related lines in /etc/nginx/sites-available/mysite.example.org and run
    $ sudo systemctl reload nginx.service
    

Fail2Ban

Webservers are usually a good target for hackers. A lot of them contain outdated, insecure and misconfigured software and if your server run languages like PHP, the attacker would be able to execute pretty much any action once he cracked your server.

Warning: The rules described here protect against generic attacks on your webserver. If you install some specific software that has it's own authentication (owncoud, roundcube...) you need to create rules for it.

nginx-http-auth

First rule is pretty simple simple. It protect against http authentication (the ugly popups asking your password before you enter the site).

Create file /etc/fail2ban/jail.d/nginx-http-auth.conf

[nginx-http-auth]
enabled = true
port    = http,https
logpath = /var/log/nginx/*error.log

nginx-botsearch

This rule match 404 errors when bots try to find unsecure software on your server. While it should generally work fine, you should check ban report to make sure you don't lock out legitimate users.

Create file /etc/fail2ban/jail.d/nginx-botsearch.conf

[nginx-botsearch]
enabled  = true
port     = http,https
logpath  = /var/log/nginx/*error.log
maxretry = 2