Secure Ghost with nginx and Let’s Encrypt

This is a step by step guide on how to secure Ghost with nginx and Let’s Encrypt. This guide is part of the Deploy Ghost series and is split into three parts - Deploying, Securing, and Optimising. This is part two and once we’re done, we will have our Ghost blog running over HTTPS, with a good TLS and security header setup.

Before you start typing into your console, please take the time to research and understand what it is that you’re actually executing on your server. This process worked for me, and might change as the applications have new patches and versions rolled out.

Series Links


Now that we’ve got our server up and running, we need to make the whole thing a lot more secure. Case in point, when you access the Ghost administrator panel and log in, you’re sending those credentials through HTTP, not HTTPS. That’s bad.

Get Utilities

In order to get Let’s Encrypt, we need to use git. Update you repositories and grab it now.

1
2
sudo apt-get update
sudo apt-get install git

Let’s Encrypt

Before we get and execute Let’s Encrypt, we need to set the stage up a bit. In order for Let’s Encrypt to verify that we own the domain we’ll claim we do, we need to open up a directory for it to do it’s verification. We need to edit our ghost configuration file.

1
sudo nano /etc/nginx/sites-available/ghost.conf

Then we need to put this in:

1
2
3
 location '/.well-known/acme-challenge' {
        root        /var/www/ghost;
 }

So we end up with our configuration file looking like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
server {
    listen 80 default_server;
    server_name example.com www.example.com;
    location '/.well-known/acme-challenge' {
        root        /var/www/ghost;
    }
    location / {
        proxy_set_header   Host      $http_host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_pass         http://127.0.0.1:2368;
    }
}

To apply the changes, we need to make sure to parse and reload nginx.

1
sudo nginx -t && sudo nginx -s reload

To get Let’s Encrypt, we clone the repository and we will put it in /opt/letsencrypt.

1
sudo git clone https://github.com/certbot/certbot /opt/letsencrypt

Then we need to execute the certbot. Follow any prompts and read the output to see if it worked correctly. Set example.com to your own domain, and make sure to add in www.example.com as another entry.

1
sudo /opt/letsencrypt/certbot-auto certonly --agree-tos --webroot -w /var/www/ghost -d example.com -d www.example.com

Now we’ve got our certificates lined up, we’ll want to make sure they are automatically renewed. We can do this by using crontab.

1
sudo crontab -e

We’ll want to run the certbot to renew our certificates once a week, and reload nginx if it does perform the renewal. The below should be one line in our crontab.

1
43 5 * * 1 /opt/letsencrypt/certbot-auto renew --quiet --post-hook "/usr/sbin/service nginx reload" >> /var/log/le-renew.log

Configure nginx

To utilise HTTPS we need to change our nginx configuration. We need to add in a permanent redirect to HTTPS, add in our Let’s Encrypt certificates as well as make some adjustments for talking to Ghost. We’ll have a section listening for insecure requests on port 80 and execute a permanent redirect to HTTPS with a 301. Then our section listening for secure requests on port 443 can talk to Ghost, with some slight configuration changes to do so.

So we want to edit our ghost configuration file.

1
sudo nano /etc/nginx/sites-available/ghost.conf

Our configuration file should look like the below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
server {
    listen 80 default_server;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    location '/.well-known/acme-challenge' {
        root /var/www/ghost/;
    }
    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://127.0.0.1:2368;
    }
}

As with all things nginx, we then want to parse the file and reload nginx for our changes to take effect.

1
sudo nginx -t && sudo nginx -s reload

Ghost is Secure

Not only do we have a functioning blog, but it’s also covered by HTTPS. Now we can securely log into the Ghost admin panel, and visitors can be safe knowing their connection is encrypted.

Our job isn’t done just yet as we should strengthen our configuration. There are some things in here that might break your website - so be careful here. Pay attention to what you’re doing and don’t just copy and paste stuff in.

Strengthening Security

The only thing we want to do before we configure nginx is to generate a stronger Diffie-Hellman parameter by using at least 2048 bits. We’ll need this for one of our nginx directives.

1
sudo openssl dhparam 2048 -out /etc/ssl/certs/dhparam.pem

Here’s what our complete configuration looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
server {
    listen 80 default_server;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    server_tokens off;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    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;
    add_header Strict-Transport-Security max-age=15768000; 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";

    location '/.well-known/acme-challenge' {
        root /var/www/ghost/;
    }

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://127.0.0.1:2368;
    }
}

Then parse and reload nginx.

1
sudo nginx -t && sudo nginx -s reload

You can then use Qualys SSL Labs and Security Headers to test out your new security configuration.

Configuration Details

This section will very briefly touch on what each directive does. Please go and research what each one does in depth, especially the headers Strict-Transport-Security and Content-Security-Policy as they can break things badly.

General

By preventing nginx from divulging its version number, we can deny a malicious attacker some useful information. A particular version might be vulnerable to a certain attack.

1
server_tokens off;

SSL/TLS

All we’re doing here is telling nginx where our certificates are.

1
2
3
4
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

This is responsible for enabling OCSP Stapling and provides a DNS server.

1
2
3
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4;

Optimising SSL/TLS.

1
2
3
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

Here we are disabling SSL in favour of TLS. We’ll only accept TLSv1.2, then we provide some ciphers and favour those.

1
2
3
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;

Headers

Instructs client browsers to force HTTPS for 6 months, this includes subdomains. If you don’t serve something via HTTPS, it will not load for the client - for six months.

1
add_header Strict-Transport-Security max-age=15768000; includeSubDomains;  

Prevents a browser from loading your site in an iframe - this safeguards against clickjacking. An iframe is only allowed when it’s being loaded from your domain.

1
add_header X-Frame-Options "SAMEORIGIN" always;

Prevents a browser from MIME-type sniffing and stops the browser from interpreting files as something that they’re not.

1
add_header X-Content-Type-Options "nosniff" always;

Prevents Cross-Site Scripting by sanitising the page if such an attack is detected.

1
add_header X-Xss-Protection "1";

Provides a white-list of approved sources of content that a browser is allowed to load. You can prevent your website from loading with this header. A reporting mode exists, use Content-Security-Policy-Report-Only instead of Content-Security-Policy.

1
add_header Content-Security-Policy "default-src 'self'; script-src 'self' *.google-analytics.com";

Ghost is Secure

We’ve now got a functioning, secure blog. It will keep running and serving content securely, but we can make it serve content a little better. Part three, Optimising Ghost will speed up the blog while reducing server load.

Series Links

References