Skip to content

Fixed dev gateway forcing X-Forwarded-Proto https on ActivityPub routes#29076

Merged
9larsons merged 1 commit into
mainfrom
slars/dev-gateway-forwarded-proto
Jul 6, 2026
Merged

Fixed dev gateway forcing X-Forwarded-Proto https on ActivityPub routes#29076
9larsons merged 1 commit into
mainfrom
slars/dev-gateway-forwarded-proto

Conversation

@9larsons

@9larsons 9larsons commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Accessing the ActivityPub service through the docker dev gateway over plain http://localhost:2368 fails: Ghost↔AP auth breaks and you can't get the two talking locally.

Root cause: the gateway hardcodes header_up X-Forwarded-Proto https on the three ActivityPub routes. The AP service reconstructs request URLs from that header (via x-forwarded-fetch's behindProxy), and its role-guard only downgrades the Ghost JWKS fetch to http when the request URL isn't https (role-guard.ts). With the spoofed header it always tries https://localhost:2368/ghost/.well-known/jwks.json, nothing serves TLS there, JWT verification fails, and every AP request 401s.

Why the hardcode existed

The dev gateway was built around the tunnel workflow (né dev:forward): the documented AP dev setup points a Tailscale funnel / ngrok https domain at port 80, TLS terminates at the agent, and everything reaches Caddy as plain http. Forcing https upstream kept Ghost from redirect-looping and made the AP service mint https federation URLs in that flow.

Fix

Tell the AP service the truth, without breaking the tunnel flow:

  • Added trusted_proxies static private_ranges to the global options. Everything reaching this gateway arrives from loopback or the docker bridge, so Caddy treats the sender as a proxy hop and passes through X-Forwarded-Proto when the client supplies it — which tailscale funnel, ngrok, and cloudflared all do.
  • Removed the hardcoded X-Forwarded-Proto: https from the three AP routes. Direct localhost requests now get http (fixing local dev); tunneled requests still arrive upstream as https.
  • The Ghost-backend routes are untouched and still force https: Ghost builds its URLs from config and only uses this header for redirect decisions, so the lie is harmless there and prevents redirect loops when url is an https tunnel domain.

This also matches the AP repo's own reference proxy — its dev nginx config maps X-Forwarded-Proto to https-if-claimed-else-http. The hardcode on these routes was a copy-paste artifact from the Ghost blocks.

Verification

caddy validate + caddy fmt clean with the dev-gateway image. Ran the real gateway container against a header-echo upstream:

Request X-Forwarded-Proto seen upstream
/.ghost/activitypub/* direct localhost http ✅ (was https — the bug)
/.ghost/activitypub/* with tunnel-agent header https ✅ passed through
/.well-known/webfinger, /.well-known/nodeinfo same as above ✅
/foo, /ghost/api/* (Ghost backend) https ✅ unchanged

Known edge: a tunnel that terminates TLS but injects no X-Forwarded-Proto (e.g. a raw ssh -R TCP forward) would now stamp http where the hardcode papered over it — the documented agents all inject the header.

No automated test — this config only runs in the local dev environment; verification above exercises the real image + Caddyfile.

no ref

- the ActivityPub service derives its URLs — and whether it fetches Ghost's
  JWKS over http or https — from X-Forwarded-Proto (via x-forwarded-fetch's
  behindProxy), so hardcoding https broke plain http://localhost:2368 dev:
  the AP service tried to fetch the JWKS over TLS from a plain-http gateway
  and Ghost<->AP auth failed
- the hardcode existed for the tunnel workflow (tailscale funnel / ngrok),
  where TLS terminates at the agent and everything reaches Caddy as http;
  trusted_proxies now covers that case instead — the agents inject
  X-Forwarded-Proto: https and connect from private ranges, so Caddy passes
  the header through untouched
- this matches the AP repo's own dev nginx config, which maps
  X-Forwarded-Proto to https-if-claimed, otherwise http; the hardcode on
  these routes was a copy-paste artifact from the Ghost blocks
- the Ghost-backend routes keep forcing https: Ghost builds URLs from config
  and only uses the header for redirect decisions, so the lie is harmless
  there and prevents redirect loops when url is configured as https
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The dev-gateway Caddyfile is updated to configure trusted proxies on the :80 site via servers { trusted_proxies static private_ranges }, allowing Caddy to preserve the incoming X-Forwarded-Proto header rather than overriding it. Additionally, the explicit X-Forwarded-Proto https header previously forced on upstream requests for the /.ghost/activitypub/*, /.well-known/webfinger, and /.well-known/nodeinfo reverse proxy routes is removed, leaving only Host and X-Real-IP forwarded to the ActivityPub proxy target.

Changes

  • Added trusted_proxies static private_ranges configuration to the :80 site block
  • Removed forced X-Forwarded-Proto: https header from three ActivityPub-related reverse proxy blocks
  • Updated accompanying comments to reflect scheme derivation from the real request scheme

Related issues: None specified.

Related PRs: None specified.

Possibly related PRs

  • TryGhost/Ghost#28983: Directly overlaps with the same dev-gateway reverse_proxy blocks' forwarded header edits for ActivityPub/WebFinger/NodeInfo endpoints.

Suggested labels: docker, configuration

Suggested reviewers: None specified.

🐰 A hop, a trust, a proxy's flight,
No forced scheme to override the light,
Caddy now lets the true proto through,
ActivityPub knows just what to do.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: stopping the dev gateway from forcing X-Forwarded-Proto https on ActivityPub routes.
Description check ✅ Passed The description is directly related to the changeset and explains the bug, fix, and verification steps.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slars/dev-gateway-forwarded-proto

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 816d68a

Command Status Duration Result
nx run @tryghost/admin:build ✅ Succeeded 8s View ↗
nx run ghost:build:assets ✅ Succeeded 2s View ↗
nx run ghost:build:tsc ✅ Succeeded 7s View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded <1s View ↗
nx run-many -t lint -p ghost-monorepo ✅ Succeeded <1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-03 13:48:16 UTC

@9larsons 9larsons requested a review from sagzy July 3, 2026 13:50
@9larsons 9larsons merged commit e278ff7 into main Jul 6, 2026
42 checks passed
@9larsons 9larsons deleted the slars/dev-gateway-forwarded-proto branch July 6, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants