Problem
When trying to enable ActivityPub/Network features on a self-hosted Ghost instance (with or without Cloudflare), you may encounter:
- The Network tab shows errors or fails to load
- API endpoints return 403 errors with
ROLE_MISSINGorSITE_MISSINGcodes - Ghost logs show:
ERROR No webhook secret found - cannot initialise - Critical issue:
/ghost/.well-known/jwks.jsonreturns 404
Summary
The key insight is that Ghost’s ActivityPub initialization requires reading a JWKS (JSON Web Key Set) endpoint from your local Ghost instance, not from ap.ghost.org. If nginx proxy configuration prevents this endpoint from being served, the ActivityPub handshake fails. By adding an exact-match nginx location block for /ghost/.well-known/jwks.json that proxies to your local Ghost, and then restarting Ghost to trigger re-initialization, the ActivityPub feature should work correctly.
This was tested and verified working on Ghost 6.3.0 with Ubuntu 22.04, nginx 1.18+, and Cloudflare CDN.
Solution
The fix involves ensuring nginx properly proxies the JWKS endpoint to your local Ghost instance instead of blocking it or proxying it to ap.ghost.org.
Step 1: Verify the Problem
Test the JWKS endpoint directly on your Ghost upstream (from the server):
curl -I http://127.0.0.1:2368/ghost/.well-known/jwks.json
Expected result: HTTP/1.1 200 OK
Now test via your public domain:
curl -I https://yourdomain.com/ghost/.well-known/jwks.json
If this returns 404, the nginx configuration needs adjustment.
Step 2: Add JWKS Proxy Configuration
Edit your nginx site configuration (typically /etc/nginx/sites-available/yourdomain.conf):
Before the general location ~ /.well-known block, add the location that matches the endpoint path exactly:
# Serve JWKS from local Ghost app (required for ActivityPub)
location = /ghost/.well-known/jwks.json {
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;
}
Important placement notes:
- This must come before any generic
.well-knownlocation blocks - It must come before the main
location /proxy block - Use
location =(exact match) to prevent conflicts with other paths
Step 3: Validate and Reload nginx
# Test configuration syntax
sudo nginx -t
# If successful, reload nginx
sudo systemctl reload nginx
Step 4: Verify JWKS Endpoint
Test the public endpoint again:
curl -sS https://yourdomain.com/ghost/.well-known/jwks.json
Expected output (keys will be different for your site):
{"keys":[{"e":"AQAB","kid":"xxx","kty":"RSA","n":"xxx","use":"sig"}]}
Step 5: Restart Ghost
For ActivityPub to initialize properly, Ghost needs to restart after the JWKS endpoint is accessible:
# If using systemd (most common)
sudo systemctl restart ghost_yoursitename
# Wait 2 minutes for Ghost to fully initialize
Step 6: Verify ActivityPub Endpoints
After Ghost restarts, test the ActivityPub endpoints:
# Should return 200 (may show data or require authentication)
curl -I https://yourdomain.com/.well-known/webfinger?resource=acct:index@yourdomain.com
# May still show 403 until you configure ActivityPub in admin panel
curl -I https://yourdomain.com/.ghost/activitypub/v1/site
Step 7: Enable Network Feature
- Log into your Ghost Admin panel
- Navigate to Settings → Labs
- Enable the Network toggle
- The Network tab should now load without errors
Common Pitfalls
1. Incorrect Location Block Order
nginx processes location blocks in a specific order. Exact matches (location =) take priority, but if you have overlapping regex matches, placement matters.
Wrong:
location ~ /.well-known {
# Generic block catches everything first
}
location = /ghost/.well-known/jwks.json {
# This never gets hit
}
Correct:
location = /ghost/.well-known/jwks.json {
proxy_pass http://127.0.0.1:2368;
}
location ~ /.well-known {
allow all;
}
2. Proxying JWKS to ap.ghost.org
Some guides suggest proxying all ActivityPub paths to ap.ghost.org. The JWKS endpoint is an exception – it must be served from your local Ghost instance.
Wrong:
location ~ ^/ghost/.well-known {
proxy_pass https://ap.ghost.org; # This breaks JWKS
}
3. Not Waiting Long Enough After Restart
Ghost’s ActivityPub initialization can take 1-2 minutes. If you check immediately after restart, you might still see errors. Wait at least 2 minutes before testing.
4. Cached 404 Responses
If you tested the JWKS endpoint before fixing it, Cloudflare or your browser may have cached the 404. Use:
# Force fresh request
curl -H "Cache-Control: no-cache" https://yourdomain.com/ghost/.well-known/jwks.json
Verification Commands
To confirm everything is working:
# 1. JWKS should return 200 and JSON
curl -sS https://yourdomain.com/ghost/.well-known/jwks.json | jq .
# 2. Webfinger should return 200
curl -I https://yourdomain.com/.well-known/webfinger?resource=acct:index@yourdomain.com
# 3. Check Ghost logs for initialization (Ubuntu/systemd)
sudo journalctl -u ghost_yoursitename -n 50 --no-pager | grep -i activitypub
Additional Notes for Cloudflare Users
If you’re behind Cloudflare, ensure:
- SSL/TLS mode is set to “Full” or “Full (strict)” in Cloudflare dashboard
- Your origin server has valid SSL certificates
- Cloudflare proxy is enabled (orange cloud) for your domain
Still Having Issues?
If this doesn’t resolve your problem, check:
- Ghost logs:
sudo journalctl -u ghost_yoursitename -f(look for ActivityPub errors) - nginx error logs:
/var/log/nginx/error.log - Verify Ghost is actually proxied: The JWKS endpoint must reach Ghost on port 2368 (or your configured port)
- DNS resolution: Ensure
ap.ghost.orgis reachable from your server
Complete Working nginx Example
Here’s a minimal working configuration for the HTTPS server block:
server {
listen 443 ssl http2;
server_name yourdomain.com;
# SSL configuration here...
# CRITICAL: JWKS must be served from local Ghost
location = /ghost/.well-known/jwks.json {
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;
}
# ActivityPub endpoints proxy to ap.ghost.org
location ~ ^/.ghost/activitypub/ {
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_ssl_server_name on;
proxy_pass https://ap.ghost.org;
}
# Federation discovery endpoints
location ~ ^/.well-known/(webfinger|nodeinfo) {
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_ssl_server_name on;
proxy_pass https://ap.ghost.org;
}
# Main Ghost proxy
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;
}
# Allow other well-known endpoints
location ~ /.well-known {
allow all;
}
}