`BUZZ_RELAY_URL` serves three purposes at once, so an agent cannot reach the relay over a private network path
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
`buzz-acp` derives three separate things from the single `BUZZ_RELAY_URL` value:
1. the TCP/TLS connection target
2. the HTTP `Host` header presented on the WebSocket upgrade
3. the `relay` tag signed into the NIP-42 AUTH event
The relay then constrains (2) and (3) independently, and the two constraints together mean the value cannot be changed at all — even when the *connection target* needs to differ from the *public identity* of the relay.
The practical result: an agent deployed **beside** the relay, in the same Kubernetes cluster, cannot connect to it by any in-cluster path. It must egress to the relay's public address and come back in, which is exactly the topology a co-located agent should not need.
## Environment
- Relay: `buzz-relay` 0.2.1, deployed from the published Helm chart `oci://ghcr.io/block/buzz/charts/buzz` 0.1.7
- Agent: `ghcr.io/block/buzz-sprig` (`main`), running `buzz-acp` → `buzz-agent`
- Both in the same Kubernetes namespace; relay reachable in-cluster as a `ClusterIP` Service on plain HTTP
- Relay configured with `RELAY_URL=wss://`, `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`
## Constraint 1 — `Host` must be the bare public authority
The relay resolves its community from the `Host` header before the WebSocket upgrade. `normalize_host` strips only `:443` and `:80`, so any other port remains part of the authority and fails to match.
Measured against a running relay, varying only the `Host` header on the same socket:
| Request | `Host` | Result |
|---|---|---|
| `http://:3000/` | `` | `200`, NIP-11 document |
| `http://:3000/` | `:3000` | `404 relay: no community is configured for this host` |
| `http://:3000/` | `:3000` | `404 relay: no community is configured for this host` |
Reasonable on its own — it fails closed rather than silently selecting a default tenant.
## Constraint 2 — the signed scheme is deployment-wide, not per-connection
`nip42_expected_relay_url` builds the expected AUTH tag as `{scheme}://{tenant.host()}`, where the scheme comes from the deployment's configured `RELAY_URL` rather than from the connection actually in use. The doc comment states this is deliberate, to preserve the deployment's TLS posture.
So a client connecting over plain `ws://` on a trusted internal network still has to sign `wss://…`, and `buzz-acp` signs `BUZZ_RELAY_URL` verbatim.
Measured with a real NIP-42 handshake — same socket, same `Host`, same challenge, varying only the signed `relay` tag:
```
relay tag ws:// -> ["OK", …, false, "auth-required: verification failed"]
relay tag wss:// -> ["OK", …, true, ""]
```
## Why the two together are the problem
Taken separately each is defensible. Together they mean `BUZZ_RELAY_URL` must be **exactly** the public `wss://` URL, so the connection target cannot be varied independently of the identity the client asserts.
Every in-cluster option is therefore closed:
| Attempt | Outcome |
|---|---|
| Point at the `ClusterIP` Service | Fails constraint 1 — wrong `Host`, and the port cannot be stripped |
| Service on port 80 so the port is elided, plus a host alias | Fails constraint 2 — still signs `ws://` |
| Terminate TLS in-cluster with a private CA | `buzz-acp` is built with `rustls-tls-webpki-roots`, so only compiled-in Mozilla roots are trusted. No env var or mounted bundle can add a CA |
| Register a second community for the internal host | Scheme is deployment-wide; and a second host is a separate tenant, so the agent would not see the existing channels |
The remaining option is to make the public hostname reachable from inside the network, which for us meant widening a load-balancer security group that was deliberately restricted. That is a real security trade forced by a client-side URL-parsing decision.
## Suggested direction
Any one of these would resolve it; the first seems smallest:
1. **Derive the expected AUTH scheme per-connection** rather than from the deployment-wide `RELAY_URL` — or accept either scheme for the same host when the connection is already trusted.
2. **Separate the concerns in `buzz-acp`**: an optional connect-target override (something like `BUZZ_RELAY_CONNECT_URL`) while `BUZZ_RELAY_URL` remains the identity used for `Host` and the signed tag. This mirrors how many clients separate "where to dial" from "who I am talking to".
3. **Allow additional accepted hosts for a community**, so an internal address maps to the same tenant rather than a new one.
Happy to test a patch against our deployment.
## Not a bug report about
Relay membership, NIP-42 itself, or the `Host`-based tenant lookup in isolation. Membership and auth both work correctly — a member key signing `wss://` authenticates cleanly. The issue is only that the three uses of one value cannot be varied independently.
Contributor guide
Research direction
Start in buzz-acp by tracing how BUZZ_RELAY_URL supplies the connection target, Host header, and signed NIP-42 relay tag, then inspect nip42_expected_relay_url on the relay side. Reproduce the in-cluster connection cases described in the issue and choose a design that lets the dial target vary without changing the relay identity. Done means the existing public connection still authenticates and a private network path reaches the same relay community.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- helm, kubernetes, rust
- Domain
- authentication, infrastructure, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100