NGINX Caching and Securing "Laika Boss"

Just wanted to share some of my NGINX config to cache everything that ghost does, and securing the admin panel. This is very aggressive caching and my have some vulnerabilities.

Ideally I wanted to cache not only visitors, but logged in users as well. Testing on a t4g.small AWS instance, it was able to handle about 600 non-cached requests at once before timing out due to CPU usage. With caching I’m able to throw over 10k requests without going past 15% CPU usage. So I setup two "proxy_cache_path"s in a ramdisk one for visitors, and the other for logged in users. Since all logged in users see the same pages with just one tier of subscription, they all work fine being served from the same path. However, I did try to make individual user caches but the etag header ends up breaking things and would require a work-around for that.

So all basic page and posts are cached, the admin panel is secured by IP, Certbot is used for SSL since the ghost ssl script only uses TLS 1.2 for some reason. Remember to purge the cache whenever you change content. Doing this manually at the moment so you can do it however you like.

Any suggestions are appreciated.

This uses cookies as validating the cache so there may be vulnerabilities associated with that

proxy_cache_path /var/www/cache levels=1:2 keys_zone=ghostcache:100m inactive=60m;
proxy_cache_path /var/www/cache/users levels=1:2 keys_zone=ghostusercache:100m inactive=60m;
proxy_cache_key "$scheme$request_method$host$request_uri$cookie_ghost-members-ssr.sig$cookie_ghost-members-ssr";
proxy_cache_use_stale error timeout invalid_header http_500;
proxy_ignore_headers Cache-Control Expires Set-Cookie;


server {

    server_name MYWEBSITEHERE.com;
    root /var/www/MYWEBSITEHERE/system/nginx-root; # Used for acme.sh SSL verification (https://acme.sh)
	
	set $skip_cache 0;
	set $cache ghostcache;
	
	if ($http_cookie ~* "ghost-members-ssr|ghost-admin-api-session") {
		set $cache ghostusercache;
	}
		
	if ($request_uri ~* "ghost/|p/|members/") {
		set $skip_cache 1;
	}  
	
    location / {
        # Add header for cache status (miss or hit)
        add_header X-Cache-Status $upstream_cache_status;
		
		
		proxy_cache_bypass $skip_cache;
		proxy_no_cache $skip_cache;
		proxy_cache_valid 60m;
		proxy_cache $cache;
		
		
	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;		
        
    }
	
	location ~ ^/(ghost/) {
        allow PUTYOURIPHERE;
        deny all;
		
	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;
	}
	
    location ~ /.well-known {
        allow all;
    }
	

    client_max_body_size 50m;

    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/MYWEBSITEHERE.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/MYWEBSITEHERE.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
    if ($host = MYWEBSITEHERE.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name MYWEBSITEHERE.com;
    return 404; # managed by Certbot


}
2 Likes

Thank you for sharing! :smiling_face_with_three_hearts:

I tried something similar a long time ago, but couldn’t find a way to automatically purge the cache after the content is updated. :confused:

Maybe someone here has a solution for this? :thinking:

I updated the post, but you may want to change the cache key to
proxy_cache_key “$scheme$request_method$host$request_uri$cookie_ghost-members-ssr.sig$cookie_ghost-members-ssr”;

Since this is using cookies to validate cache, there can be vulnerabilities associated with that.

Playing around I realized that NGINX won’t read the value of the cookies set by ghost, and I haven’t figured out why. Instead I decided to use the stripe cookies to create individual user caches, and serve the same public cache to everyone else.

proxy_cache_path /var/www/cache levels=1:2 keys_zone=ghostcache:10m max_size=120m inactive=1d;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;

server {

    server_name MYWEBSITE.com;
    root /var/www/MYWEBSITE/system/nginx-root; # Used for acme.sh SSL verification (https://acme.sh)
	
	set $skip_cache 0;
    set $usercookie 0;

	if ($http_cookie ~* "ghost-members-ssr(.sig)") {
        set $usercookie $cookie___stripe_mid;
    }

	if ($request_uri ~* "ghost/|p/|members/") {
		set $skip_cache 1;
	}  
	
    location / {
        add_header X-Cache-Status $upstream_cache_status;
		
		proxy_cache_key $request_uri$usercookie;
		proxy_cache_bypass $skip_cache;
        proxy_no_cache $skip_cache;
		proxy_cache_valid 1d;
		proxy_cache ghostcache;
		
		
		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;		
        
    }
	
	location ~ ^/(ghost/) {
        #Add your IP here
        allow MYIPHERE;
        deny all;
		
		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;
	}
	
    location ~ /.well-known {
        allow all;
    }
	

    client_max_body_size 50m;

    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/MYWEBSITE.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/MYWEBSITE.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
    if ($host = MYWEBSITE.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name MYWEBSITE.com;
    return 404; # managed by Certbot


}

Looks like we can make a per user cache by using regex to read the weird cookie name.

proxy_cache_path /var/www/cache levels=1:2 keys_zone=ghostcache:10m max_size=120m inactive=7d;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;

server {

    server_name MYWEBSITE.com;
    root /var/www/MYWEBSITE/system/nginx-root; # Used for acme.sh SSL verification (https://acme.sh)
	
	set $skip_cache 0;
    set $usercookie 0;

    if ($http_cookie ~* "ghost-members-ssr.sig=([A-Za-z-0-9]+)") {
        set $usercookie $1;
    }

	if ($request_uri ~* "ghost/|p/|members/") {
		set $skip_cache 1;
	}  
	
    location / {
      
		proxy_cache_key $request_uri$usercookie;
		proxy_cache_bypass $skip_cache;
        proxy_no_cache $skip_cache;
		proxy_cache_valid 7d;
		proxy_cache ghostcache;
		
		
		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;		
        
    }
	
	location ~ ^/(ghost/) {
        allow MYIPADDRESS;
        allow 127.0.0.1;
        deny all;
		
		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;
	}
	
    location ~ /.well-known {
        allow all;
    }
	

    client_max_body_size 50m;

    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/MYWEBSITE.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/MYWEBSITE.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
    if ($host = MYWEBSITE.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;

    server_name MYWEBSITE.com;
    return 404; # managed by Certbot


}
1 Like