Tweaks for NGINX web server

Tutorials Apr 24, 2020

Since I'm using NGINX as a web server for Ghost, the information below is adapted to its config. But it is easily modifiable to whatever web server you are using, these are the tweaks I did to mine.

Security Headers

Adding headers to increase the security of your site, protecting it from cross-site scripting attacks (XSS) should be on top of your things to add to NGINX configuration, for your peace of mind.

I used this site to check how well I did implement the headers. At the beginning it was all red. Not that I expected it to be all fine from the start. After the test, you will have a table with the missing headers and their explanation. Take the time to read what each header does.

Initial check

So I proceeded with adding the missing headers in the configuration file related to my site.

Open the configuration file for your domain and in it go to the server - location. For myself, I had to add the following missing headers:

add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
add_header Content-Security-Policy "child-src 'self'";
add_header Referrer-Policy same-origin;
add_header Feature-Policy "vibrate 'self'; sync-xhr 'self' https://boratory.net";
Missing headers in nginx configuration

Now retest. This time you'll like the results.

After headers in place

Enable HTTP/2

Many people think of HTTP/2 as a shiny, superior successor to HTTP/1. I do not share this opinion, and here’s why. HTTP/2 is actually just another transport layer for HTTP/1, which isn’t bad because as a result, you can use HTTP/2 without having to change your application – it works with the same headers. You can just switch on HTTP/2 in NGINX and NGINX will gently handle all the protocol stuff for you. - Valentin Bartenev of NGINX

With this in mind, locate the listen variables associated with port 443 in your config file. The first one is for IPv6 connections and the second one is for all IPv4 connections. So why not enable HTTP/2 for both since you're here.

listen [::]:443 ssl http2 ipv6only=on;
listen 443 http2 ssl;
Tell Nginx to use HTTP/2 with supported browsers

Now that you edited your config, it's a good time to check it before restarting nginx. For that, you have to run

sudo nginx -t
Check yourself before you break yourself

If everything's ok, reload NGINX web server with the following command:

sudo /etc/init.d/nginx reload
Restart nginx, you're done

To test your work, use the https://http2.pro/ web site. You should see this result:

Yay! Job well done!

Enable NGINX caching

Yesterday evening, in my quest to push the Futro S900 thin client to its limits, I enabled caching with NGINX. In the last flood test that I run, I saw that the requests were hitting hard Node.js and consumed a lot of CPU power to serve the website for a load of a hundred users. It just choked after 32 simultaneous connections. Enabling the NGINX cache means that the requests are now served by NGINX, and the Node.js server will be left out of the game.

Flood test results - from 30s / request down to 56 ms response time

As you can see, the impact of caching is huge! I will run some additional tests, but as the things are looking now, I'm assuming that only the network bandwidth can limit the website performance. Enough with the talking, here's my complete configuration file for boratory.net:

server {
  listen 81;
  server_name localhost;

  access_log off;
  allow 127.0.0.1;
  deny all;

  location /nginx_status {
    # Choose your status module

    # freely available with open source NGINX
    stub_status;

    # for open source NGINX < version 1.7.5
    # stub_status on;

    # available only with NGINX Plus
    # status;
  }
}

server {
    listen 80;
    server_name boratory.net www.boratory.net;
    return 301 https://boratory.net$request_uri;
    server_tokens off;
}

proxy_cache_path /var/cache/nginx/ levels=1:2 keys_zone=boratory_cache:75m max_size=512m inactive=60m;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_methods GET HEAD;

server {

    server_name boratory.net;
    server_tokens off;
    root /var/www/boratory/system/nginx-root;

    # disable any limits to avoid HTTP 413 for large image uploads
    client_max_body_size 0;
    # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
    chunked_transfer_encoding on;
    
	# gzip every proxied responses
	gzip_proxied any;

	# gzip only if user asks it
	gzip_vary on;

    # gzip only theses mime types
    gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json application/javascript;

    access_log /var/log/nginx/boratory-access.log;
    error_log  /var/log/nginx/boratory-error.log;

    location / {
        proxy_cache boratory_cache; 
        proxy_cache_valid any 10m;
        add_header X-Proxy-Cache    $upstream_cache_status;
        proxy_cache_use_stale       error timeout http_500 http_502 http_503 http_504;
        proxy_set_header X-NginX-Proxy true;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 900;
        proxy_redirect off;

        # Remove cookies which are useless for anonymous visitor and prevent caching
        proxy_ignore_headers Set-Cookie Cache-Control;
        proxy_hide_header Set-Cookie;
        # use conditional GET requests to refresh the content from origin servers
        proxy_cache_revalidate on;
        proxy_buffering on;
        # Allows starting a background sub-request to update an expired cache item,
        # while a stale cached response is returned to the client.
        #proxy_cache_background_update on;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:2368;

        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
        # add the domains that embed content in ghost posts
        add_header Content-Security-Policy "child-src 'self' disqus.com *.youtube.com *.datadoghq.com";
        add_header Referrer-Policy same-origin;
        add_header Feature-Policy "vibrate 'self'; sync-xhr 'self' https://boratory.net";

    }

    # Bypass ghost for static assets
    location ^~ /assets/ {
        root /var/www/boratory/content/themes/liebling-2;
    }

    # Bypass ghost for original images but not resized ones
    location ^~ /content/images/(!size) {
        root /var/www/boratory;
    }

    # Don't try to cache the admin part
    location ^~ /ghost/ {
        proxy_set_header Host $http_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_pass http://localhost:2368;
    }

    location ~ /.well-known {
        allow all;
    }

    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 http2 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/boratory.net/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/boratory.net/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
    if ($host = boratory.net) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name boratory.net;
    return 404; # managed by Certbot

}
NGINX complete configuration file

Drop a comment if something looks odd to you. I'm always fiddling with the configuration, but for the moment I'm pleased with the results.

Radu

Since there's no place like 127.0.0.1, I try my best to keep it up to date and add network services using various (mostly old and cheap) network devices.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.