How I configure my sites in NGINX

Tuning / Improving your security using external online tools would be another possible headline for this article. There are a few pages I regularly use to optimize my configuration. Here’s a small guide.

NGINX configuration

First of all check the NGINX and OpenSSL version you’re using. We will need that later.

root@web:~# dpkg -l | grep nginx-core
ii  nginx-core  1.18.0-6.1  amd64  nginx web/proxy server (standard version)

root@web:~# dpkg -l | grep openssl
ii  openssl  1.1.1k-1  amd64  Secure Sockets Layer toolkit - cryptographic utility

Then take a look at the NGINX configuration structure. I assume you’re using Debian (I’m not sure, if it is similar in different distributions). The main configuration file is located in /etc/nginx/nginx.conf. Then there is a folder called conf.d whose files are included in nginx.conf. Finally there are sites-enabled and sites-available. In sites-enabled you just create symlinks to files in sites-enabled.

lrwxrwxrwx 1 root root   39 Jul 30 20:51 blog.jeanbruenn.info -> ../sites-available/blog.jeanbruenn.info
lrwxrwxrwx 1 root root   34 Aug 11 02:33 jeanbruenn.info -> ../sites-available/jeanbruenn.info

The remaining folders aren’t relevant for this article. You might want to create a folder called ssl to store ssl certificates and the dhparam file (later) within that folder. It’s not necessary, though.

Configuring SSL

The very first page I check when starting my configuration is the moz://a SSL Configuration Generator. I choose:

  • Software: NGINX
  • Mozilla Configuration: Intermediate
  • Environment:
    • Server Version: 1.18.0
    • OpenSSL Version: 1.1.1k
  • Miscellaneous:
    • HTTP Strict Transport Security
    • OCSP Stapling

10th August, 2021 the generator returned the following configuration:

# generated 2021-08-10, Mozilla Guideline v5.6, nginx 1.18.0, OpenSSL 1.1.1k, intermediate configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&guideline=5.6
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate /path/to/signed_cert_plus_intermediates;
    ssl_certificate_key /path/to/private_key;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
    ssl_dhparam /path/to/dhparam;

    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
    add_header Strict-Transport-Security "max-age=63072000" always;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # verify chain of trust of OCSP response using Root CA and Intermediate certs
    ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;

    # replace with the IP address of your resolver
    resolver 127.0.0.1;
}

I could simply use that snippet as-is and modify my blog.jeanbruenn.info and jeanbruenn.info files in sites-enabled/. However, I want this stuff as default configuration and I want to modify a few things. For that I create /etc/nginx/conf.d/ssl.conf.

/etc/nginx/conf.d/ssl.conf

#
# default ssl configuration. Used the mozilla ssl configurator as template 
# See: https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&guideline=5.6
# 

ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
ssl_session_tickets off;

# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

You may need to remove some ssl_* settings from nginx.conf if there are any else nginx -t moans about duplicate entries.

Diffie Hellman / Forward Secrecy

Then I create the diffie hellman file (instead of downloading one from mozilla) by issuing:

cd /etc/nginx
openssl dhparam -out dhparam.pem 4096
chmod 0600 dhparam.pem

You may prefer to place that file into /etc/nginx/ssl/. I noticed that no curves are used (I read somewhere it’s default value is auto and assumed it would just use it – however, it’s only used if I define ssl_ecdh_curve explicitly). Hence next to ssl_dhparam I also define ssl_ecdh_curve in ssl.conf:

ssl_dhparam /etc/nginx/dhparam.pem;
ssl_ecdh_curve X25519:secp521r1:secp384r1:prime256v1;

Mind that I ordered the curves so strongest goes first. However; using the special value “auto” might be better for you.

Prefer Server Order

I seriously prefer my server ciphers over those the client wants.

ssl_prefer_server_ciphers on;
Resolver

Because I run my own resolver, I do set this accordingly. I often saw guides which show how to use some public DNS here; be aware that this might be used for DNS spoofing. So I strongly suggest you do use your own resolver.

# replace with the IP address of your resolver
resolver 127.0.0.1;
HSTS

HTTP Strict Transport Security has two more possible arguments: includeSubdomains and preload. Preload should be used with care. It will force that every subdomain (if you use includeSubdomains) has to be used with HTTPS unless you opt-out. Please read more about that on hstspreload.org.

However, if I use add_header in my ssl.conf it will also set this header on http. map { } allows us to use this for https only. So

# HSTS (ngx_http_headers_module is required) (63072000 seconds)
map $scheme $hsts_header {
    https   max-age=63072000;
}
add_header Strict-Transport-Security $hsts_header always;

If the STS header field is only emitted by “example.com” but UAs typically bookmark — and links (from anywhere on the web) are typically established to — “www.example.com”, and “example.com” is not contacted directly by all user agents in some non-zero percentage of interactions, then some number of UAs will not note “example.com” as an HSTS Host, and some number of users of “www.example.com” will be unprotected by HSTS Policy.

To address this, HSTS Hosts should be configured such that the STS header field is emitted directly at each HSTS Host domain or subdomain name that constitutes a well-known “entry point” to one’s web application(s), whether or not the includeSubDomains directive is employed.

RFC6797. HTTP Strict Transport Security (HSTS). Considerations for Offering Web Applications at Subdomains of an HSTS Host.

First I thought, that for the parent domain (superdomain) all arguments (includeSubdomains, preload, max-age) should be used while for the subdomain you only use max-age. However, I didn’t find anything in the RFC which states if one should add includeSubdomains and preload to subdomains as well or just to the parent domain.

So you have two options with my configuration snippet above:

(1) You decide that all pages regardless of subdomain or parent domain send the same HSTS header. Then just add includeSubdomains (and if you like preload) to https max-age=63072000;. Separate them using a semicolon and surround everything with quotes like this:

https   "max-age=63072000;includeSubdomains";

(2) Alternatively you may (I’m seriously not sure if this is good or bad, so probably don’t do this) only set max-age like I did above and whenever you have a parent domain, you edit the site configuration and you add:

add_header Strict-Transport-Security "$hsts_header;includeSubdomains" always;

That way you may send includeSubdomains and preload on parent domains, while subdomains will only get the max-age one. I’ll write an update once I know if this should be done or not.

If you have multiple domains / web customers you don’t want to add includeSubdomains and preload globally. Well, you could to force em’ to use SSL 😀

/etc/nginx/conf.d/http-https-redirect.conf

In case the following does not work for you (it does for me) you may replace “default_server” with the FQDN of the site you’re configuring and add the following block to each site you configure. First try this way, though. It will in general redirect all http to https traffic – and nowadays with letsencrypt there really shouldn’t be any non-https websites anyway.

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        return 301 https://$host$request_uri;
    }
}

You may need to remove the default-page from /etc/nginx/sites-enabled for this to work.

/etc/nginx/sites-available/blog.jeanbruenn.info

I just had to add the ssl_trusted_certificate which is – if you use letsencrypt, just the chain.pem:

server {
    server_name blog.jeanbruenn.info;
    [..]
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/blog.jeanbruenn.info/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.jeanbruenn.info/privkey.pem;
    # verify chain of trust of OCSP response using Root CA and Intermediate certs
    ssl_trusted_certificate /etc/letsencrypt/live/blog.jeanbruenn.info/chain.pem;
    [..]
}

HTTP2

Not directly related to SSL, enabling HTTP2 is cool. Just add http2 to your listen-directive (in this case I do this in /etc/nginx/sites-available/blog.jeanbruenn.info)

    listen 443 http2 ssl;

CAA

This is a DNS setting/record. It tells WHO may issue certificates. In my case this is Letsencrypt, so I do add a CAA record to blog.jeanbruenn.info which will reflect this:

root@ns3:~# nsupdate -l
> zone jeanbruenn.info
> update add blog.jeanbruenn.info. 3600 IN CAA 0 issue "letsencrypt.org"
> update add blog.jeanbruenn.info. 3600 IN CAA 0 iodef "mailto:himself@jeanbruenn.info"
> send

Letsencrypt

Did you know, that you could use ECDSA certificates instead of the old boring RSA ones? You may convert your existing certificates by issuing e.g.

certbot renew --key-type ecdsa --cert-name jeanbruenn.info --force-renewal

And while we’re at it.. Did you know, you can select secp384r1 instead of the default 256′ one? That works like this:

certbot renew --key-type ecdsa --elliptic-curve secp384r1 --cert-name jeanbruenn.info --force-renewal

And should work fine with current browsers. A good page to check how your ciphers, key exchange, ecetera is is CryptCheck. So I basically converted my old RSA certificates to new ECDSA ones.

There are ways to use RSA and ECDSA certificates at the same time if compatibility is a must for you. I will maybe cover that in another blog post. You can add several ssl_certificate an ssl_certificate_key arguments to your site configuration.

Verification

The pages I use for verification of these settings are

The former gives you information about possible vulnerabilities and your SSL related settings in general. The latter gives you information about the strength of your SSL settings.

Results

Here are the results. However, please keep in mind: Security is sometimes a compromise between compatibility and safety. The higher your settings, the lesser compatible you are in regard to old clients. It might well be that some old browsers and mobile devices are unable to read my blog. So a company might not want to drive the settings that high, while some people would go even higher than my settings are. A ranking of B or B+ might be still good enough for most people.

On the other hand (just out of curiosity I checked that) my house bank (pretty known in germany) has an A+ rating and supports more devices than my site does.

SSL Labs (Ranking: A+)
https://www.ssllabs.com/ssltest/analyze.html?d=blog.jeanbruenn.info
CryptCheck (Ranking: A+)
https://cryptcheck.fr/https/blog.jeanbruenn.info

Configuring headers

Just by adding a few headers you can tune the security (and privacy) of your website in a simple way. At least if one ignores Content-Security-Policy – probably the best and the worst header at the same time. Ever tried to optimize wordpress for a secure CSP? Anyway.

I create another nginx configuration file which I call headers.conf. I won’t explain the specific headers here, though I added links to good documentation on that stuff. I’d especially recommend the links to Scott’s page, because Scott did an excellent job explaining them IMO.

/etc/nginx/conf.d/headers.conf (security headers)

#
# securityheaders
#

# see: https://scotthelme.co.uk/hardening-your-http-response-headers/#x-frame-options
#      https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
#      https://infosec.mozilla.org/guidelines/web_security#x-frame-options
add_header X-Frame-Options "SAMEORIGIN" always;

# see: https://scotthelme.co.uk/hardening-your-http-response-headers/#x-content-type-options
#      https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
add_header X-Content-Type-Options "nosniff" always;

# see: https://scotthelme.co.uk/a-new-security-header-referrer-policy/
#      https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
add_header Referrer-Policy "no-referrer" always;

# see: https://scotthelme.co.uk/goodbye-feature-policy-and-hello-permissions-policy/
#      https://github.com/w3c/webappsec-permissions-policy/blob/main/permissions-policy-explainer.md
#      https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md
add_header Permissions-Policy "camera=(), microphone=()" always;

# see: https://scotthelme.co.uk/a-new-security-header-feature-policy/
#      https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
# For compatibility reasons, including Feature-Policy (the former header for
# Permission-Policy, as well.
add_header Feature-Policy "microphone 'none'" always;

/etc/nginx/conf.d/headers.conf (mozilla observatory)

#
# mozilla observatory
#

# A setting of 0 disables this, and currently the observatory will reduce
# your points if you disable it. However, read the github issue - you want
# it disabled.
# see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
#      https://github.com/mozilla/http-observatory/issues/432
add_header X-XSS-Protection 0 always;

# see: https://scotthelme.co.uk/content-security-policy-an-introduction/
#      https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
#      https://developers.google.com/web/fundamentals/security/csp
# you REALLY want to check what this is doing BEFORE using it. 2nd link.
# add_header Content-Security-Policy "frame-ancestors 'none'" always;

Verification

My favorite check pages for these are

Both gave me a pretty bad Ranking when I first checked. Securityheaders said D and mozilla observatory said D+ (Score: 40/100). However, that was expected because by default none of those headers are sent.

Results

The above settings result in a Ranking of A+ for security headers and B+ (Score: 80/100) for the mozilla observatory. 100/100 should be reachable with a proper Content-Security-Policy (which is not done within a few minutes – that needs to be done per-site). Similarly one might implement Subresource Integrity which is also not done within a few minutes and needs to be done per-site.

securityheaders.com
https://securityheaders.com/?q=blog.jeanbruenn.info&hide=on&followRedirects=on
Mozilla Observatory
https://observatory.mozilla.org/analyze/blog.jeanbruenn.info

There is more…

Pagespeed Insights

Probably nothing new for you but Google PageSpeed Insights is one of those useful tools. When I first used it after the above optimizations it gave me a score of 87. One reason for that score was, that I did not use compression for my text-based files.

I added another configuration file called compression.conf in /etc/nginx/conf.d and added:

gzip on;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1400;
gzip_comp_level 3;

The min length of 1400 is because Lighthouse will not flag it anyway if it’s smaller than 1.4 KB and I also believe we shouldn’t waste resources for compression if resources are that small. A Compression level of 3 should be fine. gzip_proxied is set, so that proxies will get compressed files, as well.

You might have to remove default configuration (gzip related) from nginx.conf or you get duplicate configuration errors

Pagespeed result?

https://developers.google.com/speed/pagespeed/insights/?hl=en&url=https%3A%2F%2Fblog.jeanbruenn.info%2F

Webbkoll

I stumbled upon this page a few days ago and I like it. It does not give you a score like the above pages did. There wasn’t much I could take to optimize my webserver configuration any further (just like the above tools it moans about the missing / incomplete CSP). My blog is very simple and I disabled a lot of stuff due to privacy reasons anyway.

However, this page informed me about the X-XXS-Protection; while the recommendation in the past was to set it to 1; mode=block nowadays the recommendation is to disable it completely (0). Yes disabling it causes some of the above checks to show a worse ranking (because those test tools weren’t updated to reflect this change) but you’re doing this to increase the security – so what?

Anyway. I really like the GDPR (if you choose german language you get the german DSGVO stuff) information that page displays.

https://webbkoll.dataskydd.net/en/results?url=http%3A%2F%2Fblog.jeanbruenn.info

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.