Securing the Blog with TLS

· by Jonas K├Âritz · Read in about 4 min · (740 Words)
Web nginx letsencrypt

Using HTTPS on every HTTP-based system seems like a good idea, but the cost of trusted certificates has been a deterrent for many non-commercial blog authors and open source projects. For some time now letsencrypt offers a free and secure alternative to the costly certificate authorities. I wanted to secure this blog using HTTPS too and went through the Let’s Encrypt certification process.

Let’s Encrypt uses the ACME Protocol to verify that you are the owner of the domain name you are requesting a certificate for. To get a certificate issued by Let’s Encrypt, you have to use an ACME compatible client. The EFF provides an ACME compatible client named Certbot that is easy to use and well documented. Certbot is especially well suited for deploying certificates to web servers like Apache or nginx.

To verify your ownership of the domain, Certbot offers identification methods known as Plugins. The most commonly used Plugins might be the web server specific apache and nginx Plugin as well as the universal web-root plugin.
I did not want Certbot to take over my web server conifiguration and chose the web-root plugin to prove my domain ownership. The ACME protocol uses a well-known path on your web server to verify the domain name. Certbot will place a file at the provided web-root location that the verifying server expects at this well-known path. In nginx you will have to configure a new location for this path.

location ~ /.well-known {
	allow all;
}

After downloading certbot, all you need to do is run it with the appropriate parameters, my nginx server document root is at /usr/share/nginx/html so certbot will have to place its verification files inside it.

certbot certonly --webroot -w /usr/share/nginx/html -d blog.jonaskoeritz.de -d jonaskoeritz.de

certonly will tell certbot to only request the certificates and store them in your filesystem. By default the certificate and private key will be stored inside /etc/letsencrypt/live/<first domain name>. Certbot also provides a handy fullchain.pem file containing the required intermediate certificates for direct usage in nginx like this:

ssl on;
ssl_certificate /etc/letsencrypt/live/blog.jonaskoeritz.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.jonaskoeritz.de/privkey.pem;

For extra security, head over to Mozillas SSL Configuration Generator and generate a suitable configuration for your server. The generator takes care of the critical TLS parameters like cipher suites for you.

Finally head over to the Moziall Observatory to test your configuration against security best practices. The attached documentation will help you with any problems reported. In my case, I had to provide additional HTTP headers to pass the tests. Fortunately, nginx makes this super easy:

add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' https:";
add_header X-XSS-Protection "1; mode=block";

The “Content-Security-Policy” is the most tricky bit. Compliant browsers like Chrome will restrict the loading of any resources to the sources you provide inside this HTTP header (Have a look at the Network tab in Chrome developer tools, it will show blocked sources as status {blocked:csp}). In my case, I am using Google Web Fonts and Disqus comments on my site (because the Hyde-Y theme uses Google Web Fonts). Unfortunatly Disqus is integrated as inline JavaScript in Hyde-Y and Disqus uses the eval() function inside its embed.js script, this requires to set unsafe-inline and unsage-eval in your Conten-Security-Policy header. After all, Mozilla granted me a B+ rating!

The finished nginx configuration file looks like this:

server {
	listen 80;
	server_name blog.jonaskoeritz.de;

	root /usr/share/nginx/html;

	# Serve ACME-Challenges via HTTP
	location /.well-known/acme-challenge {
		location ~ /.well-known/acme-challenge/(.*) {
			default_type text/plain;
		}
	}

	# Redirect everything else permanently to HTTPS
	location / {
		return 301 https://$host$request_uri;
	}
}

server {
	listen 443;
	server_name blog.jonaskoeritz.de;
	
	# Enable TLS and setup letsencrypt certificates
	ssl on;
	ssl_certificate /etc/letsencrypt/live/blog.jonaskoeritz.de/fullchain.pem;
   	ssl_certificate_key /etc/letsencrypt/live/blog.jonaskoeritz.de/privkey.pem;

   	# Use openssl to generate your own dhparams.pem
   	# openssl dhparam -out /etc/nginx/ssl/dhparams.pem 2048
   	# This will take some time
	ssl_dhparam /etc/nginx/dhparams.pem;
	
	# TLS Configuration as suggested by Mozilla
	ssl_session_timeout 1d;
	ssl_session_cache shared:SSL:50m;

	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    	ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
    	ssl_prefer_server_ciphers on;
	
	ssl_stapling on;
	ssl_stapling_verify on;
	
	# Trust letsencrypt for OCSP Stapling
	ssl_trusted_certificate /etc/letsencrypt/live/blog.jonaskoeritz.de/chain.pem;

	# Add headers to satisfy Mozilla Observatories best practices
	add_header Strict-Transport-Security max-age=15768000;
	add_header X-Content-Type-Options nosniff;
	add_header X-Frame-Options DENY;
	# unsafe-inline and unsafe-eval are needed for Disqus to work correctly, https: ensures every resource is loaded via https
	add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' https:";
	add_header X-XSS-Protection "1; mode=block";

	# Set Hugos 404 Page as the error page
	error_page 404 /404.html;

	# Set the document root to Hugos public folder
	location / {
		root /var/www/blog/public;
	}
}

Comments