Securing your Self-Hosted Ghost 5.20 instance with NGINX

Posted on Wed 02 November 2022 in Ghost, NGINX, Security

I just set up blog.riamaria.com a few days ago. It's been really fun setting up the server. I looked up some ways to secure my Ghost instance better. Great ideas here and there, and mostly just regular AppSec things.

With this guide, I assume that Ghost handled the NGINX configs for you upon installation. Here are some things that you can do with NGINX to harden your Self-Hosted Ghost instance:

💡 Note that you after saving the NGINX config files, you should run nginx -t before reloading NGINX ( server reload nginx ) to propagate changes gracefully.

Redirect from HTTP to HTTPS

Unfortunately, the default Ghost instance will allow http://yourblog.example to go through to your site. I hope at this point in time people know better than to go to HTTP only sites due to lack of encryption in-transit.

As root, edit /etc/nginx/sites-enabled/yourblog.example.conf:

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

/etc/nginx/sites-available/yourblog.example.conf

to

location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        return 301 https://$host$request_uri;
}

/etc/nginx/sites-available/yourblog.example.conf

This will permanently redirect you from HTTP to HTTPS if you go to the HTTP site.

Removing HTTP headers

One reason to not publish the NGINX version in the response headers of your site is to make it a little more difficult for attackers to sniff what version you are running on. Running on a known vulnerable version will make it more likely for bad actors to try and do something funny on your instance.

As root, edit /etc/nginx/nginx.conf and uncomment server_tokens off;. It should look like this:

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        server_tokens off;

/etc/nginx/nginx.conf

Another thing is that you might want to remove the X-Powered-By: Express header as well. In both /etc/nginx/sites-enabled/yourblog.example.conf and /etc/nginx/sites-enabled/yourblog.example-ssl.conf, add the following line: proxy_hide_header X-Powered-By;. It should look like this:

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        return 301 https://$host$request_uri;
        proxy_hide_header X-Powered-By;
    }

/etc/nginx/sites-available/yourblog.example.conf

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

    }

/etc/nginx/sites-available/yourblog.example-ssl.conf

Restrict Admin Access to your IP only

This part was a little tricky for me to get. Depending on whether or not you are using Cloudflare, you may have to use a different conditional.

Adapted from: https://www.blakejarvis.com/miscellaneous-articles/how-to-secure-the-ghost-blogging-platform

    location = /ghost/ {
        if ($proxy_add_x_forwarded_for ~* [YOUR_IP_ADDRESS]) {
            proxy_pass http://127.0.0.1:2368$request_uri;
        }
    }

    location = /ghost/api/admin/ {
        if ($proxy_add_x_forwarded_for ~* [YOUR_IP_ADDRESS]) {
            proxy_pass http://127.0.0.1:2368$request_uri;
        }
    }

    location = /ghost/api {
        allow all;
        proxy_pass http://127.0.0.1:2368$request_uri;
    }

Using Cloudflare: /etc/nginx/sites-available/yourblog.example-ssl.conf

As you can see above, we are using the $proxy_add_x_forwarded_for variable to match with the IP address. Below is an example of a server that does not use Cloudflare:

    location = /ghost/ {
        if ($remote_addr ~* [YOUR_IP_ADDRESS]) {
            proxy_pass http://127.0.0.1:2368$request_uri;
        }
    }

    location = /ghost/api/admin/ {
        if ($remote_addr ~* [YOUR_IP_ADDRESS]) {
            proxy_pass http://127.0.0.1:2368$request_uri;
        }
    }

    location = /ghost/api {
        allow all;
        proxy_pass http://127.0.0.1:2368$request_uri;
    }

Not using Cloudflare: /etc/nginx/sites-available/yourblog.example-ssl.conf

Do take note that if you made a mistake and you tested it, your browser may cache the 301 Redirect. Simply turn on your Developer tools (F11) and under Network, tick Disable Cache. That should allow you to go back to http://yourblog.example/ghost and try again.

Configure Security Headers in NGINX

Another thing you can do is add security headers to your NGINX config.

Read the following article for more information: https://webdock.io/en/docs/how-guides/security-guides/how-to-configure-security-headers-in-nginx-and-apache

Note: I have spent a long while to try and establish a proper Content Security Policy, but that doesn't seem to be possible with how Ghost themes are currently made. Your mileage may vary.