Nginx & Letsencrypt: HTTPS for all

Why HTTPS?

Securing a web server using HTTPS not only gives you encryption but also guarantees that the request reaches the destination without being compromised.

The request crosses over a lot of network paths to reach the destination, and if not secured (using HTTP), its content can be changed or it can be redirected to another server without knowing it.

With a well configured HTTPS server, using a public Web-Of-Trust, you have this assurance.

This post will briefly cover how Letsencrypt works, and how to configure Nginx to handle the ACME challenge protocol. Also, we will see how to tell Nginx to redirect all HTTP traffic to HTTPS, and will use safe SSL/TLS Ciphers parameters.

Letsencrypt overview

For about one year now (mid 2015) Letsencrypt has been able to generate HTTPS certificates for free. We will not dig into the details of HTTPS certificate validation here, you can find plenty of information about that on the Internet. Basically, your web browser includes some well known Certificate Authorities (CA) public keys that are trusted by default. Then if a certificate encountered on a web site is signed with one of these authorities, your web browser will considered the connection as secure.

Letsencrypt has cross-signed their certificate with IdenTrust. IdenTrust is a certificate authority that is known by major browsers. So if you encountered a Letsencrypt certificate, it will be trusted without any security alert by your web browser.

The following diagram gives you an overview of the actual Letsencrypt Certificate Chain-Of-Trust:

[object Object]
isrg_keys.png, Oct 2016

ACME protocol challenge

Letsencrypt certificate generation and signature is made really easy thanks to their ACME (Stands for : Automatic Certificate Management Environment) protocol challenge.

Everything described below aims at giving an overview of the ACME protocol challenge. But you do not have to do this step manually (and in fact, it can be pretty hard to do it), everything is automated in the Letsencrypt tool (called certbot).

Register to the ACME server

The first time you want to deal with Letsencrypt certificate generation, you generate locally an asymmetric key pair and contact information. Contact information is then signed by this key pair.

Letsencrypt ACME server will get your public key and your contact information to register an account on the server. The server will verify that you hold the private key. All messages sent to the ACME server will be signed using your private key, and the ACME server will authenticate these messages with your public key.

Certificate Generation

When you ask for a certificate generation for a domain name, the ACME protocol challenge checks that you own the domain name (DNS). An HTTP request is made on this domain, and verify on a pre-defined URL request on your web server, that you own the private key associated with your account.

Once the validation is done, the client generates a certificate and a Certificate Signing Request (CSR) which is signed using your private key. Then the signature and CSR are sent to the ACME server.

ACME server checks the signature and if it matches and if the server agrees to issue the certificate, signs your certificate and sends back to you the signature.

You can find more information about the ACME protocol challenge here.

Install Letsencrypt on Debian Jessie (8)

The easiest Letsencrypt client (that implements the ACME protocol challenge) to use is called « certbot ». It is not available directly in Debian Jessie (8) but a back-ported version is available.

Check in your APT sources.list that you have enabled the jessie-backports repository:

deb http://httpredir.debian.org/debian jessie-backports main

Do not worry, packages from jessie-backports are configured with a version (~) that does not take precedence over Stable Debian packages. So if you do not force your package management utility to use Jessie-backports, it will keep using those coming from stable.

To install certbot run:

# apt update
# apt install -t jessie-backports certbot

Certbot is written in Python, and it depends on a lot of Python packages.

A crontab is also set-up by the Debian package to automate the certificate renewal. You can see it in:

/etc/cron.d/certbot

Using certbot with Nginx

If you followed the previous steps, you understand that, in order to do the ACME challenge, certbot needs to :

  • Be able to communicate with the ACME server on port 80
  • Access a directory of your Nginx server to expose some signing materials that are checked by the ACME server.

Certbot offer several methods to accomplish that:

  1. Apache2
  2. Standalone
  3. Webroot

Apache2 method can be ignored, we are targeting Nginx.

Standalone method can also be ignored, since we already have an Nginx web server on port 80 (HTTP). We can use it, but manually, because Nginx server must be stopped before using certbot.

Here we use the webroot method.

Configure Nginx to accept the ACME challenge and redirect all HTTP traffic to HTTPS

We create a default virtualhost for all HTTP requests on port 80. If a request is for the ACME server, we serve a path to handle the challenge. If not, we redirect the request to the same address but using a secured HTTPS connection.

The default path to be configured by Nginx to handle the challenge, is « /var/www/letsencrypt/ » (but you are free to choose another folder)

Configure the "default" virtualhost by editing the file « /etc/nginx/sites-available/default »:

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

        include snippets/letsencrypt-acme-challenge.conf;

        location / {
          return 301 https://$host$request_uri;
        }
        access_log /var/log/nginx/letsencrypt_acme-access.log;
(...)
}

The magic line that enables redirection for all HTTP requests to HTTPS is the following:

return 301 https://$host$request_uri;

301 is an HTTP return code to tell the client (i.e. your web browser) that the address has "Moved Permanently" to another location. Here, the same location but with "https://" instead of "http://".

Now, create the file « /etc/nginx/snippets/letsencrypt-acme-challenge.conf » with this content:

location ^~ /.well-known/acme-challenge/ {
    # Set correct content type.
    default_type "text/plain";
    root         /var/www/letsencrypt;
}
# Hide /acme-challenge subdirectory and return 404 on all requests.
location = /.well-known/acme-challenge/ {
    return 404;
}

Certbot client configuration

We need to configure the certbot client to let it know the Nginx Letsencrypt root path and make it use the webroot method by default.

Create a file in « /etc/letsencrypt/cli.ini », and enter this:

# Uncomment and update to register with the specified e-mail address
email = <Your Email to register to the ACME server>

authenticator = webroot
webroot-path = /var/www/letsencrypt/

Bonus: 4096 Key size

Also, you can upgrade the default certificate key size. Letsencrypt certbot client generates by default a 2048 bits key. To generate a more secure certificate, for example 4096 bits long, add this to the file « /etc/letsencrypt/cli.ini »:

# Use a 4096 bit RSA key instead of 2048
rsa-key-size = 4096

Running Certbot

We must reload Nginx to be able to serve the virtualhost for ACME challenge protocol.

# systemctl reload nginx

We can now run certbot client in order to ask Letsencrypt for a certificate signature.

To sign the domain example.com, and the sub-domain sub.example.com, we must check that the DNS for example.com and sub.example.com point to the server that runs certbot client. ACME server will try to access both domain using HTTP request, and will check for particular files in the webroot to authenticate you as a legitimate ACME user and as the owner of example.com AND sub.example.com.

If you have checked theses prerequisites, you can run certbot:

# certbot certonly -d example.com -d sub.example.com

You should see this message:

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/<Your.domain.tld>/fullchain.pem. Your cert will
   expire on YYYY-MM-DD. To obtain a new or tweaked version of this
   certificate in the future, simply run certbot again. To
   non-interactively renew *all* of your certificates, run "certbot
   renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

Hardening Nginx for SSL

I am not a security wizard, and even if I have worked on security software (mainly for Set-Top-Box system) you should always stay up-to-date on security-related news. Computer security is constantly evolving.

The following Nginx security parameters come from this website [raymii.org].

Create a file « /etc/nginx/snippets/ssl_letsencrypt.conf ». You will include it in all your virtualhosts.

Letsencrypt Certificate, Key and Chain

You need to give Nginx the private key and Letsencrypt key chain with:

ssl_certificate /etc/letsencrypt/live/<Your.domain.tld>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<Your.domain.tld>/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/<Your.domain.tld>/chain.pem;

Diffie-Hellman parameters

First if you have not already done that, generate strong enough Diffie-Hellman parameters for Nginx:

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

This process takes about 30 minutes on a good server, with enough entropy on the system.

Pass the generated Diffie-Hellman parameters:

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

Use Secure Ciphers

We will use only Secure ciphers, that will works on major web browser. Also, we force the server to choose the cipher.

ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;

HTTP Strict Transport Security

We are enabling HSTS, which stands for HTTP Strict Transport Security, and enable it for all sub-domains. By doing this, the client's web browser knows that all the domain AND sub domains (option : includeSubDomains) must be visited using HTTPS. This will be valid for 63072000 seconds (750 days)

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

HTTP Public Key Pinning

The Web-Of-Trust mechanism for SSL/TLS can be a good thing. But in fact, you trust Letsencrypt because your browser is already bundled with the trust of Letsencrypt CA. Consider that Letsencrypt Certification Authority get compromised, and an attacker can issue SSL/TLS certificates for your site. He can forge the request (Using man in the middle) and made think to your browser that the HTTPS connection is legitimate... (Just to say that a lot of Company proxies does that, in order to be able to watch the HTTPS traffic from their employees.)

HPKP, stands for HTTP Public Key Pinning, allows you to protect yourself by providing a white list of Public key that the browser should trust. HPKP will say for a period of time, that it should only accept a set of certificates.

But HPKP can be problematic, especially with Letsencrypt CA. You have to choose to enable it or not ! Letsencrypt says that they don't support this mechanism officially. Please read this.

Personally, for the moment, I have chosen not to enable HPKP with Letsencrypt and the Nginx configuration provided here will not enable it. But if you want to, read this excellent post from Scott Helme.

Online Certificate Status Protocol Stapling

OCSP (Stands for "Online Certificate Status Protocol") is a protocol to check whether an SSL Certificate has been revoked. It will basically make a request to an OCSP responder, a server configured by the Certification Authority (CA), that will respond about the revocation of the certificate.

OCSP stapling allows the web server to query the OCSP responder directly and will be presented in the SSL/TLS handshake via the Certificate Status Request extension response. The client web browser checks that it have the same response from the web server and the OCSP responder.

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

To check OCSP stapling, you can run openssl to make a TLS request to your server with this command :

$ openssl s_client -connect <You_domain>:443 -tls1 -tlsextdebug -status |grep -i ocsp

You should see something like :

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

Full snippet file

Below the full snippet file, remember that I choose to save it in « /etc/nginx/snippets/ssl_letsencrypt.conf »:

# SSH Certificate & Chain
ssl_certificate /etc/letsencrypt/live/<Your.domain.tld>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<Your.domain.tld>/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/<Your.domain.tld>/chain.pem;

# DH Parameters
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

# Safe SSL Ciphers
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;

# HSTS Header
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

You should now include this file in your virtualhost defined to serve HTTPS on port 443.

Taking virtualhost example.com in "/etc/nginx/site-available/example.com" :

server {
        listen 443 ssl;
        listen [::]:443 ssl;
        include snippets/ssl_letsencrypt.conf;
        server_name example.com;
        (...)

Certificate Renewal

Letsencrypt certificate are only valid for 3 months. But the certificate renewal is automated thanks to Certbot client and the crontab installed by the Debian package.

When one of your certificate will be near from expiration, Certbot will detect it and renew your certificate.

Be aware that Nginx server needs to be reloaded to take the new certificate. Maybe you will have to add a line in the crontab to reload Nginx with :

# systemctl reload nginx

Conclusion

You have generated a valid HTTPS certificate using Letsencrypt and configured Nginx to use this certificate with some common HTTPS security features enabled. But do not think you are always safe with that configuration. Security is a moving domain, and I advise you to read often common security news and leak.

Also we are only covering SSL Hardening here, and not all the HTTP headers hardening. To avoid confusing, I will talk about Nginx Headers hardening in another post.

You can check the security grade of your web server, using some tools on the Internet, like this one.

To conclude, Letsencrypt team have done a very good work and that lead to a revolution for a more secured Internet. At this time of writing, and since the launch of Letsencrypt to the public in Q4 2015, the HTTPS growth rate have quadrupled ! So please consider donate to them.

Nowadays, everyone should use an HTTPS connection...

Source Links

Add a comment

Comments can be formatted using a simple wiki syntax.

Add ping

Trackback URL : https://blog.beneth.fr/trackback/38

Page top