08e344b047
Both corrections come from the first real Phase 1 start on synlig (2026-08-12).
1. The Pangolin resource target was documented as bare `172.17.0.1:8765` with no
scheme, and the obvious guess from that is `https://` -- which cannot work.
`contrib/systemd/mempalace-serve.service` runs `serve --host 172.17.0.1
--port 8765` with no --tls-cert, so the primary speaks plaintext HTTP; TLS
terminates at Pangolin, which is the whole point of the RFC 6.2 decision.
Point a proxy at https:// and it attempts a TLS handshake against a plaintext
listener: 502 from outside, while `curl 172.17.0.1:8765/healthz` on the box
still says ok -- a confusing pair of symptoms. Now spelled `http://` with the
failure mode named, in the runbook and in the unit's comments.
2. `curl -s 127.0.0.1:8765/healthz` was documented as "expect 403". Wrong: the
real run returns empty. With a docker0-only bind nothing is listening on
loopback, so the connection is refused at TCP level before any header is sent
(%{http_code} -> 000, exit 7). The 403 is the *loopback-bind* case verified
2026-08-10 -- server on 127.0.0.1 answering a proxy-forwarded foreign Host.
Two distinct behaviours had been collapsed into one expectation in three
places (both runbooks and the unit).
Worth stating why the correction matters rather than just fixing it: refusal
is the *stronger* signal. A 403 proves only that a request was rejected; a
refused connection proves the loopback and LAN surface is not listening at
all. Someone who expected 403, saw silence, and "fixed" it by rebinding to
0.0.0.0 would have converted a correct configuration into an exposed one.
The docs now also say what to do if it hangs, or if ss shows 0.0.0.0:8765.
68 lines
3.5 KiB
Desktop File
68 lines
3.5 KiB
Desktop File
[Unit]
|
|
Description=MemPalace remote MCP server (the fleet primary — RFC-001)
|
|
Documentation=https://gitea.jordbo.se/joakimp/mempalace-toolkit
|
|
Documentation=file:%h/mempalace-toolkit/docs/rfc-001-global-palace.md
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
# Refuse to start if the palace is missing — better a clear failure than a
|
|
# server quietly creating an empty palace somewhere unexpected.
|
|
ConditionPathExists=%h/.mempalace/palace
|
|
|
|
[Service]
|
|
Type=simple
|
|
# ── Bind address: NOT 127.0.0.1. This is deliberate and load-bearing. ────────
|
|
# mempalace pins the HTTP Host header to loopback literals *only on a loopback
|
|
# bind* (mcp_server.py: enforce_host_pin = _http_is_loopback(host)). Behind a
|
|
# reverse proxy / tunnel that forwards the public hostname, a loopback bind
|
|
# answers 403 Forbidden. Verified empirically on synlig 2026-08-10:
|
|
# bind 127.0.0.1 + Host: palace.example.com -> 403
|
|
# bind 172.17.0.1 + Host: palace.example.com -> 200
|
|
# 172.17.0.1 is the docker0 gateway: non-loopback (so the pin relaxes), but
|
|
# reachable only from this host and its containers — so a newt/Pangolin tunnel
|
|
# container on this box can reach it while the LAN cannot. Use 0.0.0.0 only if
|
|
# the tunnel does not run in Docker here, and only with the firewall closed.
|
|
#
|
|
# The Origin check is NEVER relaxed: a request carrying a non-loopback Origin
|
|
# is 403 with no override. Fine for MCP clients (they send none); fatal for
|
|
# browser-based clients.
|
|
#
|
|
# No --token here on purpose: for a non-loopback bind, serve reuses or mints a
|
|
# 0600 token at ~/.mempalace/server/<hash-of-palace-path>/token and keeps it
|
|
# stable across restarts. Read it from there to configure clients; never paste
|
|
# it into this unit (units are world-readable).
|
|
ExecStart=%h/.local/bin/mempalace serve --host 172.17.0.1 --port 8765
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
# The server serializes every request behind one lock, so a wedged process is
|
|
# a fleet-wide outage. Fail fast and let Restart= recover.
|
|
TimeoutStopSec=30
|
|
# Journal: journalctl --user -u mempalace-serve -f
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
# Modest hardening (user units can't do much, but these are free)
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectKernelTunables=true
|
|
ProtectControlGroups=true
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
|
|
# ── Enabling (needs one sudo, hence not done by the Phase 0 prep) ───────────
|
|
# sudo loginctl enable-linger $USER # else the unit dies with your login
|
|
# systemctl --user daemon-reload
|
|
# systemctl --user enable --now mempalace-serve
|
|
# curl -s 172.17.0.1:8765/healthz # -> ok
|
|
# curl -s localhost:8765/healthz # -> nothing: connection refused, exit 7.
|
|
# Corrected 2026-08-12 (the earlier "-> 403" here was wrong). With this
|
|
# docker0-only bind nothing listens on loopback, so the connection is
|
|
# refused before any header is sent. The 403 above is the *loopback-bind*
|
|
# case: server on 127.0.0.1 receiving a forwarded foreign Host header.
|
|
# Refusal is the stronger signal -- it proves loopback/LAN isn't listening.
|
|
#
|
|
# ── Reverse-proxy target: use http://, not https:// ──────────────────────────
|
|
# There is no --tls-cert below, so this server speaks PLAINTEXT HTTP. Point
|
|
# the tunnel/proxy resource at http://172.17.0.1:8765. Targeting https:// makes
|
|
# the proxy attempt a TLS handshake against a plaintext listener: 502 from
|
|
# outside, while curl on 172.17.0.1 still says ok. TLS belongs at the proxy.
|