docs: record the 302 auth-redirect fingerprint and the server's exact HTTP surface
Phase 1 exposure now verified end to end (2026-08-12): /healthz -> ok and an unauthenticated POST /mcp -> 401, both from a client container and from the primary itself. 3.4 and 3.6 marked passed. Two findings from the failure in between, worth more than a checkbox: 1. Leaving Pangolin's resource authentication on does NOT surface as 401 or 403. It is a 302 with `location: https://pangolin.jordbo.se/auth/resource/<uuid> ?redirect=...`, sent *before* the request is proxied -- so the palace never sees it and its journal stays silent, `curl -s` prints an empty body, and an MCP client gets non-JSON. That is 1.1's "Pangolin's HTTP auth is browser-shaped; the clients are not" arriving as a concrete symptom rather than an argument. Now recorded with the exact header shape and the two curl flags that reveal it (-D-, -w '%{redirect_url}'), plus the reading: a 302 is good news, because DNS, TLS and routing all worked and only auth intervened. 2. Read the server's routing to settle whether path-scoped proxy rules are sufficient. The entire HTTP surface is two exact paths (mcp_server.py:5299- 5318): GET /healthz (no token, Host/Origin gated) and POST /mcp (Bearer, compare_digest on the exact string). Everything else is a 404 from the palace itself. So path-scoped rules are tighter than a host-wide proxy and lose nothing. Two client-facing consequences: /mcp is matched exactly, so a trailing slash 404s -- configure clients without one; and there is no GET /mcp, no SSE, no session id, no DELETE, so this is plain JSON-RPC over POST, not MCP streamable-HTTP. A strict client opening with a GET handshake sees 404. Also means the verify step needs no initialize and no Accept: text/event-stream, which the old snippet left ambiguous. Also: run the 200 and 401 from two different networks, not one -- passing from only the primary leaves split-horizon DNS untested.
This commit is contained in:
@@ -194,10 +194,11 @@ If that hangs or refuses while the Pangolin dashboard shows the site online, the
|
||||
*target* is wrong — look at the resource's upstream address (§3.5), not at newt. Note this check needs
|
||||
§3.1 done first: if `mempalace-serve` is not running yet, it fails for that reason alone.
|
||||
|
||||
### 3.4 DNS at the web hotel
|
||||
### 3.4 DNS at the web hotel — ✅ done 2026-08-12
|
||||
|
||||
One CNAME: `mempalace` → **the same target your existing Pangolin resources use** (nyvaken's public
|
||||
hostname). RFC §6.2 costed this as *"one DNS record per service on the web hotel is the whole setup cost."*
|
||||
Done: `mempalace.jordbo.se` resolves and terminates TLS at Pangolin on nyvaken — the §6.2 estimate held.
|
||||
|
||||
⚠️ Not verified from here: nyvaken's public FQDN, and whether your web hotel permits a CNAME at that label
|
||||
(some require an A record, or forbid CNAME where other records exist). Confirm before assuming a 5-minute job.
|
||||
@@ -212,20 +213,56 @@ hostname). RFC §6.2 costed this as *"one DNS record per service on the web hote
|
||||
looks perfectly healthy on `curl 172.17.0.1:8765/healthz`. (Got this wrong on the first attempt
|
||||
2026-08-12, because this line used to omit the scheme.)
|
||||
- **Auth: none at the Pangolin layer** (§1.1). TLS termination only.
|
||||
- ⚠️ **If resource auth is left on, the signature is a `302`, not a 401 or 403** — hit for real 2026-08-12:
|
||||
```
|
||||
HTTP/2 302
|
||||
location: https://pangolin.jordbo.se/auth/resource/<uuid>?redirect=https%3A%2F%2Fmempalace.jordbo.se%2Fhealthz
|
||||
content-length: 0
|
||||
```
|
||||
This is §1.1's "browser-shaped auth" arriving as a concrete symptom: Pangolin sends the login redirect
|
||||
**before** proxying, so the palace never sees the request and its journal stays silent. `curl -s` shows
|
||||
an empty body and an MCP client sees non-JSON. Diagnose with `-D-` or
|
||||
`-w '%{http_code} %{redirect_url}'` — a `location:` pointing at `/auth/resource/…` means the fix is in
|
||||
the Pangolin UI (switch the site's authentication off), not in the palace, the unit, or newt.
|
||||
**A 302 is unambiguously good news:** DNS, TLS and routing all worked — only the auth layer intervened.
|
||||
- Do **not** attach an `Origin`-injecting proxy or browser client: a *present* non-loopback `Origin` is a
|
||||
hard 403 with no override (runbook §2.4 B3).
|
||||
|
||||
### 3.6 Verify end-to-end before flipping any client
|
||||
**The server's entire HTTP surface is two exact paths**, so path-scoped rules cover it completely
|
||||
(`mcp_server.py:5299-5318`, read 2026-08-12):
|
||||
|
||||
| Method + path | Auth | Notes |
|
||||
| --- | --- | --- |
|
||||
| `GET /healthz` | none (Host/Origin gated only) | the liveness probe; works with no creds by design |
|
||||
| `POST /mcp` | `Authorization: Bearer <token>`, `hmac.compare_digest` on the exact string | the whole tool surface |
|
||||
| anything else | — | `send_error(404)` from the palace itself |
|
||||
|
||||
Three consequences worth having in writing:
|
||||
|
||||
- **Path-scoped Pangolin rules are not a compromise here, they are tighter than a host-wide proxy** and
|
||||
lose nothing — there is no third endpoint to forget.
|
||||
- **`/mcp` is matched exactly** (`if path != "/mcp"`), so a client URL with a trailing slash gets a 404
|
||||
from the palace. Configure clients as `https://mempalace.jordbo.se/mcp` — no trailing slash.
|
||||
- **There is no `GET /mcp`, no SSE, no session id, no `DELETE`.** This is plain JSON-RPC over POST, not MCP
|
||||
streamable-HTTP. A strict client that opens with a `GET` handshake will see 404; pi's extension,
|
||||
opencode `type:remote` and the feeder all POST directly and are fine.
|
||||
|
||||
### 3.6 Verify end-to-end before flipping any client — ✅ passed 2026-08-12
|
||||
|
||||
```sh
|
||||
curl -s https://mempalace.jordbo.se/healthz # ok
|
||||
curl -s https://mempalace.jordbo.se/healthz # ok ✓ from devbox AND synlig
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
|
||||
https://mempalace.jordbo.se/mcp # 401 — the token is doing its job
|
||||
https://mempalace.jordbo.se/mcp # 401 ✓ the token is doing its job
|
||||
TOKEN=$(cat ~/.mempalace/server/f5d849287f6d73f0141b29d7/token) # on synlig
|
||||
curl -s -X POST https://mempalace.jordbo.se/mcp \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 300 # 36 tools
|
||||
```
|
||||
|
||||
No `initialize` and no `Accept: text/event-stream` needed — see the surface table above; a bare
|
||||
`tools/list` POST is a complete request. Run the 200 and the 401 from **two different networks** (a client
|
||||
box and the primary itself): passing from only one leaves split-horizon DNS untested.
|
||||
|
||||
The 401 check matters as much as the 200: it is the only evidence that the thing you just published to the
|
||||
internet is not open. Then, and only then, Phase 1 client flip — **one machine first** (RFC §8), and
|
||||
remember opencode containers need the §4.1 sidecar merge before their `.env` takes effect.
|
||||
|
||||
Reference in New Issue
Block a user