One pitfall I encountered, that I suppose many other self-hosting enthusiasts have as well, is how to configure Ghost to work correctly with SSL behind a reverse proxy. Turns out, it is quite simple! All one has to do is inject proper forwarded for headers for every step in the chain, and set the base url in the config.production.json
to be https
.
The assumptions I am making here is that you are on a RHEL family Linux distro. If you use a Debian family Linux distro or other *nix distro please sub out the appropriate config file locations respective to your system.
In example, I use a Traefik for my main load-balancer and SSL termination endpoint for my local network, then I use Nginx on my Ghost server to proxy the traffic to the NodeJS backend. The traffic flow looks like this:
Internet -> Cloudflare -> Firewall -> Traefik -> Nginx -> Ghost
Traefik
I manage my Traefik config with an automation tool (SaltStack), and use the dynamic file provider for all my various services. Below is a snipped of my traefik.yml config for the core and Ghost bits. Including, the Cloudflare bits to work with LetsEncrypt. This guide will not cover that specific portion but includes it as a reference so you know what to put in there for Traefik because it was annoying to get working because every guide out there only covers Docker. Lastly, I use Traefik as a managed Systemd service and not as a Docker container.
# /usr/lib/systemd/system/traefik.service
[Unit]
Description=traefik proxy
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
[Service]
Restart=on-abnormal
; Get environmental variables
EnvironmentFile=/etc/environment
; User and group the process will run as.
User=traefik
Group=traefik
; Always set "-root" to something safe in case it gets forgotten in the traefikfile.
ExecStart=/usr/local/bin/traefik --configfile=/etc/traefik/traefik.yml
; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
LimitNOFILE=1048576
; Use private /tmp and /var/tmp, which are discarded after traefik stops.
PrivateTmp=true
; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pi's or other devices, so it has been disabled in this dist.)
PrivateDevices=false
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
; Make /usr, /boot, /etc and possibly some more folders read-only.
ProtectSystem=full
; … except /etc/ssl/traefik, because we want Letsencrypt-certificates there.
; This merely retains r/w access rights, it does not add any new. Must still be writable on the host!
ReadWriteDirectories=/etc/traefik/acme
; The following additional security directives only work with systemd v229 or later.
; They further restrict privileges that can be gained by traefik. Uncomment if you like.
; Note that you may have to add capabilities required by any plugins in use.
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
# /etc/traefik/traefik.yml
global:
checkNewVersion: true
sendAnonymousUsage: false
serversTransport:
insecureSkipVerify: true
log:
level: info
filePath: /var/log/traefik/traefik.log
accessLog:
filePath: /var/log/traefik/access.log
api:
insecure: true
entryPoints:
web:
address: :80
websecure:
address: :443
certificatesResolvers:
letsencrypt:
acme:
email: me@example.com
storage: /etc/traefik/acme/acme.json
keyType: EC384
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 0
providers:
file:
directory: /etc/traefik/dynamic/
http:
routers:
blog:
rule: "Host(`blog.example.com`)"
service: ghost
tls:
certResolver: letsencrypt
services:
ghost:
loadBalancer:
servers:
- url: http://<hostname>.<subdomain>.<domain>.<tld>:80
And don't forget to setup a logrotate for the Traefik log files! This should be enough to get you started.
# /etc/logrotate.d/traefik
/var/log/traefik/*.log {
size 5M
rotate 5
missingok
notifempty
compress
dateformat .%Y-%m-%d
postrotate
/bin/systemctl kill --signal="USR1" traefik
endscript
}
HAProxy
Haproxy config snippets /etc/haproxy/haproxy.cfg
:
Frontend Section
frontend http_redirectTo_https
mode http
bind :80
acl http ssl_fc,not
http-request set-header X-Forwarded-Protocol http if http
http-request redirect scheme https
frontend main
mode http
bind :443 ssl crt /etc/letsencrypt/live/<domain>/fullcert.pem
# set https forward
acl https ssl_fc
http-request set-header X-Forwarded-Protocol https if https
# Add a X-Forwarded-For containing the client IP address if not present
acl h_xff_exists req.hdr(X-Forwarded-For) -m found
http-request add-header X-Forwarded-For %[src] unless h_xff_exists
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
# custom service backends
acl root_dir path_reg ^$|^/$
# Ghost Blog
acl host_blog hdr(host) -i <blog domain>
use_backend blog if host_blog HTTP
Backend Section
# Ghost
backend blog
balance roundrobin
mode http
server blog <ip or fqdn>:<port>
Nginx
Then on my Ghost server /etc/nginx/conf.d/<site>.conf
:
Note: you must hardcode the X-Forwarded-Proto
header to https
server {
listen 80;
server_name <site url>;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:<ghost port>;
proxy_buffering off;
}
location ~ /.well-known {
allow all;
}
client_max_body_size 0;
}
Lastly, in the config.production.json
file under the Ghost root directory, ensure the url
key is HTTPS.
"url": "https://<site url>"
Then do a ghost restart
and now Ghost will properly understand that it is an SSL enabled site.
Cheers.
How To Configure Ghost Behind A Reverse Proxy (Nginx & HAProxy or Traefik)
Configure Ghost to work with HTTPS behind a local network proxy.