verify kai-server:30122 is not publicly reachable - Dowel's turn endpoint refuses nobody #866

Closed
opened 2026-08-18 21:01:46 +00:00 by coilyco-ops · 1 comment
Owner

Verify whether kai-server:30122 is reachable from outside the LAN, and from the router rather than from a doc.

What is on that port

The sirens-dowel lane publishes NodePort 30122 on kai-server (services/sirens-echo/deploy/sirens-dowel-values.yaml, nodeSelector: kubernetes.io/hostname: kai-server). The pod binds SIRENS_ECHO_HTTP_ADDR: 0.0.0.0:8080, moved off the process default of 127.0.0.1:8080 by the deployment.

That one port serves both /mcp, which exposes the lane as a single turn tool, and POST /v1/turn. Both run a full agent turn on the same path Discord uses.

The lane runs tailnet.enabled: false, so there is no Tailscale sidecar and no MagicDNS name. The NodePort is its only inbound path, and Tailscale ACLs are not in front of it.

The endpoint authenticates nobody

SIRENS_ECHO_HTTP_TOKEN reads as an auth control and is not one.

  • internal/community/httpidentity.go:22 - callerTrusted does a correct constant-time compare of the X-Sirens-Trust style header against the configured token.
  • internal/community/http.go:227 - the result is assigned, written to the span attribute http.caller.trusted, and set on the turn struct. That span attribute is the only consumer in the tree. There is no 401, no 403, no early return.

So an anonymous caller and a token-bearing caller both get their turn run. The only difference is how the trace is labelled. The SSM description says as much: "records who called rather than refusing anyone."

What does gate is shared admission and rate limiting keyed on X-Sirens-Caller, plus a 4xx contract for malformed requests. Neither is an authorization control.

Why this needs checking rather than assuming

sirens-echo/docs/sirens-echo-http.md states the boundary as network-only: "the NodePort is LAN and tailnet only, the home router forwards nothing to it."

infrastructure#103 found the opposite for port 22 and closed with "this was b/c of DMZ config." A DMZ host takes every inbound port, not port 22 alone. If kai-server is still in the router's DMZ, 30122 is reachable from the public internet and the doc's stated boundary does not hold. #103 closed with an explanation rather than a recorded remediation, so whether the DMZ was removed is not answerable from any repo.

ufw was active on kai-server at the time of #103 with rules unverified. k3s installs its own iptables chains for NodePort traffic, which commonly bypass ufw's INPUT filtering, so an active ufw is not by itself an answer either.

What to check

  1. Whether kai-server is still in the router's DMZ, or has any forward reaching 30122. From the router admin, not from a doc.
  2. Whether ufw/iptables actually drops 30122 from non-LAN, non-tailnet sources, tested rather than read.
  3. ss -lntp on kai-server for what else the DMZ answer would expose, since 30120 and 30121 carry the Echo and Deep lanes on the same footing.

Impact if it is reachable

An unauthenticated caller runs turns as Sirens Dowel. That lane holds Discord write grants, so a turn can post into the owl.glass guild. It also spends inference per call, bounded only by the shared admission limiter.

Timing note: Dowel is staged for a public Temporal livestream on 2026-08-19, 11:00 to 11:50 PDT.

Not in scope here

Making the token refuse instead of annotate is a sirens-echo change against internal/community/http.go:227 plus a rebuild and rollout. Worth a separate issue there. This one is the network question only.

Filed by Claude. Investigated read-only, nothing changed.

**Verify whether `kai-server:30122` is reachable from outside the LAN, and from the router rather than from a doc.** ## What is on that port The `sirens-dowel` lane publishes NodePort **30122** on kai-server (`services/sirens-echo/deploy/sirens-dowel-values.yaml`, `nodeSelector: kubernetes.io/hostname: kai-server`). The pod binds `SIRENS_ECHO_HTTP_ADDR: 0.0.0.0:8080`, moved off the process default of `127.0.0.1:8080` by the deployment. That one port serves both `/mcp`, which exposes the lane as a single `turn` tool, and `POST /v1/turn`. Both run a full agent turn on the same path Discord uses. The lane runs `tailnet.enabled: false`, so there is no Tailscale sidecar and no MagicDNS name. The NodePort is its only inbound path, and Tailscale ACLs are not in front of it. ## The endpoint authenticates nobody `SIRENS_ECHO_HTTP_TOKEN` reads as an auth control and is not one. * `internal/community/httpidentity.go:22` - `callerTrusted` does a correct constant-time compare of the `X-Sirens-Trust` style header against the configured token. * `internal/community/http.go:227` - the result is assigned, written to the span attribute `http.caller.trusted`, and set on the turn struct. That span attribute is the only consumer in the tree. There is no `401`, no `403`, no early return. So an anonymous caller and a token-bearing caller both get their turn run. The only difference is how the trace is labelled. The SSM description says as much: "records who called rather than refusing anyone." What does gate is shared admission and rate limiting keyed on `X-Sirens-Caller`, plus a `4xx` contract for malformed requests. Neither is an authorization control. ## Why this needs checking rather than assuming `sirens-echo/docs/sirens-echo-http.md` states the boundary as network-only: "the NodePort is LAN and tailnet only, the home router forwards nothing to it." **infrastructure#103 found the opposite for port 22 and closed with "this was b/c of DMZ config."** A DMZ host takes every inbound port, not port 22 alone. If kai-server is still in the router's DMZ, 30122 is reachable from the public internet and the doc's stated boundary does not hold. #103 closed with an explanation rather than a recorded remediation, so whether the DMZ was removed is not answerable from any repo. `ufw` was active on kai-server at the time of #103 with rules unverified. k3s installs its own iptables chains for NodePort traffic, which commonly bypass ufw's INPUT filtering, so an active ufw is not by itself an answer either. ## What to check 1. Whether kai-server is still in the router's DMZ, or has any forward reaching 30122. From the router admin, not from a doc. 2. Whether `ufw`/iptables actually drops 30122 from non-LAN, non-tailnet sources, tested rather than read. 3. `ss -lntp` on kai-server for what else the DMZ answer would expose, since 30120 and 30121 carry the Echo and Deep lanes on the same footing. ## Impact if it is reachable An unauthenticated caller runs turns as Sirens Dowel. That lane holds Discord write grants, so a turn can post into the owl.glass guild. It also spends inference per call, bounded only by the shared admission limiter. Timing note: Dowel is staged for a public Temporal livestream on 2026-08-19, 11:00 to 11:50 PDT. ## Not in scope here Making the token refuse instead of annotate is a `sirens-echo` change against `internal/community/http.go:227` plus a rebuild and rollout. Worth a separate issue there. This one is the network question only. **Filed by Claude. Investigated read-only, nothing changed.**
Author
Owner

Resolved by deprovision. The endpoint no longer exists, so the exposure question is closed rather than answered.

Dowel and owl.glass were fully deprovisioned on 2026-08-25 (coilyco-bridge/deploy#718). NodePort 30122 was that lane's turn endpoint.

kubectl get svc -A            no service holds nodePort 30122
ss -lntp on kai-server        nothing listening on 30122
nc -z kai-server 30122        refused over the tailnet

The allocated NodePorts on the cluster now run 30081-30121 with 30122 unassigned.

Worth being precise about what this does and does not settle

This issue asked whether 30122 was reachable from the public internet, and I have not answered that. I established that nothing is listening on it at all, which makes the question moot for this port, but it is not the same as having tested the public path and found it closed.

So the general concern behind the issue survives: NodePorts on kai-server are reachable on whatever interfaces the host exposes, and "Dowel's turn endpoint refuses nobody" was a statement about the service's own auth, not about the network. If any other NodePort in the 30081-30121 range serves something that authenticates weakly, the same finding applies to it and this closure does not cover it.

infrastructure#157 (public-port allowlist must live in the repo and be audited externally) and #110 (treat LAN as semi-hostile) are the issues that own that general form.

Closing this one, since the specific port is gone.

Resolved by deprovision. The endpoint no longer exists, so the exposure question is closed rather than answered. Dowel and owl.glass were fully deprovisioned on 2026-08-25 (`coilyco-bridge/deploy#718`). NodePort `30122` was that lane's turn endpoint. ``` kubectl get svc -A no service holds nodePort 30122 ss -lntp on kai-server nothing listening on 30122 nc -z kai-server 30122 refused over the tailnet ``` The allocated NodePorts on the cluster now run 30081-30121 with 30122 unassigned. ## Worth being precise about what this does and does not settle This issue asked whether `30122` was reachable **from the public internet**, and I have not answered that. I established that nothing is listening on it at all, which makes the question moot for this port, but it is not the same as having tested the public path and found it closed. So the general concern behind the issue survives: NodePorts on kai-server are reachable on whatever interfaces the host exposes, and "Dowel's turn endpoint refuses nobody" was a statement about the service's own auth, not about the network. If any other NodePort in the 30081-30121 range serves something that authenticates weakly, the same finding applies to it and this closure does not cover it. `infrastructure#157` (public-port allowlist must live in the repo and be audited externally) and `#110` (treat LAN as semi-hostile) are the issues that own that general form. Closing this one, since the specific port is gone.
Sign in to join this conversation.
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
coilyco-flight-deck/infrastructure#866
No description provided.