Almost a year ago, I changed my blogging system from Wordpress to Ghost - Ghost 1.x, nginx, Ubuntu 16.04 DigitalOcean droplet. This blog was running on Ghost 1.x till the day before yesterday. Ghost team released 2.0 and I could not stop myself from upgrading. These are my notes taken during the upgrade.

SOME HISTORY

This blog was happily hosted on Wordpress's server in 2017. Things were smooth, stable and reliable. What was lacking was control. I decided to self-host this blog.

I looked at Amazon Web Services, Azure and many other cloud services. I decided to try DigitalOcean. I began my self-hosting adventures on a 512 MB memory, 20 GB SSD, Ubuntu 16.04 droplet. I just fell in love with the whole experience. Eventually, I moved on to a 1 GB memory, 30 GB SSD, Ubuntu 16.04 droplet.

DigitalOcean modified their droplet plans and I benefitted. My droplet was upgraded to a 2 GB memory, 50 GB SSD.

I have had zero issues with DigitalOcean as far as my droplet is concerned. Their billing system, however, never accepts my credit cards. So, I have to PayPal my way out of trouble.

THE UPGRADE ADVENTURE

Preparing for demolition

  • Installed FileZilla on my Fedora 28 workstation.
  • Connected to my DigitalOcean droplet. Instructions on how to connect can be found here.
  • Backed up /etc, /var and /home/<user>
  • Exported the json containing all data using Ghost admin console.
  • Took a snapshot of the droplet in DigitalOcean.

Demolition

Destroyed the droplet mercilessly. This is the point of no return. The importance of a proper backup plan cannot be oveemphasized. You may back up everything and not need it at all. However, if you do not backup wisely and end up needing some old data...

Resurrection

  • Choosing the right droplet and priming it  Ubuntu 18.04, 2 GB RAM, 1 vCPU, 50 GB SSD, 2 TB transfer, $10/mo - Bangalore.
  • Arranged appropriate SSH key for secure communication with my workstation and tested the connection Open /etc/ssh/sshd_config and add the following to prevent SSH connection from getting timed out:
ClientAliveInterval 120
ClientAliveCountMax 720
  • Restart sshd service:
systemctl restart sshd

Let’s Encrypt certificate

Let’s Encrypt is a free, automated, and open Certificate Authority.

Let's Encrypt client

Let’s Encrypt uses the ACME protocol to verify that you control a given domain name and to issue you a certificate. To get a Let’s Encrypt certificate, you’ll need to choose a piece of ACME client software to use.

The list of clients is huge. I chose my old trusted friend acme.sh by Neilpang. Read GitHub instructions properly for thorough understanding:

curl https://get.acme.sh | sh

The installer will perform 3 actions:

  • Create and copy acme.sh to your home dir ($HOME): ~/.acme.sh/. All certs will be placed in this folder too.
  • Create alias for: acme.sh=~/.acme.sh/acme.sh.
  • Create daily cron job to check and renew the certs if needed.

Cron entry example:

0 0 * * * "/home/user/.acme.sh"/acme.sh --cron --home "/home/user/.acme.sh" > /dev/null

Enable autoupgrade for acme.sh:

acme.sh --upgrade --auto-upgrade

The last time I had acquired certificates for my domain in 2017, I used the manual method. There were a few manual steps needed to prove my ownership over my domains. One of the steps involved creating a directory and mentioning the same in Nginx configuration. This directory is then accessed by Let's Encrypt servers to solve a challenge.

This time though, I decided to use an automated method - Automatic DNS API integration for DigitalOcean:

export DO_API_KEY="1234ab1234de1234ef1234gh1234ij1234kl1234mn1234op1234qr"
  • Get the certificates:
acme.sh --issue --dns dns_dgon -d example.com -d www.example.com

This will get all needed certificates without any further human intervention.

After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers. You MUST use this command to copy the certs to the target files, DO NOT use the certs files in ~/.acme.sh/ folder, they are for internal use only, the folder structure may change in the future.

An example:

acme.sh --install-cert -d msiyer.com -d www.msiyer.com -d blog.msiyer.com \
--key-file       /etc/nginx/certs/<domain>.key  \
--fullchain-file /etc/nginx/certs/fullchain.cer \
--reloadcmd     "service nginx force-reload"

Install Ghost

Follow the instructions in the Ghost production install guide to the word. The last step is to run the following command:

ghost install

A few questions were asked:

  • Answered honestly.
  • When automatic setting up of Nginx was offered, I decline.
  • I decline the automagical setting up of SSL also.
  • Transcript:
<user>@<vps>:/var/www/<directory>$ ghost install
✔ Checking system Node.js version
✔ Checking logged in user
✔ Checking current folder permissions
✔ Checking operating system compatibility
✔ Checking for a MySQL installation
✔ Checking memory availability
✔ Checking for latest Ghost version
✔ Setting up install directory
✔ Downloading and installing Ghost v2.0.3
✔ Finishing install process
? Enter your blog URL: https://www.msiyer.com
? Enter your MySQL hostname: localhost
? Enter your MySQL username: ghost
? Enter your MySQL password: [hidden]
? Enter your Ghost database name: db_name
✔ Configuring Ghost
✔ Setting up instance
Running sudo command: useradd --system --user-group ghost
? Sudo Password [hidden]
Running sudo command: chown -R ghost:ghost /var/www/<directory>/content
✔ Setting up "ghost" system user
? Do you wish to set up "ghost" mysql user? Yes
MySQL user is not "root", skipping additional user setup
ℹ Setting up "ghost" mysql user [skipped]
? Do you wish to set up Nginx? No
ℹ Setting up Nginx [skipped]
Task ssl depends on the 'nginx' stage, which was skipped.
ℹ Setting up SSL [skipped]
? Do you wish to set up Systemd? Yes
✔ Creating systemd service file at /var/www/<directory>/system/files/ghost_www-msiyer-com.service
Running sudo command: ln -sf /var/www/<directory>/system/files/ghost_www-msiyer-com.service /lib/systemd/system/ghost_www-msiyer-com.service
Running sudo command: systemctl daemon-reload
✔ Setting up Systemd
? Do you want to start Ghost? No

Nginx configuration

Used the following as template for Secure Sockets Layer (SSL) connection and created /etc/nginx/sites-available/<https_config>.conf file:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name <SD.SLD.TLD>;
    root /var/www/<directory>/system/nginx-root;

    ssl_certificate /etc/nginx/certs/fullchain.cer;
    ssl_certificate_key /etc/nginx/certs/<domain>.key;
    include /var/www/<directory>/system/files/ssl-params.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;

    }

    client_max_body_size 50m;
}

I created a symbolic link for this file in /etc/nginx/sites-enabled/<https_config>.conf. The reader will notice that the include /var/www/<directory>/system/files/ssl-params.conf; needs to be fulfilled for the configuration to work. Else, the nginx -t test would fail.

To fulfill the above-mentioned include, create the file with following content:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
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_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

ssl_dhparam /etc/nginx/certs/<domain>/dhparam.pem;

The ssl_ciphers block was picked up from Mozilla website.

ssl_dhparam needs to be generated using:

sudo openssl dhparam -out /etc/nginx/certs/<domain>/dhparam.pem 2048

Used the following as template for unsecured connection and created /etc/nginx/sites-available/<http_config>.conf file:

server {
    listen 80;
    listen [::]:80;

    server_name <SD.SLD.TLD>;
    root /var/www/<directory>/system/nginx-root;

    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;

    }

    client_max_body_size 50m;
}

I created a symbolic link for this file in /etc/nginx/sites-enabled/<http_config>.conf

This absence of this file will not let the server serve pages if request is http.

QUALYS SSL REPORT

How secure are my SSL settings?

QualSys SSL Report
Qualys SSL Report for www.msiyer.com

Qualys says:

    A+ - exceptional configuration
    A - strong commercial security
    B - adequate security with modern clients, with older and potentially obsolete crypto used with older clients; potentially smaller configuration problems
    C - obsolete configuration, uses obsolete crypto with modern clients; potentially bigger configuration problems
    D - configuration with security issues that are typically difficult or unlikely to be exploited, but can and should be addressed
    E - unused
    F - exploitable and/or patchable problems, misconfigured server, insecure protocols, etc.