For most of my sites that provide an administrative back-end, I try to place them behind a client-side SSL challange. This way, the only way someone can access the administrative web portal, is to present a valid client certificate signed by my personal CA. Most of this is done in nginx.

The goal is to allow HTTPS traffic to the CMS without the need for a client certificate, but restrict the admin portal. Here are both the Ghost and nginx configs for client-side SSL:

  1. While in the Ghost configuration file, ensure you include the "admin" section:
    {
    "url": "https://blog.fqdn",
    "admin": { 
      "url": "https://admin-portal.fqdn" 
    },
    "server": {
      "port": ...
    
  2. Restart Ghost. This can be done using the Ghost-cli or by using the custom FreeBSD RC script.

Next we move to nginx. In this example, the Ghost CMS is hosted on a different machine and the nginx host will proxy traffic to it:

upstream ghost_upstream {
    server 172.16.1.1:2368;
    }

server {
        listen 80;
        server_name blog.fqdn admin-portal.blog.fqdn;
        return 301 https://$server_name$request_uri;  # enforce https
        }

server {
        listen 443 ssl;
        ssl_certificate		certs/ghost.crt;
        ssl_certificate_key	certs/ghost.key;

        server_name blog.fqdn;

        location / {
                proxy_set_header        X-Forwarded-Host        $host;
                proxy_set_header        X-Forwarded-For         $proxy_add_x_forwarded_for;
                proxy_set_header        Host                    $host;
                proxy_set_header        X-Forwarded-Proto       $scheme;
                proxy_pass                                      ghost_upstream;
		proxy_redirect					off;
		client_max_body_size                  10m;
		client_body_buffer_size               128k;
	}

    location ~ ^/(?:ghost|signout) { 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass ghost_upstream;
        add_header Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0";
        proxy_set_header X-Forwarded-Proto https;
    }
}

server {
        listen 443 ssl;
        ssl_certificate		certs/ghost.crt;
        ssl_certificate_key	certs/ghost.key;
        ssl_client_certificate certs/rootca.crt;
        
        server_name admin-portal.blog.fqdn;

        ssl_verify_client	on;
        ssl_verify_depth	2;
        expires			0;
        add_header		Cache-Control private;
        if ($ssl_client_s_dn !~ "CN=My Full Name") {
            return 403;
        }

        location / {
                proxy_set_header        X-Forwarded-Host        $host;
                #proxy_set_header       X-Forwarded-Server      $host;
                proxy_set_header        X-Forwarded-For         $proxy_add_x_forwarded_for;
                proxy_set_header        Host                    $host;
                proxy_set_header        X-Forwarded-Proto       $scheme;
                proxy_pass                                      ghost_upstream;
                proxy_redirect                                  off;
                client_max_body_size                  10m;
                client_body_buffer_size               128k;
        }

    location ~ ^/(?:ghost|signout) {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass ghost_upstream;
        add_header Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0";
        proxy_set_header X-Forwarded-Proto https;
    }
}

You'll notice that the first and second server sections are fairly similar except for the following section:

ssl_client_certificate certs/rootca.crt;

ssl_verify_client	on;
        ssl_verify_depth	2;
        expires			0;
        add_header		Cache-Control private;
        if ($ssl_client_s_dn !~ "CN=My Full Name") {
            return 403;
        }

Caveat: You will need to create your own CA and client certificates. Both CA and client certificate will need to be imported into your browser.