The hole (last of three — #142 shelly-manager, #144 matter-server)
otbr's net1 is a macvlan on the IoT VLAN (the Thread backbone interface), macvlan bypasses Cilium policy, and REST_HOST=0.0.0.0 bound otbr-agent's REST API on :8081 to all interfaces. So the unauthenticated OTBR REST API was reachable from the whole IoT VLAN.
Verified today from edge-1 (on v30iot):
$ curl http://<net1 ip>:8081/
404 ← otbr-agent REST answering (404 at /, but it's the live listener)
That API reads and modifies Thread network config including the network key — enough to join or disrupt the mesh. Worst of the three exposures in capability.
Reality vs. the old wiki note:otbr-agent REST listens on :8081 (governed by REST_HOST); otbr-web is on 127.0.0.1:80 (loopback, not exposed); the container's declared rest:8080 port has no listener. REST_HOST is the whole exposure.
The fix
- name:POD_IP # declared BEFORE REST_HOST —valueFrom:{fieldRef:{fieldPath:status.podIP } }- name:REST_HOST # k8s $(VAR) only sees earlier entriesvalue:"$(POD_IP)"
REST binds the pod eth0 address; net1 no longer carries it. HA's OTBR integration keeps working via the IPv4 ClusterIP Service (otbr.home-assistant.svc:8081 → podIP:8081).
Why the border router is unaffected
REST_HOST (--rest-listen-address) governs only the REST control API. The Thread border router uses net1 through BACKBONE_INTERFACE for mDNS/RA/backbone — a separate, unchanged mechanism. Baseline before the change: state=leader, br=running, mesh 2 routers / 8 children / 8 neighbors.
Expect ~60s of self-healing CASE-session churn on Thread/Matter end devices as the mesh re-forms (per the #141 verification). The dataset-restore poll (#139), CRLF fix (#140), and protocol-aware probes (#141) mean the pod should return to leader/running on its own and only report Ready once Thread is up. I'll confirm reconvergence before calling it done.
Post-merge checks
curl http://<net1 ip>:8081/ from edge-1 now fails.
REST listens on the pod IP only.
HA reaches otbr:8081 via ClusterIP.
otbr returns to leader / br running; mesh reconverges.
matter-server's Thread nodes re-subscribe (they'll churn during the mesh re-form, same as any otbr restart).
## The hole (last of three — #142 shelly-manager, #144 matter-server)
otbr's `net1` is a macvlan on the **IoT VLAN** (the Thread backbone interface), macvlan bypasses Cilium policy, and `REST_HOST=0.0.0.0` bound `otbr-agent`'s REST API on `:8081` to all interfaces. So the unauthenticated OTBR REST API was reachable from the whole IoT VLAN.
Verified today from `edge-1` (on v30iot):
```
$ curl http://<net1 ip>:8081/
404 ← otbr-agent REST answering (404 at /, but it's the live listener)
```
That API reads and modifies Thread network config **including the network key** — enough to join or disrupt the mesh. Worst of the three exposures in capability.
**Reality vs. the old wiki note:** `otbr-agent` REST listens on `:8081` (governed by `REST_HOST`); `otbr-web` is on `127.0.0.1:80` (loopback, not exposed); the container's declared `rest:8080` port has no listener. `REST_HOST` is the whole exposure.
## The fix
```yaml
- name: POD_IP # declared BEFORE REST_HOST —
valueFrom: { fieldRef: { fieldPath: status.podIP } }
- name: REST_HOST # k8s $(VAR) only sees earlier entries
value: "$(POD_IP)"
```
REST binds the pod eth0 address; `net1` no longer carries it. HA's OTBR integration keeps working via the IPv4 ClusterIP Service (`otbr.home-assistant.svc:8081` → podIP:8081).
## Why the border router is unaffected
`REST_HOST` (`--rest-listen-address`) governs only the REST control API. The Thread border router uses `net1` through `BACKBONE_INTERFACE` for mDNS/RA/backbone — a separate, unchanged mechanism. Baseline before the change: `state=leader`, `br=running`, mesh 2 routers / 8 children / 8 neighbors.
## Verified so far
`kubectl kustomize` builds; rendered env has `POD_IP` (downward API) ordered **before** `REST_HOST=$(POD_IP)`; `BACKBONE_INTERFACE=net1` unchanged.
## ⚠️ This restarts the Thread border router
Expect **~60s of self-healing CASE-session churn** on Thread/Matter end devices as the mesh re-forms (per the #141 verification). The dataset-restore poll (#139), CRLF fix (#140), and protocol-aware probes (#141) mean the pod should return to `leader`/`running` on its own and only report `Ready` once Thread is up. I'll confirm reconvergence before calling it done.
## Post-merge checks
1. `curl http://<net1 ip>:8081/` from `edge-1` now **fails**.
2. REST listens on the pod IP only.
3. HA reaches `otbr:8081` via ClusterIP.
4. otbr returns to `leader` / `br running`; mesh reconverges.
5. matter-server's Thread nodes re-subscribe (they'll churn during the mesh re-form, same as any otbr restart).
Last of the three IoT-VLAN gate/bind bypasses (shelly-manager #142,
matter-server #144). otbr's net1 is a macvlan on the IoT VLAN (backbone
interface), macvlan bypasses Cilium policy, and REST_HOST=0.0.0.0 bound
otbr-agent's REST API on :8081 to all interfaces — so the unauthenticated
OTBR REST API was reachable from the whole IoT VLAN.
Verified 2026-07-10 from edge-1 (on v30iot): GET http://<net1 ip>:8081/
answered (404 at /, but it is the live otbr-agent REST listener). That API
can read and modify Thread network configuration, including the network
key — i.e. enough to join or disrupt the Thread mesh.
Note the reality vs. the old comment: otbr-agent's REST listens on :8081
(governed by REST_HOST / --rest-listen-address); otbr-web sits on
127.0.0.1:80 (loopback, not exposed); the container's declared rest:8080
port has no listener. So REST_HOST is the whole exposure.
Set REST_HOST to the pod eth0 address via the downward API
(POD_IP=status.podIP; REST_HOST=$(POD_IP)). POD_IP is declared BEFORE
REST_HOST because k8s $(VAR) substitution only sees earlier env entries.
HA's OTBR integration keeps working via the IPv4 ClusterIP Service
(otbr.home-assistant.svc:8081 -> podIP:8081). net1 no longer carries REST.
The border router itself is unaffected: it uses net1 through
BACKBONE_INTERFACE for mDNS/RA/backbone, independent of where REST listens.
Verified: kubectl kustomize builds; rendered env has POD_IP (downward API)
before REST_HOST=$(POD_IP); BACKBONE_INTERFACE=net1 unchanged. Baseline
before change: state=leader, br=running, mesh 2 routers/8 children/8
neighbors.
CAVEAT: this restarts the Thread border router. Expect ~60s of
self-healing CASE-session churn on Thread/Matter end devices as the mesh
re-forms (per the #141 verification). The dataset-restore race and the
protocol-aware probes from #139/#140/#141 mean the pod should come back to
leader/running on its own and only report Ready once Thread is up.
Post-merge checks to run: GET net1:8081 from the IoT VLAN now fails; REST
listens on the pod IP only; HA reaches otbr:8081 via ClusterIP; otbr
returns to leader/br running and the mesh reconverges; matter-server's
Thread nodes re-subscribe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The hole (last of three — #142 shelly-manager, #144 matter-server)
otbr's
net1is a macvlan on the IoT VLAN (the Thread backbone interface), macvlan bypasses Cilium policy, andREST_HOST=0.0.0.0boundotbr-agent's REST API on:8081to all interfaces. So the unauthenticated OTBR REST API was reachable from the whole IoT VLAN.Verified today from
edge-1(on v30iot):That API reads and modifies Thread network config including the network key — enough to join or disrupt the mesh. Worst of the three exposures in capability.
Reality vs. the old wiki note:
otbr-agentREST listens on:8081(governed byREST_HOST);otbr-webis on127.0.0.1:80(loopback, not exposed); the container's declaredrest:8080port has no listener.REST_HOSTis the whole exposure.The fix
REST binds the pod eth0 address;
net1no longer carries it. HA's OTBR integration keeps working via the IPv4 ClusterIP Service (otbr.home-assistant.svc:8081→ podIP:8081).Why the border router is unaffected
REST_HOST(--rest-listen-address) governs only the REST control API. The Thread border router usesnet1throughBACKBONE_INTERFACEfor mDNS/RA/backbone — a separate, unchanged mechanism. Baseline before the change:state=leader,br=running, mesh 2 routers / 8 children / 8 neighbors.Verified so far
kubectl kustomizebuilds; rendered env hasPOD_IP(downward API) ordered beforeREST_HOST=$(POD_IP);BACKBONE_INTERFACE=net1unchanged.⚠️ This restarts the Thread border router
Expect ~60s of self-healing CASE-session churn on Thread/Matter end devices as the mesh re-forms (per the #141 verification). The dataset-restore poll (#139), CRLF fix (#140), and protocol-aware probes (#141) mean the pod should return to
leader/runningon its own and only reportReadyonce Thread is up. I'll confirm reconvergence before calling it done.Post-merge checks
curl http://<net1 ip>:8081/fromedge-1now fails.otbr:8081via ClusterIP.leader/br running; mesh reconverges.