v1.3.0
A security release. Upgrade if you rely on aidc's egress filtering — until now
it could be bypassed trivially, and silently.
The proxy was advisory
aidc routed the dev container's HTTP through Squid by setting HTTP_PROXY. That
is a request, not a constraint. From a real session:
$ env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy \
curl -s -o /dev/null -w '%{http_code}' https://example.com/
200
$ grep -c example.com /var/log/squid/access.log
0
Four env -u flags and the sandbox was open. Worse than the bypass itself is the
second line: zero entries in the access log. The blocklist never applied, and
because the policy sidecar tails that log, taint detection was blind to it. Squid
only ever saw the traffic that asked to be seen.
Anything ignoring those variables had the same free pass — a library that doesn't
read them, a raw socket, a statically linked binary. No agent malice required.
NET-10 claimed processes inside the dev container "cannot reconfigure or bypass"
the proxy. That was not true, and had not been true in any released version.
Egress is now enforced by topology
The session bridge is a Docker internal network. Docker installs no
masquerade rule for it, so there is no route off that bridge at all. Squid is
dual-homed onto a separate egress network and is the only way out.
This is deliberately not a firewall rule. The dev container runs --privileged
(it needs it for DinD), so anything inside it can flush any rule it can see —
enforcement has to live where the agent cannot reach. Attacked as root, with the
proxy variables stripped, an explicit default route added via Squid, ip_forward
enabled, and its own MASQUERADE installed:
| attempt | result |
|---|---|
| direct HTTPS, proxy env stripped | blocked |
raw TCP to 1.1.1.1:80 |
Network is unreachable |
root + own default route + MASQUERADE |
still blocked |
| proxied HTTP | 200 |
blocklisted .cn via Squid |
403 |
Only squid, refresher (fetches threat feeds) and policy (POSTs the taint
webhook) join the egress network. dev and audit never do.
The test that should have caught it
make smoke had a step called "proxy enforcement" that passed on every release.
It only ever tested the proxied path — curl -x http://aidc-proxy:3128 — which
succeeds just as happily on a sandbox enforcing nothing, because Squid answers
when you ask it to. Nothing checked that going around Squid failed.
The suite now asserts that stripping the proxy variables fails, that raw TCP to a
public IP fails, and that Squid is still reachable, so it can no longer go green
on this class of bug.
Attaching networks still works
aidc create --network and aidc network <session> add are unchanged. A session
still reaches an attached project's postgres, NATS or redis by container name, on
any port.
Because most compose bridges are NATed — and an internal bridge has no default
route of its own — attaching one does restore general internet egress as a side
effect. aidc cannot prevent that; it does not own that network. Detaching closes
it again, and the smoke suite now asserts exactly that scoping. Attach the
narrowest network that does the job.
Escape hatch
aidc create myproj --egress direct # or `egress: direct` in .aidc/config.yamlRestores the pre-1.3.0 NATed bridge for sessions needing reachability an attached
network cannot provide — ZeroTier/Tailscale hosts, direct DNS. aidc create states
plainly that enforcement is off for that session.
Compatibility
Declared --port forwards and aidc proxy are unchanged from your side, but are
now dual-homed aidc/forwarder sidecars — an internal network cannot publish
ports, so ports: on the dev service would have silently done nothing. DinD image
pulls route through Squid (the inner daemon already inherits the proxy env).
Existing sessions keep their current behaviour until recreated. aidc kill +
aidc create picks up enforcement; aidc upgrade does not, since it reuses the
compose file rendered at create time.
Existing sessions are not converted
aidc upgrade reuses the compose file rendered at create time, so upgrading a
session created before 1.3.0 leaves it on its old NATed bridge — upgraded, but
still bypassable. aidc status <name> now reports which posture a session is in,
and aidc upgrade warns before preserving an unenforced one.
To convert: aidc kill <name> && aidc create <name> ...