Terminal sidecar pins ttyd to loopback in a deployed Container App (2026-06-07)¶
Motivation¶
Charter §9 and the AGENTS.md tripwire require: "ttyd in the terminal
sidecar binds to 127.0.0.1 only … the terminal must never be reachable
directly from the internet/VNet." The exec_server enforces this with a
hard-fail guard (terminal/exec_server.py): in a deployed Container Apps
revision (CONTAINER_APP_NAME is always set by the platform) it refuses any
non-loopback EXEC_HOST and raises at startup.
terminal/entrypoint.sh had no equivalent guard for ttyd — it only
defaulted TTYD_HOST to 127.0.0.1 (TTYD_HOST="${TTYD_HOST:-127.0.0.1}").
An operator (or a bad env injection) setting TTYD_HOST=0.0.0.0 would silently
start the interactive writable shell (ttyd -W) bound to all interfaces,
exposing it to anything that can reach the pod IP — the entire Container Apps
Environment VNet. ttyd is strictly more dangerous than the exec_server (full
interactive PTY vs. an argv-allowlisted exec channel), yet it was the one
without the guard.
User-facing change¶
None for correct deployments (the default and the Bicep template both use
loopback). A misconfigured TTYD_HOST in a deployed revision now fails fast at
sidecar startup with a clear message instead of silently exposing the shell.
API / IaC diff summary¶
terminal/entrypoint.sh— after resolvingTTYD_HOST, whenCONTAINER_APP_NAMEis set, acaseguard allows only127.0.0.1/localhost/::1and otherwise prints a refusal andexit 1beforettydstarts. Mirrors theexec_server.pyguard exactly.- No IaC change (the deployed template already passes loopback; this closes the override hole).
Validation evidence¶
uv run pytest -q api/tests/test_terminal_toolchain.py -m subprocess— 4 passed, including the newtest_entrypoint_pins_ttyd_to_loopback_in_a_container_app(deployed +0.0.0.0→ exit 1; deployed +127.0.0.1→ starts).bash -n terminal/entrypoint.shclean; manual run confirmedCONTAINER_APP_NAME=ca-elb TTYD_HOST=0.0.0.0is refused.uv run ruff check api/tests/test_terminal_toolchain.py— clean.