Skip to content

[hardening] _remote() infers authority from an absent header; make the tailscaled dependency an enforced invariant #19

Description

@marsrobertson

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions