Enterprise Network Proxy
Context
autograph containers need outbound internet access for two purposes:
- Build time --
npm install -g opencode-aiin the Dockerfile - Runtime -- LLM API calls (HTTPS), package installs from shell, registry access for the root project
In enterprise environments, direct internet access is restricted. Outbound traffic must traverse a corporate HTTP(S) proxy, TLS connections may be intercepted (custom CA certificates), and packages may come from private registries.
The current architecture has no proxy support. The Dockerfile assumes direct npm access. Agent containers have no proxy variables. No mechanism exists to inject custom CA certificates.
Beyond enterprise compatibility, a dedicated proxy container serves two strategic purposes for autograph itself:
- Centralized outbound interface -- all egress traffic passes through a single, controllable point. This decouples containers from the host's network configuration (corporate proxy, direct access, VPN -- the proxy adapts, the containers don't change).
- Centralized security enforcement -- the proxy is the natural place to implement egress filtering. Today: pass-through. Tomorrow: per-source allowlists controlling which domains each agent (or the LLM) is allowed to reach, limiting exposure to the internet and the corporate intranet.
Architecture decision already made
A dedicated proxy container (autograph.pkg.proxy) serves as the sole
outbound gateway. This ADR resolves the remaining unknowns:
- Which proxy technology?
- How does the network topology work?
- How are CA certificates handled?
- How is private registry access handled? (generalized -- npm only for now)
- What are the network isolation boundaries?
Constraints
- The proxy is always active (no optional profile). It acts as a transparent pass-through when no corporate proxy is configured.
- Corporate configuration (proxy URLs, CA certs, registry credentials) must never appear in agent workspaces or agent-accessible environment variables. This is a security requirement from the 3-layer model.
- The solution must work identically in direct-access environments (proxy as pass-through) and behind corporate proxies.
- Minimal resource footprint -- the infrastructure stack is already substantial (agents, phoenix, west). Every new container must justify its CPU and memory cost.
Decision 1: Proxy technology
Evaluation
| Criterion | squid | tinyproxy | nginx (forward proxy) | envoy |
|---|---|---|---|---|
| Image size (Alpine) | ~15-20 MB | ~2 MB | ~25 MB | ~50+ MB |
| Idle CPU/memory | ~5-10 MB RSS, negligible CPU | ~1-2 MB RSS, negligible CPU | ~5 MB RSS, negligible CPU | ~30-50 MB RSS, non-trivial CPU |
| HTTPS CONNECT | Native, robust | Native | Not native (stream + ssl_preread) |
Native |
| Upstream proxy chaining | cache_peer with auth |
upstream directive |
Not native | Native (cluster + route) |
| Custom CA injection | ssl_bump (MITM) or pass-through |
Pass-through only | Complex | Native (SDS/filesystem) |
| Egress ACL / filtering | Powerful (domain, IP, time, regex). Domain filtering on HTTPS via ssl_bump --peek (reads SNI without full interception). |
Basic (IP-based Allow/Deny). No domain filtering on CONNECT. |
Location-based (HTTP only) | Powerful (Lua, RBAC, WASM). Native SNI-based filtering. |
| Logging | Detailed (access.log, cache.log) | Basic (connect.log) | access_log (limited for CONNECT) | Structured (JSON, gRPC tap) |
| Registry auth (transparent) | URL rewrite + header injection (HTTP). ssl_bump required for HTTPS. |
Not supported | proxy_set_header (HTTP only) |
Header injection (Lua filter) |
| Config complexity | Medium (verbose, well-documented) | Low (10-15 lines) | High (forward proxy is a non-native use) | High (YAML/protobuf, complex) |
| Maintenance / community | Active project, security patches | Minimal dev activity | Very active (but forward proxy is niche) | Very active, complex upgrades |
Option A -- tinyproxy
Minimal forward proxy. HTTPS CONNECT support. Ultra-simple configuration.
- For: Smallest footprint (~2 MB image, ~1 MB RAM). Simplest config. Does
exactly what we need for basic forwarding -- HTTP forward and HTTPS CONNECT
tunnel. Upstream proxy chaining via
upstreamdirective. - Against: No TLS interception (pass-through only). No domain-level ACLs on HTTPS CONNECT -- cannot filter which destinations agents are allowed to reach. No transparent registry auth for HTTPS. Minimal logging. Low maintenance activity (risk of abandonment). Blocks the egress filtering roadmap -- would require full replacement to add per-source domain allowlists.
Option B -- squid
Full-featured caching proxy. Industry standard for corporate environments.
- For: Battle-tested in enterprise environments. Powerful ACLs -- domain
filtering on HTTPS CONNECT via
ssl_bump --peek(reads SNI without full MITM, enough for allowlists). Rich logging. Upstream proxy chaining with auth (cache_peer). Can do transparent registry auth via URL rewriting and header injection. Large community, regular security patches. Supports the egress filtering roadmap without replacing the technology. - Against: Larger footprint (~15-20 MB image, ~5-10 MB RAM). More complex
configuration (squid.conf is its own language). Cache features are
unnecessary (disabled in our config).
ssl_bumpfor full HTTPS inspection requires generating dynamic certificates (complex, but--peekmode for SNI-only filtering does not).
Option C -- nginx (stream module)
Web server repurposed as forward proxy using the stream module.
- For: Very widely known. Excellent HTTP reverse proxy. Active development.
- Against: Forward proxying is not nginx's design purpose. HTTPS CONNECT
requires
ngx_stream_ssl_preread_modulewhich is a workaround, not a native feature. No upstream proxy chaining for CONNECT tunnels. Configuration for forward proxy use is non-obvious and poorly documented. Most guides and community knowledge focus on reverse proxying. Not a viable path for egress filtering.
Option D -- envoy
Cloud-native proxy. Designed for service mesh and edge proxy use cases.
- For: Extremely capable. Native HTTPS CONNECT with SNI-based filtering. Rich observability (metrics, tracing, structured logs). Header manipulation (Lua, WASM). Active development by CNCF. Full egress filtering roadmap support.
- Against: Largest footprint (~50+ MB image, ~30-50 MB RAM, non-trivial idle CPU). Most complex configuration (YAML/protobuf, listener/filter/cluster model). Massive feature surface -- overkill for a forward proxy. Upgrades can be complex (breaking changes between versions). Designed for service mesh, not desktop dev tooling. Disproportionate resource cost for the current use case.
Decision
squid. Rationale:
- Enterprise-grade forwarding: native HTTPS CONNECT, upstream proxy chaining with authentication, rich logging. Covers the current use case with margin.
- Egress filtering roadmap:
ssl_bump --peekreads the SNI (destination domain) from HTTPS CONNECT requests without full TLS interception. This enables per-source domain allowlists (e.g., agent X can only reachapi.anthropic.comandapi.openai.com) without generating dynamic certificates or performing MITM. This is a security must-have for the medium term. - Reasonable resource cost: ~15-20 MB image, ~5-10 MB idle RAM, negligible CPU in forwarding mode (no caching, no content inspection). Acceptable given the security benefits.
- Battle-tested: 30+ years in production. Well-understood failure modes. Regular security patches. Enterprise IT teams already know it.
- Transparent registry auth (future): squid can inject authentication headers for specific upstream domains, enabling transparent access to private registries without exposing credentials to agents.
- Use an existing image: initially, no custom Dockerfile. An existing squid image (pinned version) with a mounted configuration file is sufficient.
Eliminated alternatives:
- tinyproxy: blocks the egress filtering roadmap. Would require full replacement when per-source allowlists are needed.
- nginx: forward proxying is a non-native, poorly documented use case.
- envoy: disproportionate resource cost and complexity for a forward proxy.
Decision 2: Network topology -- proxy as bridge
The isolation problem
The 3-layer security model requires agent isolation: dev-agent and metalab-agent must not be able to reach each other or see each other's traffic. If both agents join a shared network for internet access, this isolation is broken.
Design
The proxy container joins each agent's isolated network plus a gateway-network that serves as the general-purpose network for all non-agent services. Agents stay on their own networks and never share one.
internet
|
egress-net
(proxy only)
|
+------+------+
| PROXY |
+-----------+ +--+---+---+--+ +---------------+
| dev-agent | | | | | metalab-agent |
+-----+-----+ | | | +-------+-------+
| | | | |
+- agent-net -+ | +- metalab-net -+
(internal) | (internal)
|
+-----------+-----------+
| gateway-net |
| (internal) |
+--+--------+--------+--+
| | |
+--------+ +------+ +-------+
| west | | app | | ... |
+--------+ +------+ +-------+
phoenix is on agent-net + metalab-net (observability) + host-only
host-only (NOT internal, but no internet) publishes ports to the host:
phoenix :6006, the seed app dev servers. Temporary -- to be replaced by an
ingress reverse proxy. host-internet (NOT internal, NAT) is the explicit,
temporary way to the internet through the host; no container by default.
All other networks are internal: no Docker NAT, no direct internet. Only
the proxy is on egress-net (internet for agents).
How it works:
- The engine compose defines three internal networks:
agent-network,metalab-network, andgateway-network(the compose default). All three haveinternal: true-- no Docker NAT, no direct internet access. - The proxy package defines a fourth network:
egress-network(AUGR_NETWORK_EGRESS). Only the proxy container joins it -- it is the proxy's sole path to the internet. (Two other non-internal networks exist:host-only, to publish ports to the host, andhost-internet, a temporary direct way to the internet -- see below.) - The proxy container joins all four networks: the three internal networks (to be reachable by all containers) plus egress (for internet access).
- Agent containers stay on their own isolated internal network. They reach
the proxy via its hostname (
augr-proxy) which resolves on their network because the proxy is a member. - Non-agent services (west, app services) join
gateway-network(internal). They reach the proxy there. They also needhttp_proxyenv vars if they need internet access. The seed app containers (front, playground, back) do so, and load the proxy client settings (client.env); they also joinhost-onlyto publish their ports (see below). - All containers set
http_proxy/https_proxyenvironment variables pointing tohttp://augr-proxy:<port>. All outbound traffic goes through the proxy.
Host access (host-only) and direct internet (host-internet):
Docker cannot publish a container port to the host from an internal
network. The stack (task augr:up) therefore creates two non-internal
networks with fixed names (not contract variables), one per purpose:
augr-host-only: access to and from the host only, not to the network of the host -- published ports, local development. IP masquerading is off (com.docker.network.bridge.enable_ip_masquerade=false), so traffic to the outside has no way back: no internet. Phoenix (UI onhttp://localhost:6006) and the seed app containers (dev servers) join it.augr-host-internet: direct internet through the host NAT, for a container that cannot go through the proxy yet. Temporary and explicit: no container joins it by default (for exampledocker network connect augr-host-internet <container>). Its name sorts beforeaugr-host-onlyon purpose: on Docker < 28, a container on several networks takes its default route from the first non-internal one by name.
Both are temporary measures, to be replaced by an ingress reverse proxy (see
Exx-ingress-local-dns). Until then, host-only no longer gives Phoenix
and the app containers a direct way to the internet as a side effect.
Network membership per service:
| Service | agent-net (internal) | metalab-net (internal) | gateway-net (internal) | egress-net | host-only (no internet) | host-internet |
|---|---|---|---|---|---|---|
| dev-agent | yes | |||||
| metalab-agent | yes | |||||
| proxy | yes | yes | yes | yes | ||
| phoenix | yes | yes | yes | |||
| west | yes | |||||
| app services | yes | yes |
Key properties:
- All networks except egress, host-only and host-internet are
internal (
internal: truein Docker). No agent container has Docker NAT; no agent can reach the internet directly. This is network-level enforcement.host-onlyis not internal, for port publishing, but has no internet (IP masquerading off). - The proxy is the sole internet gateway for the agents. It is the only container on the egress network (its only path to the internet). It bridges the three internal networks to the internet.
- Proxy down = zero internet for the agents. Guaranteed by the
network stack for every container on an internal network. A process that
ignores
http_proxyenv vars still cannot reach the internet. The same holds for the containers onhost-only; onlyhost-internet, empty by default, bypasses the proxy. - Agents are NOT on a shared network. They cannot reach each other.
- The proxy does not forward traffic between internal networks -- it only forwards outbound HTTP(S) to the internet.
gateway-networkis the shared internal network for non-agent services. Services that need internet access sethttp_proxypointing to the proxy (same pattern as agents).- Host entry point. A client started on the host network sees no Docker
network, hence no
augr-proxy. The proxy also publishes its port on the loopback of the host only (127.0.0.1:13128,AUGR_PKG_PROXY_HOST_PORTin the pkg.proxy Taskfile; not 3128, often taken by a local corporate proxy). Used by theconnecttasks of the agents (OAuth login on the host network, see ADR agent-authentication-architecture, Decision 4). - Phoenix is on agent-net and metalab-net for observability, plus
host-onlyto publish its :6006 UI to the host. It does not use the internet (telemetry disabled).
Enforcement summary:
| Mechanism | What it guarantees |
|---|---|
internal: true on all networks but egress and the host ones |
No agent container has direct internet (no NAT). |
IP masquerading off on host-only |
Published ports without a direct way to internet. |
| Egress network with only the proxy | Single point of internet access. |
| Proxy on all internal networks | All containers can reach the proxy for egress. |
http_proxy / https_proxy env vars |
Applications route through proxy (convention). |
| Proxy down | All internet access stops (verified by test suite). |
Build-time vs runtime: two different worlds
The network topology above is runtime only. During docker build, the
proxy container may or may not be running. This creates two scenarios:
Runtime (containers running):
- All outbound traffic routes through the proxy via
http_proxy/https_proxy - The proxy handles upstream corporate proxy chaining, logging, and (future) egress filtering
- Full control via squid ACLs
Build time and host tooling (docker build, or a tooling container
started before the stack, such as West):
- The corporate network is a property of the workstation, configured once in
env/.env.augr(AUGR_NET_PROXY_URL,AUGR_NET_NO_PROXY,AUGR_NET_CA_CERT_DIR) and declared in the engine contract. squid (its upstream), West (build and run) and the package image builds read this block directly. - Image builds receive the proxy as the standard Docker build arguments
(
http_proxy,https_proxy,no_proxyand their uppercase forms) from the block: on the WSL2 target, a build has no direct route to the internet. The images that make TLS connections (West, agent, seed app) also receive the CA certificates (Decision 3). Predefined proxy arguments are not kept indocker history. - autograph does not write the Docker client configuration
(
~/.docker/config.json). - A build cannot go through the stack proxy: BuildKit only builds on the
default,hostornonenetworks, not on a stack network.
Rationale: one source for the corporate network, owned by the engine, so that the packages interoperate on any network. Before, the same proxy URL was written in the host shell, in the proxy package env file and in the Docker client configuration, and the CA directory lived elsewhere again.
The operator guide and the image convention are in
docs/internal/corporate-network.md.
Credential safety at build time. The npm install needs the registry URL
and, for an authenticated registry, a token. The URL is not sensitive; the
token must never persist anywhere in the agent image (filesystem, env,
layer, or docker history). Options:
| Option | How | Problem |
|---|---|---|
Build arg + same-layer .npmrc cleanup |
ARG NPM_REGISTRY_TOKEN + rm ~/.npmrc in the RUN |
--build-arg values are recorded in image metadata and surface via docker history (the legacy builder prefixes the RUN created_by with |N ARG=value). Same-layer cleanup closes only the filesystem vector, not history. |
| Multi-stage build | Use the token only in a discarded builder stage, COPY artifacts to the final stage |
Keeps the token out of the final image, but does not protect the build cache and adds complexity (copying global npm installs across stages). Not sufficient alone. |
| Proxy-injected auth (no token in the build) | squid injects registry credentials; the agent build only knows the URL | Strongest isolation and the long-term target, but transparent HTTPS auth needs ssl_bump (deferred -- see Registry authentication). |
| BuildKit secret mount (chosen) | RUN --mount=type=secret reads the token from a tmpfs mount for one RUN |
None for the agent image: the token never becomes a build arg, layer, history entry, env var, or part of the final image. Requires BuildKit. |
Decision: BuildKit secret mount. It is the only option that keeps the token
out of the image and out of docker history without the complexity of
multi-stage, and it is available today (unlike proxy-injected auth). Pattern:
# syntax=docker/dockerfile:1
ARG NPM_REGISTRY_URL=""
RUN --mount=type=secret,id=npm_token set -e; \
NPMRC="$(mktemp)"; export NPM_CONFIG_USERCONFIG="$NPMRC"; \
if [ -n "$NPM_REGISTRY_URL" ]; then \
npm config set registry "$NPM_REGISTRY_URL"; \
if [ -s /run/secrets/npm_token ]; then \
npm config set "$(echo "$NPM_REGISTRY_URL" | \
sed 's|^https\?:||')/:_authToken" "$(cat /run/secrets/npm_token)"; \
fi; \
fi && \
npm install -g opencode-ai@${OPENCODE_VERSION} && \
rm -f "$NPMRC"
The token is supplied via compose build.secrets (sourced from
AUGR_PROXY_NPM_REGISTRY_TOKEN); an empty or absent secret means anonymous
access. Requires BuildKit -- the default for docker compose build.
Decision 3: CA certificate handling
The problem
In enterprise environments with TLS inspection, the corporate proxy terminates and re-signs TLS connections using a private CA. Containers must trust this CA or all HTTPS connections fail with certificate errors.
Design
Certificate source: one host directory of CA files (.crt or .pem), set
once for the workstation with AUGR_NET_CA_CERT_DIR in env/.env.augr
(optional). A standard system path (e.g.,
/usr/local/share/ca-certificates/corporate) lets the host trust the same
certificates (update-ca-certificates).
West: it reaches the internet before the stack proxy exists, through
the corporate proxy (see "Build-time vs runtime"). The engine copies the
directory into the West build context (west:certs) before each build, and
the image trusts the certificates for pip (build) and git (run). A volume
would not cover the build. The build cache keeps this cheap: only a
certificate change rebuilds from that step.
The agent image: it receives the CA the same way, copied by pkg.agent
(task certs) into its own build context. On the WSL2 target, its HTTPS
download from registry.npmjs.org fails without it (SSL certificate problem). The image also sets NODE_EXTRA_CA_CERTS, which covers opencode at
runtime.
The seed app images (node-dev, rust-dev): same mechanism, copied by the
app tasks (certs, Taskfile.d/apps/app.yaml); app:rebuild fails on a
certificate error without it on the WSL2 target.
Other images (drawio, the proxy): no TLS connection, no CA. A CA step is added to an image only after a certificate error is seen on a target, in the package that owns the image.
Proxy container: squid only tunnels HTTPS (CONNECT) and never validates
TLS itself, so it needs no CA certificate.
Agents: their image trusts the corporate CA (above). The trust store is not a workspace file: no corporate configuration appears in agent workspaces.
Known limitation -- corporate TLS inspection on CONNECT tunnels: if the corporate proxy inspects TLS inside CONNECT tunnels, the re-signed certificate reaches the agent (our proxy does CONNECT pass-through), and the agent needs the corporate CA. The WSL2 corporate target inspects TLS (at least registry.npmjs.org, seen in the agent build). The agents get the corporate CA in their image (above), rather than TLS termination in our proxy (see the alternative below); the runtime path is still to be validated.
Alternative considered: inject certs into the proxy only
If the proxy did full TLS interception (squid ssl_bump in bump mode, not
just peek), certs would only be needed in the proxy and agents would trust
the proxy's own CA. This was rejected because:
- Full
ssl_bumprequires the proxy to generate dynamic certificates (complex CA management) - It adds a MITM component inside the dev environment
- It couples agent TLS trust to the proxy implementation
ssl_bump --peek(SNI-only, no interception) is sufficient for egress filtering and does not require agents to trust a proxy CA
Decision 4: Private registry access
Scope
This section covers access to private package registries in enterprise
environments. The pattern is general (any registry behind authentication),
but for now it validates npm specifically because it is the only registry
autograph uses today (npm install -g opencode-ai).
Future registries (pip, Docker Hub, Maven, etc.) would follow the same pattern with their own type-specific variables.
Variable naming convention
Registry variables include the registry type in the name to support multiple registry types in the future:
AUGR_PROXY_NPM_REGISTRY_URL-- npm registry URLAUGR_PROXY_NPM_REGISTRY_TOKEN-- npm registry auth token
Future additions would follow the pattern:
AUGR_PROXY_PIP_REGISTRY_URL,AUGR_PROXY_PIP_REGISTRY_TOKENAUGR_PROXY_DOCKER_REGISTRY_URL,AUGR_PROXY_DOCKER_REGISTRY_TOKEN
Build-time registry access
The Dockerfile npm install needs to reach the package registry. In
enterprise environments, this may be a private registry (Artifactory, Nexus,
Verdaccio).
The registry URL (AUGR_PROXY_NPM_REGISTRY_URL) is passed from the env
template through the compose file to the Dockerfile as the build arg
NPM_REGISTRY_URL (not sensitive). The token
(AUGR_PROXY_NPM_REGISTRY_TOKEN) is passed as a BuildKit secret
(compose build.secrets -> RUN --mount=type=secret,id=npm_token), never as a
build arg (see Decision 2, "Credential safety at build time").
Runtime registry access
Runtime registry access is needed in several scenarios:
- Agent shell: the operator may want to test an opencode upgrade
(
npm install -g opencode-ai@latest) from within a running container - Root project: the project workspace may need continuous registry access
for
npm installof project dependencies - Future plugins: agents may install plugins at runtime
At runtime, the proxy container handles registry access. Containers reach
the registry through the proxy via http_proxy / https_proxy. The
registry URL is configured in the container's npm config (set by the
entrypoint from AUGR_PROXY_NPM_REGISTRY_URL if non-empty).
Note: the registry URL is set in the container's npm config (visible to the
agent), but the auth token is NOT -- it stays in the proxy's
configuration (L3). At runtime, the proxy handles authentication
transparently (future capability with squid ssl_bump). For now, runtime
registry access uses the URL without token-level auth (public registries, or
registries with IP-based auth).
Exception (2026-10-19, temporary): the seed app containers. task app:init
runs pnpm install inside them, and a corporate registry may require a token.
app:prepare writes the registry URL and token in env/ (services/app/npmrc),
mounted read-only as ~/.npmrc in the front and playground containers. Risk:
those containers run the project code, which agents write; a script run there
can read the token. Accepted by the operator until the draft epic
Exx-registry-credentials provides a mechanism that keeps the token out of the
containers (a registry mirror in the stack, or squid injecting it).
Registry authentication
Current: the registry URL is a build arg; the auth token is a build-time BuildKit secret (never a build arg -- see Decision 2). At runtime, registry access works for public registries or registries with network-level auth. Token-based runtime auth is deferred.
Future: squid can inject authentication headers for specific upstream
domains, enabling transparent runtime registry auth without exposing
credentials to agents. This requires ssl_bump for HTTPS registries. A
registry mirror in the stack is the other candidate. Both are studied in the
draft epic Exx-registry-credentials, which also removes the exception for
the seed app containers (see "Runtime registry access").
Decision 5: Always-active proxy
The proxy container runs in all profiles (dev, metalab). When no
corporate proxy variables are set, it acts as a transparent pass-through --
forwarding requests directly to the internet.
Rationale:
- Uniform network topology -- services always route through the proxy, whether or not a corporate proxy is upstream
- No conditional branching in compose files (no "with proxy / without proxy" profiles)
- Future security use cases (egress filtering, domain allowlists) benefit from always having the proxy in the path -- no retrofit needed
- Acceptable resource cost: ~15-20 MB image, ~5-10 MB idle RAM, negligible CPU in forwarding mode
Pass-through behavior: when AUGR_NET_PROXY_URL is empty, squid
connects directly to the internet (no cache_peer configured). The
configuration is identical; only the upstream routing changes.
Consequences
Positive
- Enterprise unblocked: autograph works behind corporate proxies with custom CAs and private registries
- Centralized egress control: all outbound traffic passes through a single point. Today: transparent forwarding. Tomorrow: per-source domain allowlists, logging, auditing
- Agent isolation preserved: no corporate credentials, proxy URLs, or certificates in agent workspaces (L1). Agents on separate networks cannot reach each other.
- Technology supports the roadmap: squid's
ssl_bump --peekenables SNI-based egress filtering without full TLS interception, covering the security hardening roadmap - Zero-config for direct access: the proxy is transparent when no corporate config is set. Existing users see no change in behavior.
Negative / Risks
- Additional container: one more service to start, monitor, and debug. Mitigated by squid's low idle resource consumption and the always-on design (no conditional logic).
- squid configuration complexity: squid.conf is verbose and has its own DSL. Mitigated by a minimal, well-commented configuration (forwarding only, no caching) with documentation.
- Proxy is multi-homed: the proxy joins multiple networks (agent,
metalab, gateway). While it does not forward traffic between agent
networks, a vulnerability in the proxy could theoretically bridge them.
Mitigated by squid's maturity and
no-new-privileges/cap_drop: ALLhardening. - Build-time and runtime are separate mechanisms: host tooling (West) and
image builds read the corporate network block of
.env.augrdirectly; at runtime, the stack containers go through the proxy container. Two mechanisms, documented and justified (Docker and BuildKit constraints: a build cannot use a stack network). - Build-time credentials use BuildKit secrets: the npm registry token is
mounted as a secret for a single RUN, never a build arg -- so it cannot leak
via image layers or
docker history. The registry URL (non-sensitive) remains a build arg. - The corporate CA is baked into the images that need it (West, agent, seed app; when one is configured), each copied by its owner. Other images get a CA step only after a certificate error is seen on a target.
- Squid image lifecycle changed after this decision:
ubuntu/squid:6.10was based on an unsupported Ubuntu release and no longer received current Squid fixes. pkg.proxy now builds Squid from the Ubuntu 26.04 archive. The container runs as the host UID/GID, drops all Linux capabilities, disables the unused ICMP pinger, and has a package-level rebuild/reup/wait lifecycle.
Open questions
Which domains does the WSL2 corporate target inspect? registry.npmjs.org is (agent build). To check from the host for the others (certificate issuer of the domains used by the builds and the agents).
Agents at runtime behind the TLS-inspecting proxy: their image trusts the corporate CA; opencode reaching the LLM provider through the tunnel is still to be validated on the WSL2 target.
squid image selection: resolved for now by the Ubuntu 26.04 package image built by pkg.proxy. Revisit if a maintained multi-architecture image with a clear Squid-version and security-update contract provides lower maintenance cost.
Per-source egress filtering: the design supports it (squid ACLs +
ssl_bump --peek), but the configuration model (how operators define allowlists per service) is deferred to Exx-security-hardening.Third-party package egress isolation: services on
gateway-networkcan reach each other. Full isolation requires per-service gateway networks or squid ACLs by source IP. Deferred.