Hardening nginx

General information #

  • After you change the nginx configuration always test its validity by executing
$ nginx -t
  • Limit everything to what is needed for proper function. Do not use nice-to-have modules/configs that do not bear any usage for the user.

Have logging and monitor it #

Logs help you identify and analyse attacks. So use them. But collecting only does not help, you must regularly analyse them.

server {
    # ...
    error_log .../error.log warn;
    access_log .../access.log combined;
    # ...
}

If collecting logs, keep GDPR in mind and only keep them as long as it is really required for securing the server.

I’m still looking-out for a good log management tool that helps to identify threats.

Don’t expose server version #

Exposed server versions make it easier for attackers to find weaknesses of your system, so give them as few information about your system as possible.

  • Set server_tokens off in the http section of your nginx.conf, e.g.:
http {
    # ...
    server_tokens off;
    # ...
}
$ systemctl reload nginx

Limit HTTP methods #

Limit to what you need, e.g. GET only is enough for that simple blog:

location / {
    limit_except GET { deny all; }
}

Use secure TLS/SSL configuration #

As of January 2022, the following configuration is considered as safe:

server {
    # ...
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_dhparam /etc/nginx/dhparam.pem;
    ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM';
    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_session_timeout 10m;
    ssl_session_cache off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off; 
    # ...
}

Create Diffie-Hellman parameters file #

Create your own set of Diffie-Hellman (DH) parameters for the key exchange and don’t use the parameters commonly distributed with your Linux distribution. Weak DH parameters were a reason for past issues, cf. Logjam Attack.

You can create your own 4096 bit DH parameter set like this

$ openssl dhparam -out /etc/nginx/dhparam.pem 4096

SSL Protocols #

  • Protocols older than v1.2 are deprecated by IETF
  • Good write up why not to use older TLS versions can be found here.

Let the server decide on the cipher suite #

As I harden the server and am possibly more upto date than some browsers out in the wild, my server knows best which ciphers to use. Therefore, use the following option

ssl_prefer_server_ciphers on;

Verification #

You can use nmap with the ssl-enum-ciphers script. At least for Ubuntu it is already itegrated into the main packages:

$ nmap -6 --script ssl-enum-ciphers -p 443 blog.straubs.eu
Starting Nmap 7.80 ( https://nmap.org ) at 2022-01-16 21:42 CET
Nmap scan report for blog.straubs.eu (2a01:4f8:c2c:b754::1)
Host is up (0.036s latency).
Other addresses for blog.straubs.eu (not scanned): 116.203.209.149
rDNS record for 2a01:4f8:c2c:b754::1: vpn.straubs.eu

PORT    STATE SERVICE
443/tcp open  https
| ssl-enum-ciphers: 
|   TLSv1.2: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (ecdh_x25519) - A
|     compressors: 
|       NULL
|     cipher preference: server
|_  least strength: A

Nmap done: 1 IP address (1 host up) scanned in 1.30 seconds

SSL Labs is also a great utility to check the overall security hardening on a protocol and header level.

Ciphers #

An uptodate secure cipher list can be found add syslink.pl.

OSCP stabling #

OSCP stapling is well explained in this Mozilla tech-blog article. Roughly spoken, the server you are connecting to also includes current evidence that its certificate is not revoked by the signing CA. Thus it reduces the round-trip time for the client as it would be the clients job to request this information at the CA.

Verification #

You can verify the proper functioning of your setup with openssl:

$ openssl s_client -connect blog.straubs.eu:443 -servername blog.straubs.eu -status 2>&1 | grep -i ocsp

Mind that you need to specify the servername option as well as the connection option if you use server name indication (SNI), e.g. if you host multiple domains on your server. In case of success the output should look like this

OCSP response: 
OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response

SSL Session Cache #

As NGINX does not provide any means of regularly cleaning up the session and ticket cache, it is best to turn it off

ssl_session_cache off;
ssl_session_tickets off; 

That’s important because to correctly implement forward secrecy (PFS) the short-term secret, i.e. unique session keys, must not fall in the hands of an attacker. Therefore, it must be deleted/swapped regularly otherwise PFS is undermined. Besides disabling the caches at all, one could also regularly recreate the session ticket key (see Stephan Herber - Sichere TLS Konfiguration mit NGINX).

Add security headers #

My current defaults:

server {
    # ...
    add_header Strict-Transport-Security "max-age=63072000; preload;";
    add_header X-Frame-Options "SAMEORIGIN";
    add_header Content-Security-Policy "default-src 'self'" always;
    # ...
}

HSTS #

  • Mozialla developer information
  • Tells browser always to use secure, i.e. https, transport
  • On the page for domain.tld you can also set includeSubDomains options. This enforces for all subdomains of your domain secure transport.
  • If you also want to be preloaded as HTTPS only, you can submit your own domain to the Google operated service hstspreload.org.

X-Frame-Options #

Content Security Policy (CSP) #

add_header Content-Security-Policy "default-src 'self'; report-uri https://collector.straubs.eu/collector" always;