Let's Encrypt & Nginx
State of the art secure web deployment
Not long ago SSL encryption was still considered just a nice-to-have feature, and major services secured only log-in pages of their applications.
Things have changed, and for the better: encryption is now considered a must-have, and enforced by most players. Search giant Google even takes SSL implementation into account in search results ranking.
Despite the larger reach of SSL, setting up your own secure web service is still considered daunting, time consuming, and error-prone.
A recent player in the field, Let's Encrypt promises to make SSL certificates more widely available and to radically simplify the workflow of maintaining a website's security.
Combined with the powerful Nginx web server, and with some additional hardening tips, you can use it to achieve top notch security grades, rating A on the popular Qualys SSL and securityheaders.io analysers.
In this article, we walk through the steps needed to achieve this.
What you will do
Here are the steps you will go through:
- Spawn a cloud instance which will host our demo website.
- Do some basic hardening of our server and set up Nginx.
- Install a brand new Let's Encrypt certificate and set up its automatic renewal
- Harden the Nginx configuration
- Harden the Security Headers
- Get that shiny A+ security rating you are looking for
This tutorial will use Exoscale as cloud provider since they offer integrated firewall and DNS management. On top of that Exoscale has a strong focus on data safety / privacy and security. Of course you can follow along using any other cloud or traditional hosting service.
UPDATE 1: This post has been updated on 2016-06-03 to reflect Let's Encrypt evolution (out of beta, new Certbot client), and now deployed on the new Ubuntu 16.04 LTS instead of 14.04.
UPDATE 2: SSL/TLS Ciphers list updated to remove outdated 3DES and switch to Mozilla recommended list.
UPDATE 3 2017/03/03: Added the new Referrer-Policy support
UPDATE 4 2017/06/07: Switch to new Certbot client package
UPDATE 5 2017/07/03: Updated the link to new demo.tar.gz release. Added link to Ansible playbook
Let's Encrypt overview
Let's Encrypt is a new open source certificate authority (CA) providing free and automated SSL/TLS certificates. Their root certificate is well trusted by most browsers, and they are actively trying to reduce the painful workflow of creation - validation - signing - installation - renewal of certificates.
A word of warning before moving on, there are still a few caveats to take into account when considering Let's Encrypt and its Certbot client:
- It requires root privileges.
- The client does not yet "officially" support Nginx (but it works flawlessly).
- It requires a few dependencies (ex. Python).
- Throttling is enforced so you cannot request more than 5 certificates per week for a given domain.
- Certificate is valid for 90 days.
It's possible to get a certificate using other alternate lightweight and less intrusive clients however this tutorial won't cover them.
Infrastructure setup
Let's begin by spawning a new cloud instance. First of all you'll need a public SSH key at hand. If you don't have your own key, or want a quick setup, Exoscale lets you generate one on the fly before starting your machine.
On the Exoscale portal (or the cloud provider of your choice), start a Linux Ubuntu 16.04. For this demo a micro instance (512mb RAM, 1 Vcpu & 10GB disk) will be more than enough. Choose your SSH key on creation and verify that the "default" Security Group is checked.
Within a few seconds our instance is available and ready for use. You can now note down its IP address in order to proceed with the DNS setup.
Exoscale provides DNS zone hosting. Just go under DNS and create a new zone ("letsecure.me" in this example, you'll need to use your own domain here).
Now you may add a "A" record with the value of the IP address of our freshly spawned instance, as well as a "catch all" (wildcard) CNAME record.
If you are following the tutorial on Exoscale don't forget to update the nameservers of your domain with the ones below:
- ns1.exoscale.com
- ns1.exoscale.ch
- ns1.exoscale.net
- ns1.exoscale.io
Basic security hardening of your server
You are now ready to work on our cloud instance, but before beginning to play with certificates and web services, we're going to apply a few elementary security best practices.
On the firewall side you need to allow only the required traffic and deny any other transit. Specifically we'll need to add the rules below:
- 22 (SSH)
- 80 (HTTP)
- 443 (HTTPS)
- ICMP ping (not mandatory but convenient)
On Exoscale firewalls are managed through the interface with what is called Security Groups. By default all incoming traffic is denied and all outgoing traffic is allowed. A good and simple choice on Ubuntu would be UFW.
Another recommended step to harden your machine is to administer it via SSH and keypairs authentication only.
You can now login via SSH using the ubuntu user:
ssh ubuntu@yourdomain.hereNow, if you're using SSH key authentication, and only if so, you may disable SSH password authentication:
sudo sed -i 's|PasswordAuthentication yes|PasswordAuthentication no|g' /etc/ssh/sshd_config
sudo service ssh restartIf you're using UFW, add the rules below:
sudo ufw allow out 22/tcp
sudo ufw allow out 80/tcp
sudo ufw allow out 443/tcpThe next thing to do is to apply all the software updates and patches and reboot the instance:
sudo apt-get update && sudo apt-get dist-upgrade -y && sudo rebootEnable automatic security updates:
sudo dpkg-reconfigure --priority=low unattended-upgradesIt's good practice to install fail2ban to prevent brute force SSH attacks:
sudo apt-get install -y fail2banBasic Nginx Setup
Now that everything is secured you may take care of Nginx. Install the package from the Ubuntu repository:
sudo apt-get install -y nginxCreate the target folder from where our website will be served:
sudo mkdir /var/www/
# download our demo website
wget https://github.com/llambiel/letsecureme/releases/download/1.1.0/demo.tar.gz
sudo tar zxf demo.tar.gz -C /var/www
sudo chown -R root:www-data /var/www/Remove the default Nginx configuration and start with a fresh blank file:
sudo rm /etc/nginx/sites-enabled/default
sudo touch /etc/nginx/sites-enabled/default.confAdjust the Nginx configuration block in /etc/nginx/sites-enabled/default.conf:
server {
listen 80;
server_name default_server;
root /var/www/demo;
}Reload Nginx to apply our configuration change:
sudo nginx -t && sudo nginx -s reloadLet's Encrypt setup, SSL certificates and Nginx HTTPS config
As per the official documentation, Certbot (Let's Encrypt client) can be installed using APT:
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbotYou can now request a certificate for your domain. You'll get prompted to provide your email address for the expiring notifications and accept the Terms:
sudo certbot certonly -a webroot --webroot-path=/var/www/demo -d yourdomain.here -d www.yourdomain.hereOur cert should now be issued and installed! You'll see a success message like:
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/letsecure.me/fullchain.pem. Your cert will
expire on 201X-XX-XX. To obtain a new version of the certificate in
the future, simply run Let's Encrypt again.
- Your account credentials have been saved in your Let's Encrypt
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now.To use your new certificate you need to instruct Nginx to serve it and bind the port 443 on ssl. Use the following minimal configuration block in /etc/nginx/sites-enabled/default.conf:
server {
listen 443 ssl;
server_name yourdomain.here www.yourdomain.here;
root /var/www/demo;
ssl_certificate /etc/letsencrypt/live/yourdomain.here/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.here/privkey.pem;
}Let's reload Nginx:
sudo nginx -t && sudo nginx -s reloadYour website homepage should now be served over HTTPS!
Automatic Certificate Renewal
Let's Encrypt delivers certificates that are valid 90 days only. To ensure automatic renewal, create a script called renewCerts.sh:
#!/bin/sh
# This script renews all the Let's Encrypt certificates with a validity < 30 days
if ! certbot renew > /var/log/letsencrypt/renew.log 2>&1 ; then
echo Automated renewal failed:
cat /var/log/letsencrypt/renew.log
exit 1
fi
/usr/sbin/nginx -t && /usr/sbin/nginx -s reloadSet up a daily cron job:
sudo crontab -eAdd a line with the @daily macro:
@daily /path/to/renewCerts.shDon't forget to make the script executable:
chmod +x /path/to/renewCerts.shNginx SSL/TLS hardening
Remove the actual config in /etc/nginx/sites-enabled/default.conf and replace it with the block below:
server {
listen 80;
listen 443 ssl http2;
server_name yourdomain.here www.yourdomain.here;
ssl_protocols TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers On;
ssl_certificate /etc/letsencrypt/live/yourdomain.here/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.here/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.here/chain.pem;
ssl_session_cache shared:SSL:128m;
add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";
ssl_stapling on;
ssl_stapling_verify on;
# Your favorite resolver may be used instead of the Google one below
resolver 8.8.8.8;
root /var/www/demo;
index index.html;
location '/.well-known/acme-challenge' {
root /var/www/demo;
}
location / {
if ($scheme = http) {
return 301 https://$server_name$request_uri;
}
}
}Reload Nginx:
sudo nginx -t && sudo nginx -s reloadDetail of the SSL/TLS configuration
listen 443 ssl http2;With this directive, you tell Nginx to listen over SSL and also support the connection over the new HTTP/2 standard, if the client browser supports it. Please note that HTTP/2 is SSL/TLS only!
ssl_protocols TLSv1.2;Disable old and weak SSLv2/SSLv3 & early TLS protocols, and allow only the TLSv1.2. Such configuration will prevent older clients to connect (ex. IE10 and older, Android 4.3 and older). Depending on your target audience, you may choose to keep older TLS protocols:
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:...';
ssl_prefer_server_ciphers On;This is the cipher list you tell Nginx to support. This list is well balanced between security and support by older web browsers. Nginx will prefer those ciphers over the ones requested by the client.
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.here/chain.pem;
ssl_stapling on;
ssl_stapling_verify on;Enable OCSP stapling, which improves SSL/TLS handshake performance and privacy.
add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";This adds an HTTP header instructing the client browser to force a HTTPS connection to your domain and to all of its subdomains for 1 year.
Warning! Be careful before applying it in production. You must ensure first that all your subdomains (if any) are being secured as well. Your subdomains will be forced over https as well, and if not properly configured, will become unreachable.
Security headers hardening
Scott Helme created a great HTTP response headers analyser to assess the security grade of our content based on headers.
Let's tune our Nginx configuration by adding a few HTTP headers:
add_header X-Content-Type-Options "nosniff" always;The X-Content-Type-Options header stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type.
add_header X-Frame-Options "SAMEORIGIN" always;The X-Frame-Options header tells the browser whether you want to allow your site to be framed or not. By preventing a browser from framing your site you can defend against attacks like clickjacking.
add_header X-Xss-Protection "1";The X-Xss-Protection header sets the configuration for the cross-site scripting filter built into most browsers.
add_header Content-Security-Policy "default-src 'self'";The Content-Security-Policy header defines approved sources of content that the browser may load. It can be an effective countermeasure to Cross Site Scripting (XSS) attacks.
WARNING! This header must be carefully planned before deploying it on production website as it could easily break stuff and prevent a website to load its content! Use the report mode first:
Content-Security-Policy-Report-Only instead of Content-Security-Policyadd_header Referrer-Policy origin-when-cross-origin;The Referrer-Policy header allows a site to control how much information the browser includes with navigations away from a document.
Final secured Nginx configuration
Your final Nginx configuration should look like this:
server {
listen 80;
listen 443 ssl http2;
server_name yourdomain.here www.yourdomain.here;
ssl_protocols TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers On;
ssl_certificate /etc/letsencrypt/live/yourdomain.here/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.here/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.here/chain.pem;
ssl_session_cache shared:SSL:128m;
add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Xss-Protection "1";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' *.google-analytics.com";
add_header Referrer-Policy origin-when-cross-origin;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8;
root /var/www/demo;
index index.html;
location '/.well-known/acme-challenge' {
root /var/www/demo;
}
location / {
if ($scheme = http) {
return 301 https://$server_name$request_uri;
}
}
}Reload Nginx to apply our new headers:
sudo nginx -t && sudo nginx -s reloadScan your site using securityheaders.io (remember to scan it with the https:// prefix) - you should get an "A" grade!
Setup automation
Thanks to @marcaurele who contributed with an Ansible playbook to automate the above setup.
Conclusion
There are many reasons for deploying SSL/TLS on your website. Security is, naturally, the most important and obvious one. However, it's also a trust building marker for parts of your audience. There are no drawbacks to having an active certificate on your website. With a free certificate from Let's Encrypt and following the steps described in this tutorial, there is absolutely no reason to hesitate.
Let's Encrypt can be easily deployed and maintained on top of Nginx, and with specific SSL/TLS and browser headers hardening you can achieve a modern and secure web deployment.
Source files of this project can be downloaded directly from GitHub.