100% ssllabs score with nginx

In this article I want to describe how to harden the nginx SSL security and what it needs to get a 100% score atssllabs. The scoring criteria are outlined by ssllabs officialdocumentation.There exists also an excellent SSLguidefrom ssllabs. Here we focus more on hands-on examples using nginx.

Using an up-to-date version of nginx is expected. Variants for configuring nginx directly or using thenginx-inc Kubernetes Ingress controller are provided. For theIngress controller we useHelm for the installationand configuration.

Protocol Support

This is easy, only support TLS 1.2 and TLS 1.3, so we add to the HTTP section:

ssl_protocols TLSv1.2 TLSv1.3;

If we use the nginx-inc Kubernetes ingress controller, we canset the configmapresource:

controller.config.entries.ssl-protocols: "TLSv1.2 TLSv1.3"

Key Exchange

Key

Elliptic curves

With elliptic curves it is rather easy to get full score here.For the key you have to get in contact with the certificate authority that issues your key. I personally use Let'sEncrypt.Certbot offers a command line option

--key-type ecdsa --elliptic-curve secp384r1

For the Kubernetes Cert-Manager you have to explicitly specify your certificaterequest as a Kubernetes resources to be able to configure therequested key size:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata: [...]
spec:
  [...]
  privateKey:
    algorithm: ECDSA
    size: 384

RSA

Here we have to make sure that our key as well as our DH parameter strength is equal to or larger than 4096 bits.For the key you have to get in contact with the certificate authority that issues your key. I personally use Let'sEncrypt.Certbot offers a command line option

--rsa-key-size 4096

For the Kubernetes Cert-Manager you have to explicitly specify your certificaterequest as a Kubernetes resources to be able to configure therequested key size:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata: [...]
spec:
  [...]
  privateKey:
    algorithm: RSA
    size: 4096

Diffie-Hellman parameter

The default DH parameter is from OpenSSLs default and is just 1024 bits long. We can use OpenSSL to generate acustom DH parameter. Also, the DH parameter length has to be greater or equal than 4096 bits for the 100% score.

openssl dhparam -out ssl-dhparams.pem 4096

The nginx configuration variants expects that the resulting ssl-dhparam.pem file is stored in the referencedlocation and goes into the http block:

ssl_dhparam /etc/ssl/ssl-dhparams.pem;

For the nginx-inc Kubernetes controller we can just provide the content of the DH param file in the values for theHelm chart:

controller.config.entries.ssl-dhparam-file: |
  # content of ssl-dhparams.pem goes here

Cipher strength

Here we have to make sure that we use at least 256 bits for the Cipher strength.

TLSv1.2

This part is surprisingly the more easy one. Mozilla provides a very useful sitethat generates secure cipher setups. For our purpose the approach that worked for me the best was to use theintermediate profile and remove all Ciphers that use less than 256 bits.For the nginx_config this yields at the time of writing for the http block:

ssl_prefer_server_ciphers off; ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384;

And for the values of the nginx-inc Kubernetes Ingress Helm chart:

controller.config.entries:
  ssl-prefer-server-ciphers: "False"
  ssl-ciphers: ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384

TLSv1.3

Technical part

This part is not technically harder, the problematic part starts when we read the RFC for TLSv1.3. So let's startwith the hands on part for nginx before coming back to the RFC concerns.For both config variants discussed here removing 128 bits ciphers boils down to two lines in the http section. For thenginx-inc Ingress we can set this via the controller.config.entries.http-snippets value.

# secp521r1 not supported by chrome anymore ssl_ecdh_curve secp384r1; # TLSv1.3, more secure, but not RFC 8446 compliant to exclude TLS_AES_128_GCM_SHA256 ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;

Specification concerns

Let's quote the relevant RFC8446, section 9.1:

A TLS-compliant application MUST implement the TLS_AES_128_GCM_SHA256[GCM] cipher suite and SHOULD implement the TLS_AES_256_GCM_SHA384[GCM] and TLS_CHACHA20_POLY1305_SHA256 [RFC8439] cipher suites (seeAppendix B.4).

Hence, if we want to be RFC compliant we have to implement the 128 bits Cipher.

Bonus

These points are nice to have, but do not influence the score at ssllabs.

DNS CAA entry

A simple DNS entry that limits which certificate authority canissue certificates for your domain. Since2017 it is mandatory for certificateauthorities to honor this DNS entries.

@ 10800 IN CAA 128 issue "letsencrypt.org"

The 128 is a flag that marks the entry as critical. If a certificate authority cannot process this entry, they arenot allowed to issue certificates for this domain. The other value allowed byRFC6844 is 0 which means that a certificate authority might ignorethe CAA DNS entry if they cannot process it.

SSL sessions and OCSP stapling

Both reduce the overhead the SSL causes and can be set in the http section analogous to the TLSv1.3 setting:

ssl_session_timeout 1d; ssl_session_cache shared:SSL:100m; # about 400000 sessions ssl_session_tickets off; # google DNS for OCSP stapling resolver 8.8.8.8 8.8.4.4; # OCSP stapling ssl_stapling on; ssl_stapling_verify on;

HSTS

HTTP Strict Transport Security is a special header that tells thebrowser to only communicate with this domain via HTTPS in the future. This thereby avoids man-in-the-middle attacksthat can occur when the user (accidentally) starts the communication via HTTP.Due to the long-lasting nature of the headers effect it should be only deployed with care. Going back to HTTPwithout TLS becomes very hard once a relevant part of the user base has received the HSTS header. One should alsoconsider if the includeSubDomains setting is desired.

Google maintains a preload list which also used by the other browser maintainers. Anentry in this list means that the browser will always communicate with HTTPS to the given domain. Avoiding alsoman-in-the-middle attacks for the very first request of the given client.Entry in the Google preload list takes some weeks to process and propagate and has the prerequisite that the givendomain serves the HSTS header with the preload directive.

For the nginx config we can just add to the relevant server part:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

For the nginx-inc Kubernetes Ingress we can use for the Helm values:

controller.config.entries:
  # hsts implies preloading here
  hsts: "True"
  hsts-max-age: "63072000"
  hsts-include-subdomains: "True"