fix(install): trust Docker bridge for dashboard metadata

## Summary

Closes #2909.

The `persistent-docker` installer now discovers Docker's default bridge
gateway and passes the exact `/32` gateway CIDR to the proxy's dashboard
metadata allowlist when no explicit
`HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS` value is configured.
This keeps the existing metadata gate intact while allowing the
first-party loopback-published container to see its own Recent Requests
and Per-Project Savings data. Explicit user configuration continues to
take precedence.

Both native wrappers (POSIX and PowerShell) use the same behavior, and
installer integration coverage verifies the generated Docker command.

## Validation

- `python -m pytest tests/test_install/test_native_installers.py -q -k
bash` (1 skipped on Windows because Bash is unavailable)
- PowerShell wrapper smoke test with the repository fake Docker shim:
verified `docker network inspect bridge` is called and
`HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS=172.17.0.1/32` is passed
to `docker run`
- Explicit allowlist smoke test: verified an existing
`HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS` is preserved without
adding a discovered default
- `git diff --check`

## Real behavior proof

Setup tested: Windows 11 host, PowerShell wrapper, repository fake
Docker shim (Docker CLI is not installed in this environment).

Exact command: `headroom.ps1 install apply --profile smoke --port 18999
--image fake/headroom:test`.

Observed result: the generated Docker invocation included `docker
network inspect bridge --format ...` and `--env
HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS=172.17.0.1/32`, and the
installer completed successfully.

Not tested: a live Docker daemon/dashboard request on this host.

---------

Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
This commit is contained in:
Suliman Abdulrazzaq
2026-08-12 00:25:29 +03:00
committed by GitHub
parent c85abf7a87
commit e044139001
3 changed files with 170 additions and 3 deletions
+25 -1
View File
@@ -346,6 +346,29 @@ function Get-PersistentDockerArgs {
return ,$args.ToArray()
}
function Add-DashboardGatewayEnv {
param([System.Collections.Generic.List[string]]$ArgsList)
# This default is safe only because the published dashboard port is bound
# to the host loopback interface below. A host request published through
# Docker's default bridge reaches the
# container from the bridge gateway (for example, 172.17.0.1), not from
# 127.0.0.1. Trust only that exact gateway by default so the dashboard's
# metadata gate works for the first-party persistent Docker preset while
# preserving an explicitly configured allowlist.
if (Test-Path Env:HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS) {
return
}
$gateway = (& docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}' 2>$null | Out-String).Trim()
if ($LASTEXITCODE -eq 0 -and $gateway) {
$ArgsList.Add('--env')
$ArgsList.Add("HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS=$gateway/32")
} else {
Write-Warning 'Could not determine Docker bridge gateway; dashboard metadata remains restricted'
}
}
function Get-ManifestProxyArgs {
param(
[int]$Port,
@@ -492,8 +515,9 @@ function Start-PersistentDockerInstall {
docker rm -f $containerName | Out-Null 2>$null
$dockerArgs = New-Object System.Collections.Generic.List[string]
$dockerArgs.AddRange([string[]]@('run','-d','--restart','unless-stopped','--name',$containerName,'-p',"$Port`:$Port"))
$dockerArgs.AddRange([string[]]@('run','-d','--restart','unless-stopped','--name',$containerName,'-p',"127.0.0.1`:$Port`:$Port"))
$dockerArgs.AddRange((Get-PersistentDockerArgs))
Add-DashboardGatewayEnv -ArgsList $dockerArgs
$dockerArgs.AddRange([string[]]@(
'--env',"HEADROOM_DEPLOYMENT_PROFILE=$Profile",
'--env','HEADROOM_DEPLOYMENT_PRESET=persistent-docker',
+25 -1
View File
@@ -292,6 +292,29 @@ append_persistent_container_args() {
append_passthrough_envs "$1"
}
append_dashboard_gateway_env() {
local -n ref=$1
# This default is safe only because the published dashboard port is bound
# to the host loopback interface below. A host request published through
# Docker's default bridge reaches the
# container from the bridge gateway (for example, 172.17.0.1), not from
# 127.0.0.1. Trust only that exact gateway by default so the dashboard's
# metadata gate works for the first-party persistent Docker preset while
# preserving an explicitly configured allowlist.
if [[ -n "${HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS+x}" ]]; then
return
fi
local gateway
gateway="$(docker network inspect bridge --format '{{(index .IPAM.Config 0).Gateway}}' 2>/dev/null || true)"
if [[ -n "${gateway}" ]]; then
ref+=(--env "HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS=${gateway}/32")
else
warn "Could not determine Docker bridge gateway; dashboard metadata remains restricted"
fi
}
build_manifest_proxy_args() {
local -n out_args=$1
local port="$2"
@@ -466,8 +489,9 @@ start_persistent_docker_install() {
docker rm -f "${container_name}" >/dev/null 2>&1 || true
args=(docker run -d --restart unless-stopped --name "${container_name}" -p "${port}:${port}")
args=(docker run -d --restart unless-stopped --name "${container_name}" -p "127.0.0.1:${port}:${port}")
append_persistent_container_args args
append_dashboard_gateway_env args
args+=(
--env "HEADROOM_DEPLOYMENT_PROFILE=${profile}"
--env "HEADROOM_DEPLOYMENT_PRESET=persistent-docker"
+120 -1
View File
@@ -105,6 +105,16 @@ def main() -> int:
if command == "pull":
return 0
if command == "network" and len(args) > 1 and args[1] == "inspect":
# Match the default bridge gateway used by the native installer when
# it configures the dashboard metadata allowlist.
gateway = os.environ.get("FAKE_DOCKER_GATEWAY", "172.17.0.1")
if gateway == "FAIL":
return 1
if "--format" in args and gateway:
print(gateway)
return 0
if command == "run":
detached = "-d" in args
if not detached:
@@ -112,17 +122,29 @@ def main() -> int:
name = None
publish = None
container_env = {}
for index, arg in enumerate(args):
if arg == "--name":
name = args[index + 1]
elif arg == "-p":
publish = args[index + 1]
elif arg == "--env":
spec = args[index + 1]
if "=" in spec:
env_name, value = spec.split("=", 1)
container_env[env_name] = value
elif spec in os.environ:
container_env[spec] = os.environ[spec]
if name is None or publish is None:
raise SystemExit("missing --name or -p in fake docker run")
port = host_port_from_publish(publish)
state["containers"][name] = {"pid": start_server(port), "port": port}
state["containers"][name] = {
"pid": start_server(port),
"port": port,
"env": container_env,
}
save_state(state)
print(name)
return 0
@@ -225,6 +247,84 @@ def _read_fake_docker_log(env: dict[str, str]) -> list[list[str]]:
return [json.loads(line) for line in log_path.read_text(encoding="utf-8").splitlines() if line]
def _persistent_run_call(env: dict[str, str], profile: str) -> list[str]:
container_name = f"headroom-{profile}"
return next(
call
for call in _read_fake_docker_log(env)
if call[:2] == ["run", "-d"]
and "--name" in call
and call[call.index("--name") + 1] == container_name
)
def _persistent_container_env(env: dict[str, str], profile: str) -> dict[str, str]:
state = json.loads(Path(env["FAKE_DOCKER_STATE"]).read_text(encoding="utf-8"))
return state["containers"][f"headroom-{profile}"]["env"]
def _exercise_dashboard_gateway_overrides(wrapper_command: list[str], env: dict[str, str]) -> None:
trusted_cidrs = "HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS"
try:
for profile, configured_value in (("configured", "10.20.0.0/16"), ("empty", "")):
env[trusted_cidrs] = configured_value
port = _free_port()
_run(
[
*wrapper_command,
"install",
"apply",
"--profile",
profile,
"--port",
str(port),
"--image",
"fake/headroom:test",
],
env=env,
)
install_call = _persistent_run_call(env, profile)
assert install_call[install_call.index("-p") + 1] == f"127.0.0.1:{port}:{port}"
# Docker's name-only --env form preserves the caller's value,
# including an explicitly empty value, instead of installing the
# discovered bridge gateway default.
assert trusted_cidrs in install_call
assert not any(arg.startswith(f"{trusted_cidrs}=") for arg in install_call)
assert _persistent_container_env(env, profile)[trusted_cidrs] == configured_value
_run([*wrapper_command, "install", "remove", "--profile", profile], env=env)
env.pop(trusted_cidrs, None)
env["FAKE_DOCKER_GATEWAY"] = "FAIL"
port = _free_port()
result = _run(
[
*wrapper_command,
"install",
"apply",
"--profile",
"no-gateway",
"--port",
str(port),
"--image",
"fake/headroom:test",
],
env=env,
)
install_call = _persistent_run_call(env, "no-gateway")
assert install_call[install_call.index("-p") + 1] == f"127.0.0.1:{port}:{port}"
assert not any(
arg == trusted_cidrs or arg.startswith(f"{trusted_cidrs}=") for arg in install_call
)
assert trusted_cidrs not in _persistent_container_env(env, "no-gateway")
assert "dashboard metadata remains restricted" in (result.stdout + result.stderr)
_run([*wrapper_command, "install", "remove", "--profile", "no-gateway"], env=env)
finally:
env.pop(trusted_cidrs, None)
env.pop("FAKE_DOCKER_GATEWAY", None)
def _run(
command: list[str],
*,
@@ -402,11 +502,19 @@ def test_bash_native_installer_supports_persistent_docker_lifecycle(tmp_path: Pa
install_call = next(
call for call in docker_calls if call[:2] == ["run", "-d"] and "--name" in call
)
assert install_call[install_call.index("-p") + 1] == f"127.0.0.1:{port}:{port}"
assert "/tmp/headroom-home/.headroom/memory.db" in install_call
# Canonical filesystem contract env vars (issue #175) forwarded into
# the container so the proxy resolves state/config to the bind mount.
assert "HEADROOM_WORKSPACE_DIR=/tmp/headroom-home/.headroom" in install_call
assert "HEADROOM_CONFIG_DIR=/tmp/headroom-home/.headroom/config" in install_call
assert "HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS=172.17.0.1/32" in install_call
assert (
_persistent_container_env(env, "smoke")["HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS"]
== "172.17.0.1/32"
)
_exercise_dashboard_gateway_overrides([str(wrapper)], env)
status_result = _run(
[str(wrapper), "install", "status", "--profile", "smoke"],
@@ -654,10 +762,21 @@ def test_powershell_native_installer_supports_persistent_docker_lifecycle(tmp_pa
install_call = next(
call for call in docker_calls if call[:2] == ["run", "-d"] and "--name" in call
)
assert install_call[install_call.index("-p") + 1] == f"127.0.0.1:{port}:{port}"
assert "/tmp/headroom-home/.headroom/memory.db" in install_call
# Canonical filesystem contract env vars (issue #175).
assert "HEADROOM_WORKSPACE_DIR=/tmp/headroom-home/.headroom" in install_call
assert "HEADROOM_CONFIG_DIR=/tmp/headroom-home/.headroom/config" in install_call
assert "HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS=172.17.0.1/32" in install_call
assert (
_persistent_container_env(env, "smoke")["HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS"]
== "172.17.0.1/32"
)
_exercise_dashboard_gateway_overrides(
[powershell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(wrapper)],
env,
)
status_result = _run(
[