feat(mcp): replace bind-host guard with auth-posture interlock#396
Merged
Conversation
The transport-flag summary said local runs "over loopback HTTP", baking loopback into local-over-HTTP as if inherent. The bind-safety interlock's escape hatch can lift it, and that section already owns the nuance, so point there instead.
The bind-safety interlock rationale was stated in three places: the run_server docstring, an inline comment in the HTTP serving branch, and the McpSettings._enforce_bind_safety docstring/AGENTS.md. Keep it once, at the boundary that owns the invariant. run_server no longer narrates a settings-boundary invariant; it carries only a short regression guard at the .run site (do not re-check host/port here). The coverage claim (every serving path inherits the guarantee) now lives on _enforce_bind_safety, and AGENTS.md points there instead of restating it.
gbrlcustodio
marked this pull request as ready for review
July 13, 2026 16:15
gbrlcustodio
force-pushed
the
feat/379-auth-posture-bind-interlock
branch
from
July 13, 2026 16:27
9c81845 to
7bf41c9
Compare
gbrlcustodio
force-pushed
the
feat/379-auth-posture-bind-interlock
branch
from
July 13, 2026 16:32
7bf41c9 to
9c81845
Compare
adriannoes
approved these changes
Jul 13, 2026
adriannoes
left a comment
Collaborator
There was a problem hiding this comment.
Summary
Looks good to merge.
- Local checkout+tests on
9c818451(ruff, targeted 145, full infra+mcp 1607, lint-imports kept) and CI lint/test are green. - The auth-posture interlock at
McpSettings._enforce_bind_safetymatches #379: local+http refuses non-loopback without the hatch, remote can bind off-loopback, and the old run-path_assert_safe_http_bindis gone.
What worked well
- Moving the control to the settings boundary closes the ASGI/
streamable_http_appbypass of the oldrun_server-only guard. - Keying on auth posture (
localvsremote) fixes the false positives on container0.0.0.0and authenticated hosted binds. is_loopback_hostviaipaddress.is_loopback, with clear escape-hatch docs and tests.
Also noted
- Merge-order heads-up with the open #373 stack (PR #376 /
feat/373-request-log-middleware):server.pyconflicts where this PR removes_assert_safe_http_bindand that branch inserts_serve_streamable_http, and both rewrite the tail ofrun_server. The new testtest_run_server_remote_serves_off_loopback_without_a_bind_guardassertsfake_app.run("streamable-http"), which that serve path replaces withanyio.run(_serve_streamable_http, ...), so whoever lands second needs to retarget that assertion. The settings-boundary interlock still covers that custom uvicorn path, which is exactly the coverage argument here.
adriannoes
added a commit
that referenced
this pull request
Jul 14, 2026
Keep stack-2 HTTP serve via _serve_streamable_http (uvicorn + request log middleware); drop _assert_safe_http_bind in favor of McpSettings._enforce_bind_safety from #396.
adriannoes
added a commit
that referenced
this pull request
Jul 14, 2026
This was referenced Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #379.
What
Replaces the bind-interface guard
_assert_safe_http_bindwith an auth-posture interlock, realizing the safety interlock scoped in #300 and retiring the legacy guard it was meant to supersede.Why the old guard was wrong
The old guard refused any non-loopback HTTP bind. It checked the wrong axis. The property worth protecting is "an unauthenticated profile must not be served to untrusted callers," but bind interface is a poor proxy:
0.0.0.0and is still private (network namespace, no published port), so the guard false-positived on the entire hosted profile.remoteprofile is safe there because it has inbound auth (resource-server validation, per-request bearer). Every firing againstremotewas a false positive.It also lived in
run_server, not the app factory, so it protected nothing for callers that build the ASGI app directly viastreamable_http_app(). That bypass was the intended off-loopback path today, which trains callers to route around the control and has already led a downstream repo to vendor a guard-less copy.The interlock
Keyed on auth posture, not bind host, and expressed as a cross-field validator at the settings boundary (
McpSettings._enforce_bind_safety), so no serving path can route around it:build_pipefy_mcp_serverand any direct-ASGI-app caller both take a resolvedSettings.profile == "local"): refuses a non-loopback HTTP bind unless the escape hatchPIPEFY_MCP_ALLOW_INSECURE_HTTP_BINDis set. This is the real footgun (the full tool surface, no inbound bearer, reachable off the local machine).profile == "remote"): bind host is irrelevant and is not checked. The operator binds0.0.0.0legitimately.remotealways carries inbound auth or fails fast inMcpRuntime.for_profile, so keying onprofile == "local"is a faithful proxy for posture at the settings layer.Loopback detection moves to a new
pipefy_infra.security.is_loopback_host, a generic security primitive that usesipaddress(...).is_loopbackto cover all of127.0.0.0/8and::1, not the three string literals the old_LOOPBACK_HOSTSfrozenset hardcoded.Acceptance criteria
_assert_safe_http_bind(the bind-interface check) is removed.Testing
uv run python -m pytest packages/infra/tests/test_security.py packages/mcp/tests/test_settings.py packages/mcp/tests/test_server.py packages/mcp/tests/test_main.py: 145 passed.packages/infra/tests packages/mcp/tests: 1607 passed, 5 skipped.cd packages/mcp && uv run lint-imports: contract kept.uv run ruff check packages/infra packages/mcp: clean.Not in scope
profile == "remote"/"local"at several sites (settings, server, runtime); a single named posture property onMcpSettingswould consolidate it. Deferred to keep this change focused.