Skip to content

fix(egress): plain HTTP reaches a listed host on port 80 only, and doctor proves it (#508) - #514

Merged
edgehero merged 1 commit into
mainfrom
fix/508-plain-http-port-80
Oct 2, 2026
Merged

edgehero merged 1 commit into
mainfrom
fix/508-plain-http-port-80

Conversation

@edgehero

@edgehero edgehero commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #508.

What changes

The egress proxy let a plain (untunnelled) request reach an allowlisted host on any port. http_access allow allowed had no port rule. Now exactly two shapes pass, both only to listed hosts:

  • a CONNECT tunnel on port 443;

  • a plain request on port 80.

  • deploy/egress-proxy.conf (and its mirror) gains acl Safe_ports port 80 and http_access deny !Safe_ports !CONNECT, directly before allow allowed. The rule names only port and method ACLs, so no unlisted name is ever resolved. It never matches a CONNECT, so pi's tunnelled calls are untouched.

  • A side effect, now documented: a forward GET https://host/ is refused. Before this change it made squid open the TLS connection itself.

  • doctor and doctor --live run a third egress probe. It sends a raw plain-HTTP GET http://api.anthropic.com:443/ through the proxy, and it counts as refused only on a 403 with X-Squid-Error: ERR_ACCESS_DENIED. The live verdict now needs each of the three probes exactly once, by name.

  • doctor warns when egress is on and a triggered forge's GITLAB_URL or FORGEJO_URL is anything but https:// on 443. A job gets the proxy variables in uppercase only, and git ignores HTTP_PROXY for an http:// remote (measured in the job image, git 2.39.5), so an http:// forge fails on any port, 80 included. That was already so before this change. The proxy also refuses a CONNECT to any port but 443. The clone runs on the host, so what fails is a job's git push and fetch, and its API calls too off port 80 (glab and tea do use HTTP_PROXY). A URL with any other scheme gets its own "not an https:// URL" line. The ways out are https:// on 443, or PI_EGRESS=0. docs/gitlab.md, docs/forgejo.md and docs/egress.md say so.

  • The issue asked for the new case in the canary rows. Those rows (CANARY_LINES) describe the leftover sweep's lines only, so the new case lives in the probe loop and in docs/egress.md's sample doctor output. A new test pins that sample output to the code.

  • docs/egress.md says exactly what passes. The specs INT-EGRESS-POLICY-CONTRACT, INT-LIVE-PROBE-CONTRACT, REQ-EGRESS-ALLOWLIST and DES-EGRESS-DENY-ON-A-DEDICATED-NETWORK are amended.

Measured before and after (2026-09-30)

Runtimes: squid 6.13 (the pinned digest) on Docker 29.1.3 and rootless Podman 4.9.3 (Ubuntu 24.04), and rootless Podman 5.8.1 (Fedora). All three gave the same results.

Request through the proxy Before After
plain GET, listed host, port 8080 200 403 ERR_ACCESS_DENIED
plain GET, listed host, port 80 200 200
CONNECT, listed host, port 8080 (how pi sends) 403 403
POST https provider (no key) 401 upstream 401 upstream
plain GET, unlisted host 403, never resolved 403, never resolved
plain GET api.anthropic.com:443 (the canary) 400 from upstream, about 0.1 s 403, about 1 ms
forward GET https://api.anthropic.com/ 503, squid tried TLS itself 403
npm undici 8.10.0 EnvHttpProxyAgent without proxyTunnel, port 8080 200 403
Node 22 built-in fetch with NODE_USE_ENV_PROXY=1, http:// CONNECT, refused CONNECT, refused

Upgrading

  • Existing deployment folders keep the old egress-proxy.conf. doctor names the difference and pi-dispatch up offers a refresh; on the podman venue use service install --force.
  • Until the proxy restarts on the new file, the new doctor line fails. That line is the proof that the rule is in force.

Tests

  • A static order test on the shipped config.
  • The plain canary script run against a fake proxy, covering 7 answer shapes.
  • Argv, loop and verdict tests.
  • The forge warning, and the docs sample output against the labels.
  • 18 mutations, each shown red:
    • rule moved after the allow
    • !CONNECT dropped
    • port widened
    • rule deleted
    • any 403 counted as denied
    • fetch instead of a raw request
    • two readings accepted
    • slugs reordered
    • forge warning dropped, or ignoring the policy, the triggers, http on 80, https off 443, or the hidden URL credential
    • verdict counting readings without names
    • docs sample drifting from the label, and the label from the docs
    • fix text losing the hand-started proxy path

@edgehero
edgehero force-pushed the fix/508-plain-http-port-80 branch 3 times, most recently from 5489961 to 0d1ae6b Compare September 30, 2026 14:27
…ctor proves it (#508)

The proxy's `http_access allow allowed` had no port rule, so any client that
forwards plain HTTP (curl -x, package managers, git over http://, npm undici's
EnvHttpProxyAgent from 8.7 without proxyTunnel) reached every port of a
listed host in clear text. The rules now carry `acl Safe_ports port 80` and
`http_access deny !Safe_ports !CONNECT` directly before that allow: it
resolves nothing and never matches a CONNECT, so the tunnel rules are
untouched. A plain forward `GET https://` is now refused too.

doctor's egress canary gains a third probe, plainhttp: a raw node:http
forward request to http://api.anthropic.com:443/, counted as refused only on
squid's 403 with X-Squid-Error ERR_ACCESS_DENIED. egressVerdict requires each
of the three probes exactly once, by name, and words a wrong one by its
probe. doctor warns when egress is armed and a triggered forge's GITLAB_URL
or FORGEJO_URL is anything but https:// on 443: the job image's git ignores
the uppercase HTTP_PROXY for an http:// remote, so a job's push and fetch
fail (its API calls too, off port 80), and the proxy refuses a CONNECT off
443.
docs/egress.md's sample doctor lines are now pinned to the code.

Specs: REQ-EGRESS-ALLOWLIST AMENDED; INT-EGRESS-POLICY-CONTRACT AMENDED;
INT-LIVE-PROBE-CONTRACT AMENDED; DES-EGRESS-DENY-ON-A-DEDICATED-NETWORK
AMENDED (owner decision: port 80 kept over HTTPS only).

Signed-off-by: Rob Boerman <robboerman@live.nl>
@edgehero
edgehero force-pushed the fix/508-plain-http-port-80 branch from 0d1ae6b to cbb51e8 Compare September 30, 2026 14:49
@edgehero
edgehero merged commit af3dafc into main Oct 2, 2026
9 of 10 checks passed
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.

The egress proxy lets plain HTTP reach an allowlisted host on any port

1 participant