Skip to content

Block Docker gateway addresses in egress proxy by default#4395

Merged
tgrunnagle merged 3 commits intomainfrom
sa-m5h8_allow-docker-gateway
Mar 27, 2026
Merged

Block Docker gateway addresses in egress proxy by default#4395
tgrunnagle merged 3 commits intomainfrom
sa-m5h8_allow-docker-gateway

Conversation

@tgrunnagle
Copy link
Copy Markdown
Contributor

Block Docker gateway addresses in egress proxy by default

Summary

  • Containerized MCP servers can reach host services via host.docker.internal, gateway.docker.internal, and the Docker bridge gateway IP, enabling lateral movement from a compromised server to the host. This blocks those addresses in the Squid egress proxy by default, even when insecure_allow_all is set — host access is a separate threat surface from general internet access and requires its own explicit opt-in.
  • Added --allow-docker-gateway CLI flag (default false) that threads through RunFlagsRunConfigDeployWorkloadOptionscreateEgressSquidContainercreateTempEgressSquidConf → Squid ACL config.
  • The Squid config now emits dstdomain and dst deny rules for the gateway addresses before any allow rules (Squid is first-match-wins); when --allow-docker-gateway is passed, the deny block is omitted entirely.
  • The bridge gateway IP is resolved at deploy time via docker network inspect bridge rather than hardcoded, so the correct IP is used across Linux (172.17.0.1), Docker Desktop on macOS (192.168.65.1), and Colima/Rancher Desktop variants.
  • A nil options pointer passed with isolateNetwork=true no longer panics; allowDockerGateway defaults to false in that case.

Type of change

  • New feature

Test plan

  • Unit tests (task test)

Changes

File Change
pkg/container/docker/squid.go writeDockerGatewayDenyRules(sb, gatewayIP) emits deny ACLs; createTempEgressSquidConf and createEgressSquidContainer accept allowDockerGateway bool and gatewayIP string
pkg/container/docker/client.go getDockerBridgeGatewayIP inspects the bridge network at runtime; (*Client).createEgressSquidContainer wrapper resolves and threads the IP; nil-safe allowDockerGateway guard
pkg/container/runtime/types.go AllowDockerGateway bool added to DeployWorkloadOptions
pkg/runtime/setup.go allowDockerGateway bool param; sets containerOptions.AllowDockerGateway
pkg/runner/config.go AllowDockerGateway bool field in RunConfig
pkg/runner/config_builder.go WithAllowDockerGateway() builder option
pkg/runner/runner.go Passes r.Config.AllowDockerGateway to runtime.Setup()
cmd/thv/app/run_flags.go --allow-docker-gateway flag registered and wired into BuildRunnerConfig
pkg/container/docker/squid_test.go Existing tests updated; new table cases: opt-in removes deny rules, restrictive ACL + opt-in keeps ACL allow rules
pkg/container/docker/client_deploy_test.go fakeDeployOps captures allowDockerGateway; new test asserts value is forwarded end-to-end

Does this introduce a user-facing change?

New flag: thv run --allow-docker-gateway. When --isolate-network is active, passing this flag permits the MCP server container to connect to Docker gateway addresses that are otherwise blocked. The flag is off by default; no existing behaviour changes unless the flag is passed.

Special notes for reviewers

  • gateway.docker.internal is Docker Desktop (macOS) specific; blocking it on Linux is harmless because the name does not resolve there.
  • The dst IP rule covers direct-IP access that bypasses DNS. getDockerBridgeGatewayIP falls back to 172.17.0.1 if docker network inspect bridge fails (e.g. custom network setups), so the rule is always emitted.
  • The deployOps interface is unchanged — gateway IP resolution is an implementation detail of *Client and invisible to fakes.
  • --allow-docker-gateway without --isolate-network is silently ignored (egress container is never created without network isolation). A follow-up could add a slog.Warn for this case.

Generated with Claude Code

@github-actions github-actions bot added size/S Small PR: 100-299 lines changed size/M Medium PR: 300-599 lines changed and removed size/S Small PR: 100-299 lines changed labels Mar 26, 2026
## Why

Containerized MCP servers can reach host services via `host.docker.internal`,
`gateway.docker.internal`, and the Docker bridge gateway IP (`172.17.0.1`).
This enables lateral movement from a compromised or malicious MCP server to
services running on the host, bypassing the container network boundary.

The existing `insecure_allow_all` permission flag does not protect against this:
users enabling it intend to open general internet access, not necessarily host
access. These are distinct threat surfaces and warrant separate opt-ins.

## What changed

The Squid egress proxy config now emits ACL deny rules for the three Docker
gateway addresses **before** any allow rules. Squid evaluates access control
in first-match-wins order, so placing the deny first ensures it cannot be
bypassed by a subsequent `http_access allow all`.

A new `--allow-docker-gateway` CLI flag (default `false`) provides an explicit
opt-in for the small number of MCP servers that legitimately need host access.
The flag threads through the full call chain:

```
--allow-docker-gateway (run_flags.go)
  → RunConfig.AllowDockerGateway (config.go)
  → runtime.Setup() (setup.go)
  → DeployWorkloadOptions.AllowDockerGateway (types.go)
  → createEgressSquidContainer() (client.go)
  → createTempEgressSquidConf() (squid.go)
```

Generated Squid config with default settings (blocking active):

```squid
acl docker_gateway_hosts dstdomain host.docker.internal gateway.docker.internal
acl docker_gateway_ip dst 172.17.0.1
http_access deny docker_gateway_hosts
http_access deny docker_gateway_ip

http_access allow all   # (or ACL-based allow rules)
http_access deny all
```
@tgrunnagle tgrunnagle force-pushed the sa-m5h8_allow-docker-gateway branch from 4d8edb3 to ef0e29d Compare March 27, 2026 14:13
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 27, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 38.88889% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.48%. Comparing base (d57d73b) to head (ef0e29d).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pkg/container/docker/client.go 11.11% 16 Missing ⚠️
pkg/runner/config_builder.go 0.00% 4 Missing ⚠️
pkg/container/docker/squid.go 91.66% 1 Missing ⚠️
pkg/runtime/setup.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4395      +/-   ##
==========================================
- Coverage   69.49%   69.48%   -0.02%     
==========================================
  Files         485      485              
  Lines       49841    49875      +34     
==========================================
+ Hits        34638    34654      +16     
- Misses      12528    12543      +15     
- Partials     2675     2678       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tgrunnagle tgrunnagle merged commit 887b39c into main Mar 27, 2026
40 checks passed
@tgrunnagle tgrunnagle deleted the sa-m5h8_allow-docker-gateway branch March 27, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants