Summary
_remote() infers operator authority from the absence of a header. This is the entire authorization model for the system, and it is an unstated invariant enforced by nothing.
Evidence
fleet/bin/fleet.py:525-527:
def _remote(self):
"""True when the request arrived through the tailscale funnel."""
return bool(self.headers.get("X-Forwarded-For"))
Any request without X-Forwarded-For is the operator, with access to /ws/terminal (PTY running claude at repo root, terminal.py:110-118), /api/kill, /chat/send, /api/convene.
This is correct today, and the surrounding code shows it was reasoned about carefully — fleet.py:487-502 deliberately rewrites XFF to the last entry when forwarding, which is the fix from #10. It holds because 8787 is loopback-bound (fleet.py:1290) and tailscaled is the only thing in front (tailscale serve status: a single handler, / -> 127.0.0.1:8787).
Impact
Nothing enforces the preconditions. Anything later placed in front that drops or omits XFF — a reverse proxy, a debugging tunnel, a changed bind address — silently grants full local authority to the internet, with no error and no log line. The failure is silent and total.
SECURITY.md names fleet/bin/fleet.py as the trust boundary but describes the XFF sanitisation, not this inversion.
Acceptance criteria
- Startup asserts the listen address is loopback and refuses to start otherwise.
- SECURITY.md documents the tailscaled-in-front dependency as an explicit invariant, not an implementation detail.
- Consider requiring a positive local marker rather than inferring locality from an absent header — reviewers' input welcome on whether that is worth the complexity given the loopback assertion.
Summary
_remote()infers operator authority from the absence of a header. This is the entire authorization model for the system, and it is an unstated invariant enforced by nothing.Evidence
fleet/bin/fleet.py:525-527:Any request without
X-Forwarded-Foris the operator, with access to/ws/terminal(PTY runningclaudeat repo root,terminal.py:110-118),/api/kill,/chat/send,/api/convene.This is correct today, and the surrounding code shows it was reasoned about carefully —
fleet.py:487-502deliberately rewrites XFF to the last entry when forwarding, which is the fix from #10. It holds because 8787 is loopback-bound (fleet.py:1290) and tailscaled is the only thing in front (tailscale serve status: a single handler,/->127.0.0.1:8787).Impact
Nothing enforces the preconditions. Anything later placed in front that drops or omits XFF — a reverse proxy, a debugging tunnel, a changed bind address — silently grants full local authority to the internet, with no error and no log line. The failure is silent and total.
SECURITY.md names
fleet/bin/fleet.pyas the trust boundary but describes the XFF sanitisation, not this inversion.Acceptance criteria