feat: unify API proxy sidecar into Squid proxy container#1026
feat: unify API proxy sidecar into Squid proxy container#1026
Conversation
Merge the standalone API proxy sidecar (containers/api-proxy/) into the Squid proxy container, reducing the container count from 3 to 2 when --enable-api-proxy is used. The Node.js auth proxy now runs inside the Squid container alongside Squid itself. This eliminates the extra container, network hop, and simplifies the architecture while maintaining all security properties: - Credential isolation (API keys never reach agent container) - Domain filtering (all LLM traffic still passes through Squid ACLs) - Header stripping (client auth headers removed before injection) - Container hardening (no-new-privileges, resource limits, non-root) Key changes: - Move server.js, package.json into containers/squid/ - Update Dockerfile to install Node.js and auth proxy deps - Update entrypoint.sh to start both Squid and Node.js (non-root) - Remove api-proxy Docker Compose service from docker-manager.ts - Add security hardening to Squid service (no-new-privileges, limits) - Update iptables rules (no separate proxy IP needed) - Update agent BASE_URL env vars to point to squid IP (172.30.0.10) - Delete containers/api-proxy/ directory Security review: CONDITIONAL PASS (all mitigations implemented) - Node.js runs as non-root 'proxy' user - Squid container gets no-new-privileges and resource limits - Proper signal handling for both processes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
Coverage comparison generated by |
Go Build Test Results
Overall: PASS ✅
|
|
🤖 Smoke test results for run 22376346760 — ✅ GitHub MCP: Last 2 merged PRs — #1025 "docs: add sandbox design rationale (Docker vs microVMs)", #992 "docs: update runner and architecture compatibility" Overall: PASS
|
C++ Build Test Results
Overall: PASS
|
Build Test: Node.js Results
Overall: ✅ PASS
|
.NET Build Test Results
Overall: PASS Run outputshello-world: json-parse:
|
🦀 Build Test: Rust
Overall: ✅ PASS
|
|
Smoke Test Results — PASS ✅ GitHub MCP: #1025 docs: add sandbox design rationale (Docker vs microVMs) | #991 feat(docker): pre-seed Maven/Gradle/sbt proxy config in agent container
|
Build Test: Bun ✅
Overall: PASS Bun version:
|
Build Test: Deno Results
Overall: ✅ PASS
|
Java Build Test Results
Overall: PASS ✅ All Maven projects compiled and all tests passed successfully.
|
|
GitHub MCP merged PRs: docs: add sandbox design rationale (Docker vs microVMs); feat(docker): pre-seed Maven/Gradle/sbt proxy config in agent container
|
Chroot Version Comparison Results
Result: Some versions differ between host and chroot. Python and Node.js versions do not match (chroot uses older versions). Go matches.
|
There was a problem hiding this comment.
Pull request overview
This PR consolidates the Node.js “API auth proxy” into the existing Squid proxy container, removing the standalone api-proxy sidecar so --enable-api-proxy runs with 2 containers instead of 3 while keeping credential isolation and domain filtering via Squid.
Changes:
- Embed the Node.js auth proxy into
containers/squid(Dockerfile, entrypoint, Node app + deps). - Update runtime orchestration and firewall rules to target Squid IP/ports directly (no separate proxy IP/service).
- Refresh tests + docs to reflect the unified architecture and new routing.
Reviewed changes
Copilot reviewed 13 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/integration/api-proxy.test.ts | Updates integration tests to target the unified proxy at 172.30.0.10 instead of the sidecar IP. |
| src/host-iptables.ts | Removes proxyIp handling and adds optional allow-rule for auth-proxy ports on Squid IP. |
| src/host-iptables.test.ts | Updates expectations to match the new fixed network config (no proxyIp). |
| src/docker-manager.ts | Removes api-proxy service, injects keys into Squid service, mounts auth-proxy logs dir, and updates healthcheck + env wiring. |
| src/docker-manager.test.ts | Updates compose-generation tests for the new “embedded proxy” behavior and env vars. |
| src/cli-workflow.ts | Switches workflow dependency types and passes a boolean “apiProxyEnabled” instead of an IP. |
| scripts/ci/cleanup.sh | Ensures cleanup removes legacy awf-api-proxy container name (backward compatibility). |
| docs/api-proxy-sidecar.md | Re-documents the feature as unified proxy architecture and updates diagrams/env var expectations. |
| containers/squid/server.js | Adds the Node.js auth proxy implementation (OpenAI/Anthropic/Copilot) into the squid image. |
| containers/squid/package.json | Adds npm metadata + dependency for the embedded Node proxy. |
| containers/squid/package-lock.json | Locks https-proxy-agent dependency tree for reproducible image builds. |
| containers/squid/entrypoint.sh | Starts Node proxy alongside Squid and adds multi-process signal handling. |
| containers/squid/Dockerfile | Installs Node.js/npm and copies/builds the embedded auth proxy into the Squid image. |
| containers/agent/setup-iptables.sh | Removes sidecar-specific bypass rules (agent now reaches auth proxy on Squid IP). |
| containers/api-proxy/README.md | Deletes obsolete sidecar container docs after merge into squid. |
| containers/api-proxy/Dockerfile | Deletes obsolete sidecar Dockerfile after merge into squid. |
Comments suppressed due to low confidence (1)
src/docker-manager.ts:1087
- The code now creates/mounts
/var/log/api-proxy(and mentions the auth proxy "still writes logs separately"), butcontainers/squid/server.jsonly logs to stdout/stderr and nothing redirects output into that directory. Either wire the Node process output to a file in/var/log/api-proxy(or add file logging), or drop the extra host directory creation + volume mount to avoid unused writable paths.
// Create api-proxy logs directory for persistence
// The auth proxy now runs inside the Squid container but still writes logs separately
// If proxyLogsDir is specified, write to sibling directory (timeout-safe)
// Otherwise, write to workDir/api-proxy-logs (will be moved to /tmp after cleanup)
const apiProxyLogsDir = config.proxyLogsDir
? path.join(path.dirname(config.proxyLogsDir), 'api-proxy-logs')
: path.join(config.workDir, 'api-proxy-logs');
if (!fs.existsSync(apiProxyLogsDir)) {
fs.mkdirSync(apiProxyLogsDir, { recursive: true, mode: 0o777 });
// Explicitly set permissions to 0o777 (not affected by umask)
fs.chmodSync(apiProxyLogsDir, 0o777);
}
logger.debug(`API proxy logs directory created at: ${apiProxyLogsDir}`);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // When API proxy is enabled, add API keys and auth proxy config to the Squid container | ||
| // The auth proxy Node.js server runs alongside Squid in the same container | ||
| if (config.enableApiProxy) { | ||
| // Pass API keys as environment variables to the unified Squid container | ||
| squidService.environment = { | ||
| ...(config.openaiApiKey && { OPENAI_API_KEY: config.openaiApiKey }), | ||
| ...(config.anthropicApiKey && { ANTHROPIC_API_KEY: config.anthropicApiKey }), | ||
| ...(config.copilotGithubToken && { COPILOT_GITHUB_TOKEN: config.copilotGithubToken }), | ||
| }; | ||
|
|
||
| // Mount API proxy log directory | ||
| squidVolumes.push(`${apiProxyLogsPath}:/var/log/api-proxy:rw`); | ||
|
|
||
| // Update healthcheck to verify both Squid AND the auth proxy are running | ||
| squidService.healthcheck = { | ||
| test: ['CMD-SHELL', `nc -z localhost 3128 && curl -sf http://localhost:${API_PROXY_HEALTH_PORT}/health`], | ||
| interval: '5s', | ||
| timeout: '3s', | ||
| retries: 5, | ||
| start_period: '10s', | ||
| }; | ||
| } |
There was a problem hiding this comment.
When --enable-api-proxy is set but no provider keys are present, generateDockerCompose still switches Squid to the combined healthcheck (expects :10000/health), but containers/squid/entrypoint.sh only starts the Node auth proxy when keys exist. This makes squid-proxy permanently unhealthy and blocks the agent via depends_on.
Adjust the condition so the combined healthcheck (and/or auth-proxy startup) is enabled only when at least one key is configured, or pass an explicit flag env var to the container and always start a minimal health-only proxy when enabled.
| // When API proxy is enabled, bypass HTTP_PROXY for the Squid IP | ||
| // so the agent can reach the auth proxy ports (10000-10002) directly | ||
| // The auth proxy is now embedded in the Squid container at the same IP | ||
| if (config.enableApiProxy) { | ||
| if (environment.NO_PROXY) { | ||
| environment.NO_PROXY += `,${networkConfig.proxyIp}`; | ||
| environment.NO_PROXY += `,${networkConfig.squidIp}`; | ||
| } else { | ||
| environment.NO_PROXY = `localhost,127.0.0.1,${networkConfig.proxyIp}`; | ||
| environment.NO_PROXY = `localhost,127.0.0.1,${networkConfig.squidIp}`; | ||
| } | ||
| environment.no_proxy = environment.NO_PROXY; | ||
| } |
There was a problem hiding this comment.
This NO_PROXY update can introduce duplicates (e.g., --enable-host-access already adds networkConfig.squidIp, then --enable-api-proxy appends it again). Consider normalizing/deduplicating the comma-separated list before assigning NO_PROXY/no_proxy to keep env output stable and avoid surprising diffs.
See below for a potential fix:
const baseNoProxy = environment.NO_PROXY || 'localhost,127.0.0.1';
const entries = baseNoProxy
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
if (!entries.includes(networkConfig.squidIp)) {
entries.push(networkConfig.squidIp);
}
environment.NO_PROXY = entries.join(',');
| # Fix permissions on api-proxy log directory | ||
| chown -R proxy:proxy /var/log/api-proxy | ||
| chmod -R 755 /var/log/api-proxy | ||
|
|
There was a problem hiding this comment.
chmod -R 755 /var/log/api-proxy makes the mounted log directory world-readable. Since these logs may contain sensitive request metadata, it would be safer to restrict permissions (e.g., 750/700) and rely on proxy:proxy ownership for access.
| su -s /bin/sh proxy -c "HTTP_PROXY='$HTTP_PROXY' HTTPS_PROXY='$HTTPS_PROXY' \ | ||
| OPENAI_API_KEY='${OPENAI_API_KEY:-}' \ | ||
| ANTHROPIC_API_KEY='${ANTHROPIC_API_KEY:-}' \ | ||
| COPILOT_GITHUB_TOKEN='${COPILOT_GITHUB_TOKEN:-}' \ | ||
| node /app/api-proxy/server.js" & |
There was a problem hiding this comment.
The su ... -c "... OPENAI_API_KEY='${OPENAI_API_KEY:-}' ..." pattern embeds credentials into the shell command line. That can leak secrets via process listings and is also fragile if values ever contain quotes/shell metacharacters.
Prefer exporting the env vars and using su with environment preservation (or a tool like gosu/setpriv) so secrets stay in the process environment rather than argv/command text.
| su -s /bin/sh proxy -c "HTTP_PROXY='$HTTP_PROXY' HTTPS_PROXY='$HTTPS_PROXY' \ | |
| OPENAI_API_KEY='${OPENAI_API_KEY:-}' \ | |
| ANTHROPIC_API_KEY='${ANTHROPIC_API_KEY:-}' \ | |
| COPILOT_GITHUB_TOKEN='${COPILOT_GITHUB_TOKEN:-}' \ | |
| node /app/api-proxy/server.js" & | |
| # Preserve environment so proxy and API key variables are available to Node.js | |
| su -m -s /bin/sh proxy -c "node /app/api-proxy/server.js" & |
| // 5b. Allow traffic to API auth proxy ports (when enabled) | ||
| // The auth proxy is embedded in the Squid container, so allow ports 10000-10002 to Squid IP. | ||
| // The auth proxy routes through Squid internally, so domain whitelisting is still enforced. | ||
| if (apiProxyEnabled) { | ||
| const minPort = Math.min(API_PROXY_PORTS.OPENAI, API_PROXY_PORTS.ANTHROPIC, API_PROXY_PORTS.COPILOT); | ||
| const maxPort = Math.max(API_PROXY_PORTS.OPENAI, API_PROXY_PORTS.ANTHROPIC, API_PROXY_PORTS.COPILOT); | ||
| logger.debug(`Allowing traffic to API proxy sidecar at ${apiProxyIp}:${minPort}-${maxPort}`); | ||
| logger.debug(`Allowing traffic to API auth proxy at ${squidIp}:${minPort}-${maxPort}`); | ||
| await execa('iptables', [ | ||
| '-t', 'filter', '-A', CHAIN_NAME, | ||
| '-p', 'tcp', '-d', apiProxyIp, '--dport', `${minPort}:${maxPort}`, | ||
| '-p', 'tcp', '-d', squidIp, '--dport', `${minPort}:${maxPort}`, | ||
| '-j', 'ACCEPT', | ||
| ]); | ||
| } |
There was a problem hiding this comment.
setupHostIptables now has a new code path to allow agent traffic to the auth-proxy ports (10000-10002) when apiProxyEnabled is true, but there’s no unit test asserting the expected iptables call(s) are added for that flag.
Add a focused test that calls setupHostIptables(..., true) and verifies the ACCEPT rule for the ${minPort}:${maxPort} range to squidIp.
Summary
containers/api-proxy/) into the Squid proxy container, reducing container count from 3→2 when--enable-api-proxyis usedArchitecture Change
Before (3 containers):
After (2 containers):
Security Review
Security review performed with CONDITIONAL PASS — all 3 required mitigations implemented:
Mitigations implemented:
proxyuser (not root)no-new-privileges, memory/PID limitsexec, properwait)Files Changed
containers/squid/— Added Node.js, server.js, package.json, updated Dockerfile & entrypointcontainers/api-proxy/— Deleted (merged into squid)src/docker-manager.ts— Removed api-proxy service, unified into squid servicesrc/host-iptables.ts— Updated apiProxy handling (boolean flag, not separate IP)src/cli-workflow.ts— Updated apiProxy configurationcontainers/agent/setup-iptables.sh— Removed AWF_API_PROXY_IP handlingscripts/ci/cleanup.sh— Removed api-proxy container referencesdocs/api-proxy-sidecar.md— Updated architecture documentationgh-aw Integration
No changes required in gh-aw CLI. The
--enable-api-proxyflag and all related CLI options work unchanged. Agent environment variables (*_BASE_URL) now point to squid IP (172.30.0.10) instead of separate proxy IP (172.30.0.30), which is transparent to agent code.Test plan
npm test)npm run build)npm run lint)--enable-api-proxyflag🤖 Generated with Claude Code