Skip to content

Let the configured integration proxy live on a private network - #1731

Open
michielbdejong wants to merge 2 commits into
claude/atomic-proxy-schemefrom
claude/proxy-origin-private
Open

michielbdejong wants to merge 2 commits into
claude/atomic-proxy-schemefrom
claude/proxy-origin-private

Conversation

@michielbdejong

Copy link
Copy Markdown
Contributor

Refs #1700. Stacked on #1725.

A: a proxy origin on a private network

Before this change, ProxyOrigin::parse exempted only a literal loopback address or localhost. So --integration-proxy-url http://host.docker.internal:8787 was refused: Docker Desktop resolves it to 192.168.65.x and Linux to 172.17.0.1. A LAN proxy such as http://proxy.lan:8787 → 192.168.1.10 was refused too.

New rule (egress::refuse_proxy_address / destination_addresses_with):

  • Only for a URL whose origin (scheme, host and port) equals the configured proxy's, the addresses may be loopback, private (RFC 1918), CGNAT (100.64/10), IPv6 ULA (fc00::/7), or IPv4-mapped loopback or private.
  • Link-local (169.254.0.0/16, fe80::/10, mapped 169.254.x), unspecified and multicast addresses are still refused, even for the proxy. A literal address of that kind is refused when --integration-proxy-url is parsed at startup.
  • A literal address or localhost is connected to without a lookup. Any other name is resolved once per request, every answer must pass, and exactly those addresses are pinned into the reqwest client with resolve_to_addrs. There is no second lookup.
  • Credentials in the URL are still refused.
  • Every other destination keeps the full checked_addresses checks.
  • A minimal Resolve trait (with SystemResolver) is the test seam.

The docs for --integration-proxy-url are updated in config.rs and docs/src/plugins/creating-plugins.md.

B: end-to-end ctx.http → proxy

cargo test -p atomic-server --test it plugin_proxy runs the full path over real sockets:

  1. Starts a stub proxy on loopback that records requests.
  2. Starts a real server with --integration-proxy-url pointing at the stub.
  3. Over HTTP: pins a JS draft as a release (/plugin-release-pin), then commits an active Installation with integrationAppAgent and integrationConnections: {demo: "conn-1"}. Activation mints the node agent, which the test reads back from GET /app-agent.
  4. Runs the plugin through POST /plugin-run. The plugin calls ctx.http("atomic-proxy:/demo/items").

The test asserts that:

  • the stub got GET /proxy/conn-1/demo/items;
  • the request carries a v2 signature from the node agent, which check_auth_signature verifies and which a different method does not;
  • the plugin got the stub's body and its ctx.connections;
  • atomic-proxy:/other/..., an undeclared platform, is refused before anything connects, so the stub sees exactly one request.

Tests

  • cargo test -p atomic-server --lib plugins::: 176 passed, 3 ignored.
  • cargo test -p atomic-server --test it plugin_proxy: passes.
  • cargo clippy -p atomic-server --all-targets -- -D warnings: clean.

🤖 Generated with Claude Code

Michiel de Jong and others added 2 commits September 24, 2026 17:41
`ProxyOrigin` only exempted a literal loopback address or `localhost`, so
`--integration-proxy-url http://host.docker.internal:8787` (192.168.65.x on
Docker Desktop, 172.17.0.1 on Linux) or a LAN proxy was refused. Exactly the
configured scheme, host and port may now resolve to loopback, private, CGNAT
or ULA addresses. The name is resolved once per request and those checked
addresses are pinned into the client. Link-local and metadata addresses stay
refused even for the proxy (and a literal one is refused at startup), and so
do credentials in the URL. Every other destination keeps all the checks.

Adds a `Resolve` seam so tests can make a name resolve to a private
address, and an `it plugin_proxy` test that runs a JS plugin through
`POST /plugin-run` on a real server and checks that `ctx.http` reaches a stub
proxy as `GET /proxy/conn-1/demo/items`, v2-signed by the installation's node
agent, and that an undeclared platform is refused.

Refs #1700

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The private-network exception for the configured integration proxy let
through two instance-metadata endpoints that sit inside ranges it allows:
Alibaba Cloud's 100.100.100.200 (carrier-grade NAT) and AWS's IPv6 IMDS
at fd00:ec2::254 (unique-local). Name them and refuse them before the
exception applies, including the IPv4-mapped form, both as a literal at
startup and as a resolved answer per request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michielbdejong

Copy link
Copy Markdown
Contributor Author

Merge-risk triage (2026-09-28). This is a read-only review of this PR's own diff. The rule it applies: nothing that works today may break for existing users or clients, and new behaviour is added alongside the old. The stack is being rebased onto develop as one chain before merging, so line references are against the current stacked base.

Verdict: SAFE

Loosens the SSRF guard to private/CGNAT/ULA addresses, but only for the operator-configured proxy origin. Link-local and metadata addresses stay refused, the name is resolved once, and the checked addresses are pinned. Nothing changes when the option is unset.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant