If your leave any of your pages insecure, all of your pages are insecure, because anyone in-between your users and you (like other people in a coffee shop) can easily intercept all of the traffic and replace https:// links with http:// links. They can then do whatever they want with any page on your site. If your site uses authentication then it must also use HTTPS for the life of the session. To secure our website we will force usage of HTTPS on every page in our nginx configuration file.

server {
    listen      80;
    listen [::]:80 ipv6only=on;

    server_name example.com;

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

Secure and not secure connection in browser preview

An asymmetric system, as SSL (Secure Sockets Layer) or TLS (Transport Layer Security), uses a public key and a private key to encrypt communications. Anything encrypted with the public key can only be decrypted by the private key and vice-versa.

SSL certificates

When you request a HTTPS connection to a webpage, it will initially send its SSL certificate to your web browser. This certificate contains the public key needed to begin the secure session. Based on this initial exchange, your browser and the website then initiate the SSL handshake. It involves the generation of shared secrets to establish a uniquely secure connection between yourself and the website.

HTTPS session handshake

There are many companies that issue certificates for your company and are tied to a specific domain. If you want to use free services to get SSL certificate, only domain-validated certificates are being issued, since they can be fully automated.

Configure nginx

Fisrt, we need to define basic configuration to listen HTTPS port and turn on HTTP2:

server {
    server_tokens off;

    server_name example.com;
    root        /path/to/project;
    index       index.php index.html index.htm;

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

    ssl on;

    # rest config
}

Directive listen 443 ssl http2; enables HTTP2 for us. The most crucial feature is complete multiplexing. It follows that multiple requests may occur in precisely the exact same time over a link that remains open for the length of the transport procedure.

HTTP/1.1 vs HTTP/2

Indicate certificates

Further, specify paths to the certificates into directives ssl_certificate and ssl_certificate_key.

server {
    # ...

    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    # rest config
}

Directive ssl_certificate refers to certificate path given for your host. Intermediate certificates have to be specified in addition to a primary certificate in the following order: the primary certificate comes first, then the intermediate certificates.

If you do not have a full chain certificate, you must merge the certificate issued to your host with a CA certificate:

sudo cat host.crt server.ca.pem > /etc/nginx/ssl/fullchain.pem

OCSP stapling

TLS Certificate Status Request extension presents a time-stamped OCSP response signed by the CA to the initial TLS handshake, thereby clients don't need to contact the CA.

server {
    # ...

    ssl_stapling on;
    ssl_stapling_verify on;
    # resolver 127.0.0.1 [::1];
    resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s;
    resolver_timeout 10;

    # rest config
}

OCSP Stapling is controlled by the ssl_stapling directive and can be enabled independently of OCSP Stapling Verification. If verification is disabled, the server simply forwards to the client the OCSP response it received from the CA, without performing any validation. Directive ssl_stapling_verify enables verification of OCSP responses by the server.

SSL Stapling allows the server to attach OCSP responses, thereby reducing the time for clients to load pages. The certificate chain (domain - intermediate authorization center - root authorization center) can contain 3-4 levels. And for each level, the browser must establish a connection and receive a certificate. You can send all the certificates (including the intermediate one, so that the certificate chain is guaranteed to fit into one package transfer) at once, then the browser will check the whole chain locally, and request only the root certificate (which in most cases is already on the client).

To make it works, it is necessary to describe the resolver directive. If you have your own DNS server raised, you can set the value to 127.0.0.1 [::1]. If not, you can specify Google DNS servers 8.8.8.8 8.8.4.4, or any others. But keep in mind, using Google DNS servers for OCSP stapling, it opens the door to DNS cache poisoning of the OCSP stapling mechanism by a third-party. Data centers are easy enough to identify by IP address range. So all Google has to do is verify that someone is requesting known OCSP servers, verify that the server supports OCSP stapling, and they can easily engage in an OCSP stapling MITM attack. Running BIND locally is easy to install and set up on most distributions, eg. for Debian-based you need to install it using sudo apt-get install bind9.

Key Exchange

Directive ssl_dhparam is necessary to earn Forward Secrecy. That means that if a third party recognizes a session key, it can only access data protected by that key.

server {
    # ...

    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    # rest config
}

To maintain perfect forward secrecy (PFS), the key used to encrypt the transmitted data should not be used to obtain any additional keys. You can generate this key locally using the following command:

sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096

Chiphers

server {
    # ...

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    ssl_prefer_server_ciphers on;

    # rest config
}

Using ssl_protocols we are defining, that we want to use only TLS protocols instead of SSL. This is reasonably secure. However, disabling TLSv1.0 and TLSv1.1 gives us better score by SSL Labs checking. In ssl_ciphers directive we are disabling everything from SSL and defining ciphers we want to use. Directive ssl_prefer_server_ciphers force nginx to strictly follow only this ciphers.

Cache session

Maintaining SSL Sessions is definitely a good thing for everyone if you expect the user to be on your website for more than a single page view. Directive ssl_session_cache sets types and sizes of caches that store session parameters. In example below we will set 20 MB, this should be enough; and ssl_session_timeout directive sets a time during which a client may reuse the session parameters from cache.

server {
    # ...

    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 60m;

    # rest config
}

Strict Transport Security

An attacker can hijack the redirect with a tool like SSLStrip, and if the user is posting sensitive information then it will be leaked. You should enable HSTS (HTTP Strict Transport Security) to enforce HTTPS. This makes it so once a user's browser has been to your website, it will refuse to connect to the insecure version for some period of time. This isn't as nice as it could be, but it's much better than nothing.

server {
    # ...

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

    # rest config
}

Strict-Transport-Security response header tells browsers how long they need to remember the security requirements for our domain. In this case - 1 year, including subdomains. preload is not part of the HSTS specification and should not be treated as official, but nevertheless, browsers will never connect to your domain using an insecure connection.

Be careful with includeSubDomains flag! Sometimes certificates are issued for a limited number of subdomains, so only those have trusted certificates will work. Or you can avoid includeSubDomains flag to be included in the header.

Content Security Policy (CSP)

server {
    # ...

    add_header Content-Security-Policy-Report-Only "default-src https:; script-src https: 'unsafe-eval' 'unsafe-inline'; style-src https: 'unsafe-inline'; img-src https: data:; font-src https: data:; report-uri /csp-report";

    # rest config
}

Content Security Policy is the standard that defines the HTTP headers Content-Security-Policy and Content-Security-Policy-Report-Only, which inform the browser of the whitelist of hosts from which it can download various resources.

Putting it all together

Eventually we get the following configuration file:

server {
    server_tokens off;

    server_name example.com;
    root        /path/to/project;
    index       index.php index.html index.htm;

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

    ssl on;

    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s;
    resolver_timeout 10;

    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    ssl_prefer_server_ciphers on;

    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 60m;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header Content-Security-Policy-Report-Only "default-src https:; script-src https: 'unsafe-eval' 'unsafe-inline'; style-src https: 'unsafe-inline'; img-src https: data:; font-src https: data:; report-uri /csp-report";

    # rest config
}

As a result, we will get A+ score from SSL Labs:

SSL Labs score A+