Skip to content

Commit 7445ec7

Browse files
alexisdps-ddclaude
andauthored
fix: bind to loopback by default instead of all interfaces (#391)
* fix: bind to loopback by default instead of all interfaces The test agent created its HTTP, OTLP HTTP, OTLP gRPC, and Web UI listeners with no host argument, so aiohttp/grpc bound to all interfaces (0.0.0.0 and [::]). Under `lapdog` those servers carry captured Claude Code / Codex / Pi session content (prompts, responses, traces, and the Web UI that renders them), so anyone on the same network could read local AI session data. The plain test agent was exposed too. Make the bind host configurable via --host (env HOST) and default it to 127.0.0.1 (loopback) so the agent is safe out of the box. Exposure is now explicit opt-in: the Docker images set HOST=0.0.0.0 so their published ports keep working, and anyone running over the network passes --host 0.0.0.0. The gRPC server preserves its dual-stack [::] bind when 0.0.0.0 is requested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * add release note for loopback bind default Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 90ddd61 commit 7445ec7

5 files changed

Lines changed: 65 additions & 7 deletions

File tree

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ FROM python:3.13-slim
33
WORKDIR /src
44
EXPOSE 8126
55

6+
# The agent binds to loopback by default. In a container the published port
7+
# (EXPOSE 8126 / -p 8126:8126) is only reachable if the agent binds all
8+
# interfaces, so opt in explicitly here.
9+
ENV HOST=0.0.0.0
610
ENV SNAPSHOT_CI=1
711
ENV LOG_LEVEL=INFO
812
ENV SNAPSHOT_DIR=/snapshots

Dockerfile.windows

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ FROM python:3.13-windowsservercore
44
WORKDIR /src
55
EXPOSE 8126
66

7+
# The agent binds to loopback by default. In a container the published port
8+
# (EXPOSE 8126 / -p 8126:8126) is only reachable if the agent binds all
9+
# interfaces, so opt in explicitly here.
10+
ENV HOST=0.0.0.0
711
ENV SNAPSHOT_CI=1
812
ENV LOG_LEVEL=INFO
913
ENV SNAPSHOT_DIR=C:\snapshots

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ The test agent can be installed from PyPI:
2424
# HTTP on port 8126, OTLP HTTP on port 4318, OTLP GRPC on port 4317, with the web-ui enabled
2525
ddapm-test-agent --port=8126 --otlp-http-port=4318 --otlp-grpc-port=4317 --web-ui-port=8080
2626

27+
The agent binds to `127.0.0.1` (loopback) by default. To accept connections from
28+
other hosts, pass `--host 0.0.0.0` or set `HOST=0.0.0.0`. The Docker image sets
29+
this for you so its published ports work.
30+
2731
or from Docker:
2832

2933
# Run the test agent and mount the snapshot directory

ddapm_test_agent/agent.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,7 +1994,9 @@ async def otlp_store_request_middleware(request: Request, handler: _Handler) ->
19941994
return app
19951995

19961996

1997-
async def make_otlp_grpc_server_async(agent: Agent, http_port: int, grpc_port: int) -> Any:
1997+
async def make_otlp_grpc_server_async(
1998+
agent: Agent, http_port: int, grpc_port: int, host: str = "127.0.0.1"
1999+
) -> Any:
19982000
"""Create and start a separate GRPC server for OTLP endpoints that forwards to HTTP server."""
19992001
# Define the servicer class only when GRPC is available
20002002
server = grpc_aio.server()
@@ -2011,8 +2013,15 @@ async def make_otlp_grpc_server_async(agent: Agent, http_port: int, grpc_port: i
20112013
traces_servicer = OTLPTracesGRPCServicer(http_port)
20122014
add_TraceServiceServicer_to_server(traces_servicer, server)
20132015

2014-
# Setup and start the server
2015-
listen_addr = f"[::]:{grpc_port}"
2016+
# Setup and start the server. Preserve the historical dual-stack
2017+
# all-interfaces bind for the default; otherwise bind the requested host,
2018+
# wrapping IPv6 literals in brackets.
2019+
if host == "0.0.0.0":
2020+
listen_addr = f"[::]:{grpc_port}"
2021+
elif ":" in host:
2022+
listen_addr = f"[{host}]:{grpc_port}"
2023+
else:
2024+
listen_addr = f"{host}:{grpc_port}"
20162025
server.add_insecure_port(listen_addr)
20172026
await server.start()
20182027

@@ -2351,6 +2360,16 @@ def main(args: Optional[List[str]] = None) -> None:
23512360
help="Print version info and exit.",
23522361
)
23532362
parser.add_argument("-p", "--port", type=int, default=int(os.environ.get("PORT", 8126)))
2363+
parser.add_argument(
2364+
"--host",
2365+
type=str,
2366+
default=os.environ.get("HOST"),
2367+
help=(
2368+
"Host/interface to bind all servers to. Defaults to 127.0.0.1 "
2369+
"(loopback only). Set to 0.0.0.0 to accept connections from other "
2370+
"hosts, e.g. when running in a container with a published port."
2371+
),
2372+
)
23542373
parser.add_argument(
23552374
"--otlp-http-port",
23562375
type=int,
@@ -2543,6 +2562,13 @@ def main(args: Optional[List[str]] = None) -> None:
25432562
parsed_args = parser.parse_args(args=args)
25442563
logging.basicConfig(level=parsed_args.log_level)
25452564

2565+
# Bind to loopback by default so the agent is not exposed on the network.
2566+
# Deployments that must accept off-host traffic (the Docker image, CI)
2567+
# opt in explicitly via --host/HOST (the published Dockerfile sets
2568+
# HOST=0.0.0.0).
2569+
if parsed_args.host is None:
2570+
parsed_args.host = "127.0.0.1"
2571+
25462572
if parsed_args.version:
25472573
print(_get_version())
25482574
sys.exit(0)
@@ -2663,21 +2689,21 @@ async def run_servers():
26632689

26642690
# Start GRPC server if available (async creation)
26652691
otlp_grpc_server = await make_otlp_grpc_server_async(
2666-
agent, parsed_args.otlp_http_port, parsed_args.otlp_grpc_port
2692+
agent, parsed_args.otlp_http_port, parsed_args.otlp_grpc_port, host=parsed_args.host
26672693
)
26682694

26692695
# Create sites for both apps
26702696
if apm_sock:
26712697
apm_site = web.SockSite(apm_runner, apm_sock)
26722698
else:
2673-
apm_site = web.TCPSite(apm_runner, port=parsed_args.port)
2699+
apm_site = web.TCPSite(apm_runner, host=parsed_args.host, port=parsed_args.port)
26742700

2675-
otlp_http_site = web.TCPSite(otlp_http_runner, port=parsed_args.otlp_http_port)
2701+
otlp_http_site = web.TCPSite(otlp_http_runner, host=parsed_args.host, port=parsed_args.otlp_http_port)
26762702

26772703
# Create Web UI site if enabled
26782704
web_ui_site = None
26792705
if web_ui_runner is not None:
2680-
web_ui_site = web.TCPSite(web_ui_runner, port=parsed_args.web_ui_port)
2706+
web_ui_site = web.TCPSite(web_ui_runner, host=parsed_args.host, port=parsed_args.web_ui_port)
26812707

26822708
# Start servers concurrently
26832709
sites_to_start = [apm_site.start(), otlp_http_site.start()]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
security:
3+
- |
4+
The test agent now binds to ``127.0.0.1`` (loopback) by default instead of
5+
all network interfaces. Previously the HTTP, OTLP HTTP, OTLP gRPC, and Web
6+
UI servers were reachable from any host on the network, which could expose
7+
captured data (including ``lapdog`` Claude Code / Codex / Pi session
8+
content) to other machines on the same network.
9+
features:
10+
- |
11+
Added a ``--host`` command-line argument (and ``HOST`` environment
12+
variable) to control the interface all servers bind to. Defaults to
13+
``127.0.0.1``.
14+
upgrade:
15+
- |
16+
The default bind host is now ``127.0.0.1`` instead of all interfaces. To
17+
accept connections from other hosts (for example when running outside of a
18+
container and pointing a remote tracer at the agent), pass ``--host
19+
0.0.0.0`` or set ``HOST=0.0.0.0``. The published Docker images set
20+
``HOST=0.0.0.0`` automatically, so their published ports keep working.

0 commit comments

Comments
 (0)