fix: take the public origin out of the published default host list #5

Merged
jlxq0 merged 3 commits from fix/allowed-hosts-from-deployment into main 2026-08-27 02:00:21 +00:00
Owner

Blocked. Do not merge until oddie-apps/platform sets
HEVY_MCP_ALLOWED_HOSTS on the hevy-mcp Deployment.
That half is Clark's and
the state is handed over below.

src/config.rs carried hevy-mcp.oddie.app as a DEFAULT_ALLOWED_HOSTS entry,
asserted it in a config test, and sent it as the Host header in two router
tests. This repository is public. b7167c2 scrubbed that name from the README and
AGENTS.md; the code still published it, so the scrub achieved nothing.

It was also load-bearing rather than decorative, which is why it survived. The
deployment does not set HEVY_MCP_ALLOWED_HOSTS, so live Host validation
passes today only because the origin is compiled in.

rmcp's check is real, measured before touching it

On the default config, an initialize carrying a bearer:

Host status
localhost 200
127.0.0.1 200
any other name 403

And the same name returns non-403 once it is in allowed_hosts. So this is a
change to a setting that does something, not a tidy-up of a value nobody reads.

What changed

DEFAULT_ALLOWED_HOSTS is now localhost,127.0.0.1,::1 and names no origin.
Two tests pin it in both directions — default_allowed_hosts_are_loopback_only
and a_configured_public_host_is_accepted. The remaining tests use
hevy-mcp.example, which is reserved by RFC 2606 and resolves nowhere.

Verified by removal rather than by passing: putting a public host back into
DEFAULT_ALLOWED_HOSTS turns both new tests red plus the config test.

test config::tests::constructor_normalizes_url_and_defaults_to_loopback_hosts_only ... FAILED
test tests::default_allowed_hosts_are_loopback_only ... FAILED
test result: FAILED. 44 passed; 2 failed

The platform half

clusters/fondue/hevy-mcp/deployment.yaml, in the container's env: list:

            - name: HEVY_MCP_ALLOWED_HOSTS
              value: "hevy-mcp.oddie.app"

One name, no loopback entries needed — the variable replaces the default rather
than extending it, and the probes reach the container on its pod IP, not through
the Host check. hevy-mcp.oddie.app is the hostnames: entry in the same
directory's httproute.yaml, so the two must agree.

Setting it is a no-op against the currently deployed image, which carries the
default. It only becomes load-bearing at the first release built from this
commit — which is the ordering: platform first, this second, tag third.

What happens if the order is wrong

The first release built from this commit answers 403 to every request arriving
on the public name, while /health keeps returning 200, the readiness and
liveness probes keep passing, the pod stays Ready and ArgoCD reads Synced and
Healthy. A green rollout serving nothing — the same failure shape as the
hardcoded initialize bucket that v0.3.0 fixed, where /health stayed 200 while
every connector got 429.

Verification

Gates on 1.98.0: fmt clean, clippy clean, 46 tests pass (44 + the 2 new).

> **Blocked. Do not merge until `oddie-apps/platform` sets > `HEVY_MCP_ALLOWED_HOSTS` on the hevy-mcp Deployment.** That half is Clark's and > the state is handed over below. `src/config.rs` carried `hevy-mcp.oddie.app` as a `DEFAULT_ALLOWED_HOSTS` entry, asserted it in a config test, and sent it as the `Host` header in two router tests. This repository is public. b7167c2 scrubbed that name from the README and `AGENTS.md`; the code still published it, so the scrub achieved nothing. It was also load-bearing rather than decorative, which is why it survived. The deployment does not set `HEVY_MCP_ALLOWED_HOSTS`, so live `Host` validation passes today only because the origin is compiled in. ## rmcp's check is real, measured before touching it On the default config, an `initialize` carrying a bearer: | `Host` | status | |---|---| | `localhost` | 200 | | `127.0.0.1` | 200 | | any other name | **403** | And the same name returns non-403 once it is in `allowed_hosts`. So this is a change to a setting that does something, not a tidy-up of a value nobody reads. ## What changed `DEFAULT_ALLOWED_HOSTS` is now `localhost,127.0.0.1,::1` and names no origin. Two tests pin it in both directions — `default_allowed_hosts_are_loopback_only` and `a_configured_public_host_is_accepted`. The remaining tests use `hevy-mcp.example`, which is reserved by RFC 2606 and resolves nowhere. Verified by removal rather than by passing: putting a public host back into `DEFAULT_ALLOWED_HOSTS` turns both new tests red plus the config test. ``` test config::tests::constructor_normalizes_url_and_defaults_to_loopback_hosts_only ... FAILED test tests::default_allowed_hosts_are_loopback_only ... FAILED test result: FAILED. 44 passed; 2 failed ``` ## The platform half `clusters/fondue/hevy-mcp/deployment.yaml`, in the container's `env:` list: ```yaml - name: HEVY_MCP_ALLOWED_HOSTS value: "hevy-mcp.oddie.app" ``` One name, no loopback entries needed — the variable replaces the default rather than extending it, and the probes reach the container on its pod IP, not through the `Host` check. `hevy-mcp.oddie.app` is the `hostnames:` entry in the same directory's `httproute.yaml`, so the two must agree. Setting it is a no-op against the currently deployed image, which carries the default. It only becomes load-bearing at the first release built from this commit — which is the ordering: platform first, this second, tag third. ## What happens if the order is wrong The first release built from this commit answers `403` to every request arriving on the public name, while `/health` keeps returning 200, the readiness and liveness probes keep passing, the pod stays `Ready` and ArgoCD reads `Synced` and `Healthy`. A green rollout serving nothing — the same failure shape as the hardcoded initialize bucket that `v0.3.0` fixed, where `/health` stayed 200 while every connector got 429. ## Verification Gates on 1.98.0: `fmt` clean, `clippy` clean, 46 tests pass (44 + the 2 new).
jlxq0 force-pushed fix/allowed-hosts-from-deployment from 5a8dc3aefd
Some checks failed
CI / cargo (pull_request) Failing after 1m57s
CI / docker (pull_request) Has been skipped
to 81b6d1cbdb
Some checks failed
CI / docker (pull_request) Has been cancelled
CI / cargo (pull_request) Has been cancelled
2026-08-26 06:22:48 +00:00
Compare
jlxq0 force-pushed fix/allowed-hosts-from-deployment from 4b2b85dd80
All checks were successful
CI / cargo (pull_request) Successful in 35s
CI / docker (pull_request) Successful in 31s
to a1b7347878
All checks were successful
CI / cargo (pull_request) Successful in 2m0s
CI / docker (pull_request) Successful in 10s
2026-08-26 06:53:23 +00:00
Compare
jlxq0 force-pushed fix/allowed-hosts-from-deployment from a1b7347878
All checks were successful
CI / cargo (pull_request) Successful in 2m0s
CI / docker (pull_request) Successful in 10s
to d163d51d27
All checks were successful
CI / cargo (pull_request) Successful in 40s
CI / docker (pull_request) Successful in 6s
2026-08-26 07:34:55 +00:00
Compare
jlxq0 referenced this pull request from a commit 2026-08-27 01:58:36 +00:00
jlxq0 force-pushed fix/allowed-hosts-from-deployment from d163d51d27
All checks were successful
CI / cargo (pull_request) Successful in 40s
CI / docker (pull_request) Successful in 6s
to b80acc12ae
All checks were successful
CI / cargo (pull_request) Successful in 38s
CI / docker (pull_request) Successful in 46s
2026-08-27 01:58:36 +00:00
Compare
Author
Owner

Unblocked. oddie-apps/platform merged at eddfe3b19d915577460ef39bcc211c1bbff78a5f. Verified against the cluster myself rather than from the report, since a fact heard twice can be one measurement heard twice:

hevy-mcp-54fd4849b4-bmqzw   Running   restarts=0
imageID  sha256:1a2ad09f...                     unchanged, only env moved
HEVY_MCP_ALLOWED_HOSTS = hevy-mcp.oddie.app     on the running app container

Rebased onto a950280 and bumped to 0.4.0, since this release changes behaviour rather than only dependencies and documentation.

Gates on 1.98.0, through the build slot: fmt, clippy, test (46 passed), audit, deny all pass, and cargo +1.93.0 check --all-features --locked still passes, so the build floor is unmoved.

A probe I can no longer run, recorded because it looks like it should work

The platform lead retracted one of its own checks and it changes mine. It sent -H "Host: wrong.example" at /mcp expecting a rejection and got 200. Not the host check being absent: SNI still said hevy-mcp.oddie.app while the HTTP Host mismatched, so the Gateway never matched the HTTPRoute and something else in the ingress path answered.

A negative probe that never reaches the thing under test looks exactly like the thing under test behaving. So the allow-list cannot be verified from outside by sending a wrong Host, because the edge will not route it. A negative control would have to run inside the cluster against the pod, where SNI does not exist. The check that works from outside sends the right Host with a bearer and reads 200 against 403, which is the one already in AGENTS.md.

What the post-release check will be

Not "ArgoCD is Synced" and not /health. In order:

  1. Confirm a docker task exists on the tag ref in GET actions/tasks, then that it succeeded. Absent is a third state below skipped: ksc_web's v1.7.0 had a green test and no docker task at all, and two earlier releases were forfeited that way.
  2. Read the running container's digest by container name app, per pod, against what the tag build pushed. A pod name followed by a tab and nothing is a container name that matched nothing, not a pod without the image.
  3. The bearer probe against the live origin. This is the first time that probe carries information: today it returns 200 whether the variable is set or not, because the running image still has the origin compiled into DEFAULT_ALLOWED_HOSTS. Against the deployment built from this commit, 200 means the variable landed and 403 means it did not.
**Unblocked.** `oddie-apps/platform` merged at `eddfe3b19d915577460ef39bcc211c1bbff78a5f`. Verified against the cluster myself rather than from the report, since a fact heard twice can be one measurement heard twice: ``` hevy-mcp-54fd4849b4-bmqzw Running restarts=0 imageID sha256:1a2ad09f... unchanged, only env moved HEVY_MCP_ALLOWED_HOSTS = hevy-mcp.oddie.app on the running app container ``` Rebased onto `a950280` and bumped to **0.4.0**, since this release changes behaviour rather than only dependencies and documentation. Gates on 1.98.0, through the build slot: `fmt`, `clippy`, `test` (46 passed), `audit`, `deny` all pass, and `cargo +1.93.0 check --all-features --locked` still passes, so the build floor is unmoved. ## A probe I can no longer run, recorded because it looks like it should work The `platform` lead retracted one of its own checks and it changes mine. It sent `-H "Host: wrong.example"` at `/mcp` expecting a rejection and got **200**. Not the host check being absent: SNI still said `hevy-mcp.oddie.app` while the HTTP `Host` mismatched, so the Gateway never matched the HTTPRoute and something else in the ingress path answered. **A negative probe that never reaches the thing under test looks exactly like the thing under test behaving.** So the allow-list cannot be verified from outside by sending a wrong `Host`, because the edge will not route it. A negative control would have to run inside the cluster against the pod, where SNI does not exist. The check that works from outside sends the **right** `Host` with a bearer and reads 200 against 403, which is the one already in `AGENTS.md`. ## What the post-release check will be Not "ArgoCD is Synced" and not `/health`. In order: 1. Confirm a `docker` **task exists** on the tag ref in `GET actions/tasks`, then that it succeeded. Absent is a third state below skipped: `ksc_web`'s `v1.7.0` had a green `test` and no docker task at all, and two earlier releases were forfeited that way. 2. Read the running container's digest by container name `app`, per pod, against what the tag build pushed. A pod name followed by a tab and nothing is a container name that matched nothing, not a pod without the image. 3. The bearer probe against the live origin. **This is the first time that probe carries information**: today it returns 200 whether the variable is set or not, because the running image still has the origin compiled into `DEFAULT_ALLOWED_HOSTS`. Against the deployment built from this commit, 200 means the variable landed and 403 means it did not.
jlxq0 merged commit 78f0387b12 into main 2026-08-27 02:00:21 +00:00
jlxq0 deleted branch fix/allowed-hosts-from-deployment 2026-08-27 02:00:22 +00:00
Author
Owner

Rolled out and verified. Not from the ArgoCD tile and not from /health.

hevy-mcp-7b8dbddcbf-lldrw   Running
imageID  sha256:084f2835...        matches what the v0.4.0 tag build pushed
old pod carrying sha256:1a2ad09f... terminated before anything was sampled
HEVY_MCP_ALLOWED_HOSTS = hevy-mcp.oddie.app   on the running app container

The digest came from the registry by Docker-Content-Digest on the manifest, and the manifest pin Renovate wrote is the same value, so three independent sources agree on v0.4.0.

The probe, and its negative control

From outside, with a bearer:

POST /mcp  Authorization: Bearer probe   ->  200
/health                                  ->  200
/mcp  unauthenticated                    ->  401
/.well-known/oauth-authorization-server  ->  404

That 200 is the first one in this repository that carries information: the image no longer has any origin in DEFAULT_ALLOWED_HOSTS, so it can only be answering from the environment variable.

It still had no negative control, and the obvious one is a trap. Sending Host: wrong.example to the public name returns 200, because SNI still says hevy-mcp.oddie.app while the HTTP Host differs, so the Gateway never matches the HTTPRoute and something else in the ingress path answers. The platform lead found that and retracted its own probe; a negative probe that never reaches the process looks exactly like the process behaving.

Run inside the cluster, port-forwarded to the pod where there is no SNI:

Host status
hevy-mcp.oddie.app 200
localhost 403
wrong.example 403
hevy-mcp.kampong.social 403

The localhost row is the one that settles it. localhost is in DEFAULT_ALLOWED_HOSTS and it is being rejected, which is only possible if the environment variable replaced the compiled default rather than extending it. Under v0.3.0 both localhost and hevy-mcp.oddie.app would have returned 200 from the compiled list. That pair is what separates "the variable is doing the work" from "the old default still is", and neither the 200 alone nor a green rollout can tell them apart.

The method is going into AGENTS.md on #9.

**Rolled out and verified.** Not from the ArgoCD tile and not from `/health`. ``` hevy-mcp-7b8dbddcbf-lldrw Running imageID sha256:084f2835... matches what the v0.4.0 tag build pushed old pod carrying sha256:1a2ad09f... terminated before anything was sampled HEVY_MCP_ALLOWED_HOSTS = hevy-mcp.oddie.app on the running app container ``` The digest came from the registry by `Docker-Content-Digest` on the manifest, and the manifest pin Renovate wrote is the same value, so three independent sources agree on `v0.4.0`. ## The probe, and its negative control From outside, with a bearer: ``` POST /mcp Authorization: Bearer probe -> 200 /health -> 200 /mcp unauthenticated -> 401 /.well-known/oauth-authorization-server -> 404 ``` That 200 is the first one in this repository that carries information: the image no longer has any origin in `DEFAULT_ALLOWED_HOSTS`, so it can only be answering from the environment variable. **It still had no negative control, and the obvious one is a trap.** Sending `Host: wrong.example` to the public name returns 200, because SNI still says `hevy-mcp.oddie.app` while the HTTP `Host` differs, so the Gateway never matches the HTTPRoute and something else in the ingress path answers. The `platform` lead found that and retracted its own probe; a negative probe that never reaches the process looks exactly like the process behaving. Run inside the cluster, port-forwarded to the pod where there is no SNI: | `Host` | status | |---|---| | `hevy-mcp.oddie.app` | **200** | | `localhost` | **403** | | `wrong.example` | 403 | | `hevy-mcp.kampong.social` | 403 | **The `localhost` row is the one that settles it.** `localhost` is in `DEFAULT_ALLOWED_HOSTS` and it is being rejected, which is only possible if the environment variable replaced the compiled default rather than extending it. Under `v0.3.0` both `localhost` and `hevy-mcp.oddie.app` would have returned 200 from the compiled list. That pair is what separates "the variable is doing the work" from "the old default still is", and neither the 200 alone nor a green rollout can tell them apart. The method is going into `AGENTS.md` on #9.
Sign in to join this conversation.
No reviewers
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/hevy-mcp!5
No description provided.