diff --git a/README.md b/README.md index 65b070f..406629a 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,18 @@ This is the important part — a web page that can kill processes needs guardrai actions. That is deliberate (owner-only tailnet + token-gated actions), but do not read "loopback-only" as "not network-reachable" — a proxy in front is not a trust boundary. +- **Fronting it with a proxy needs `AUM_TRUSTED_HOSTS`.** The DNS-rebinding guard + refuses any request whose `Host` is a non-local DNS name — which is exactly what + a proxy forwards. Opt that one name in explicitly: + + ```bash + AUM_TRUSTED_HOSTS=box.your-tailnet.ts.net agent-usage-manager + ``` + + Comma-separated for several. Only add a name nobody else can mint a cert for on + this node; a name you don't control reopens the hole the guard exists to close. + Empty by default — out of the box only `localhost`, `127.0.0.1` and `::1` are + accepted. ## Limits & known issues diff --git a/agent_usage_manager/app.py b/agent_usage_manager/app.py index 8078812..261e56e 100644 --- a/agent_usage_manager/app.py +++ b/agent_usage_manager/app.py @@ -691,11 +691,18 @@ async def _lifespan(_: FastAPI): app = FastAPI(title="agent-usage-manager", lifespan=_lifespan) -# Loopback names are always local. The ts.net FQDN is the tailnet front door: -# `tailscale serve --https=8448` proxies to this app and preserves the Host -# header. Only tailscaled can mint that name/cert for this node, so allowing -# it does not reopen the DNS-rebinding hole _host_allowed exists to close. -_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", "minglongs-mac-mini.tailab5be0.ts.net"} +# Loopback names are always local. Anything else must be opted in per install via +# AUM_TRUSTED_HOSTS (comma-separated hostnames) — typically the FQDN of a reverse +# proxy or tailnet front door that preserves the Host header, e.g. +# `tailscale serve --https=8448` fronting this app. Only add a name that nobody +# else can mint a cert for on this node; any other name reopens the DNS-rebinding +# hole _host_allowed exists to close. +def _configured_trusted_hosts() -> set[str]: + raw = os.environ.get("AUM_TRUSTED_HOSTS", "") + return {h.strip().lower() for h in raw.split(",") if h.strip()} + + +_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1"} | _configured_trusted_hosts() def _hostname_of(hostport: str) -> str: