Ghost CMS runs as a Node.js application typically fronted by nginx (or Caddy). Security headers belong at the nginx layer, not in Ghost itself — Ghost doesn't have built-in header management. This guide covers every fix our Security Audit raises on Ghost: HTTP security headers in the nginx reverse proxy, Ghost's session and cookie configuration, and publishing security.txt. Tested against Ghost 5.95 on Node 20 and nginx 1.24. For nginx (Ghost's typical reverse proxy), see the nginx variant; for the full finding catalogue, see Security Audit Fixes.
Ghost-CLI installs typically create a vhost at /etc/nginx/sites-available/yourdomain.com-ssl.conf.
ls /etc/nginx/sites-available/Open the file matching your domain with
-ssl suffix: sudo nano /etc/nginx/sites-available/yourdomain.com-ssl.conf
server { listen 443 ssl http2; ... }, before the location / block: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'self'; connect-src 'self'" always;
'unsafe-inline' for both — restrict only after auditing your specific theme.js.stripe.com and api.stripe.com to script-src and connect-src respectively.sudo nginx -t sudo nginx -s reloadIf
nginx -t reports any error, fix before reloading.Ghost's session cookies inherit settings from the Node.js cookie library. The defaults are reasonable; the only common hardening is forcing HTTPS-only cookies.
config.production.json at your Ghost install root: {
"url": "https://yourdomain.com",
...
} When url uses https://, Ghost automatically sets the Secure flag on session cookies.cd /var/www/ghost ghost restart
location = /.well-known/security.txt {
default_type text/plain;
return 200 "Contact: mailto:security@yourdomain.com\nExpires: 2027-05-18T00:00:00.000Z\nPreferred-Languages: en\nCanonical: https://yourdomain.com/.well-known/security.txt\n";
} Reload nginx. This serves the file directly without disk I/O.curl -sI https://yourdomain.com/ | grep -iE "strict-transport|x-frame|x-content|referrer|permissions|content-security"Re-run the Security Audit.
Verify every header, TLS protocol and security.txt with a fresh scan.
Run Security Audit →