Skip to content

Commit c0dc5e7

Browse files
ClaudelpcoxCopilotCopilotgithub-advanced-security[bot]
authored
feat: add API proxy sidecar for secure LLM credential management (#751)
* Initial plan * feat: add envoy api proxy sidecar for credential management Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * docs: add comprehensive documentation for api proxy sidecar Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * docs: update main README with API proxy sidecar feature Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: implement all pr review suggestions - Add firewall exemption for API proxy sidecar (172.30.0.30) - Change healthcheck to Envoy admin endpoint (/ready) - Drop ALL capabilities for maximum security - Add host_rewrite_literal for proper Host headers - Update entrypoint.sh comments about disk persistence - Fix docs for conditional BASE_URL env vars - Add 19 comprehensive unit tests for API proxy - Update host-iptables tests for proxyIp Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * feat: replace Envoy with Node.js proxy that routes through Squid Replace Envoy-based API proxy with simpler Node.js implementation that: - Uses Express + http-proxy-middleware for HTTP proxying - Routes ALL traffic through Squid via HTTP_PROXY/HTTPS_PROXY env vars - Respects domain whitelisting enforced by Squid ACLs - Injects authentication headers (Authorization, x-api-key) - Healthcheck at /health endpoint instead of Envoy admin port - Much simpler implementation (~70 lines vs 166 lines) Implementation: - Created containers/api-proxy/ with Node.js proxy (Dockerfile, package.json, server.js, README.md) - Removed containers/envoy/ directory - Updated docker-manager.ts to configure HTTP_PROXY/HTTPS_PROXY pointing to Squid - Updated healthcheck to use http://localhost:10000/health - Changed image name from envoy to api-proxy in GHCR - Updated all documentation to reflect Node.js proxy and Squid routing architecture - Updated src/types.ts to mention Node.js proxy and Squid routing - Added test to verify HTTP_PROXY/HTTPS_PROXY configuration - All 168 tests pass Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * Potential fix for code scanning alert no. 613: Clear-text logging of sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 615: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 616: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix: remove API proxy firewall exemption for Squid routing Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * test: extract and test API proxy validation for coverage Extract validateApiProxyConfig() from the CLI action handler into a testable function and add 7 unit tests covering all branches: disabled, enabled without keys, with OpenAI key, with Anthropic key, with both. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: redact API keys in debug config logging Prevent clear-text logging of OPENAI_API_KEY and ANTHROPIC_API_KEY in the debug configuration output. CodeQL flagged this as a high severity security issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prevent API keys from flowing to logger (CodeQL) --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Jiaxiao (mossaka) Zhou <duibao55328@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a7a34ca commit c0dc5e7

14 files changed

Lines changed: 950 additions & 14 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ A network firewall for agentic workflows with domain whitelisting. This tool pro
1010
- **L7 Domain Whitelisting**: Control HTTP/HTTPS traffic at the application layer
1111
- **Host-Level Enforcement**: Uses iptables DOCKER-USER chain to enforce firewall on ALL containers
1212
- **Chroot Mode**: Transparent access to host binaries (Python, Node.js, Go) while maintaining network isolation
13+
- **API Proxy Sidecar**: Optional Node.js-based proxy for secure LLM API credential management (OpenAI Codex, Anthropic Claude) that routes through Squid
1314

1415
## Requirements
1516

@@ -33,6 +34,7 @@ The `--` separator divides firewall options from the command to run.
3334
- [Quick start](docs/quickstart.md) — install, verify, and run your first command
3435
- [Usage guide](docs/usage.md) — CLI flags, domain allowlists, examples
3536
- [Chroot mode](docs/chroot-mode.md) — use host binaries with network isolation
37+
- [API proxy sidecar](docs/api-proxy-sidecar.md) — secure credential management for LLM APIs
3638
- [SSL Bump](docs/ssl-bump.md) — HTTPS content inspection for URL path filtering
3739
- [GitHub Actions](docs/github_actions.md) — CI/CD integration and MCP server setup
3840
- [Environment variables](docs/environment.md) — passing environment variables to containers

containers/api-proxy/Dockerfile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Node.js API proxy for credential management
2+
# Routes through Squid to respect domain whitelisting
3+
FROM node:22-alpine
4+
5+
# Install curl for healthchecks
6+
RUN apk add --no-cache curl
7+
8+
# Create app directory
9+
WORKDIR /app
10+
11+
# Copy package files
12+
COPY package*.json ./
13+
14+
# Install dependencies
15+
RUN npm ci --only=production
16+
17+
# Copy application files
18+
COPY server.js ./
19+
20+
# Create non-root user
21+
RUN addgroup -S apiproxy && adduser -S apiproxy -G apiproxy
22+
23+
# Switch to non-root user
24+
USER apiproxy
25+
26+
# Expose ports
27+
# 10000 - OpenAI API proxy
28+
# 10001 - Anthropic API proxy
29+
EXPOSE 10000 10001
30+
31+
# Start the proxy server
32+
CMD ["node", "server.js"]

containers/api-proxy/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# AWF API Proxy Sidecar
2+
3+
Node.js-based API proxy that keeps LLM API credentials isolated from the agent container while routing all traffic through Squid to respect domain whitelisting.
4+
5+
## Architecture
6+
7+
```
8+
Agent Container (172.30.0.20)
9+
↓ HTTP request to api-proxy:10000
10+
API Proxy Sidecar (172.30.0.30)
11+
↓ Injects Authorization header
12+
↓ Routes via HTTP_PROXY (172.30.0.10:3128)
13+
Squid Proxy (172.30.0.10)
14+
↓ Domain whitelist enforcement
15+
↓ TLS connection
16+
api.openai.com or api.anthropic.com
17+
```
18+
19+
## Features
20+
21+
- **Credential Isolation**: API keys held only in sidecar, never exposed to agent
22+
- **Squid Routing**: All traffic routes through Squid via HTTP_PROXY/HTTPS_PROXY
23+
- **Domain Whitelisting**: Squid enforces ACL filtering on all egress traffic
24+
- **Header Injection**: Automatically adds Authorization and x-api-key headers
25+
- **Health Checks**: /health endpoint on both ports
26+
27+
## Ports
28+
29+
- **10000**: OpenAI API proxy (api.openai.com)
30+
- **10001**: Anthropic API proxy (api.anthropic.com)
31+
32+
## Environment Variables
33+
34+
Required (at least one):
35+
- `OPENAI_API_KEY` - OpenAI API key for authentication
36+
- `ANTHROPIC_API_KEY` - Anthropic API key for authentication
37+
38+
Set by AWF:
39+
- `HTTP_PROXY` - Squid proxy URL (http://172.30.0.10:3128)
40+
- `HTTPS_PROXY` - Squid proxy URL (http://172.30.0.10:3128)
41+
42+
## Security
43+
44+
- Runs as non-root user (apiproxy)
45+
- All capabilities dropped (cap_drop: ALL)
46+
- Memory limits (512MB)
47+
- Process limits (100 PIDs)
48+
- no-new-privileges security option
49+
50+
## Building
51+
52+
```bash
53+
cd containers/api-proxy
54+
docker build -t awf-api-proxy .
55+
```
56+
57+
## Testing
58+
59+
```bash
60+
# Start proxy with test key
61+
docker run -p 10000:10000 \
62+
-e OPENAI_API_KEY=sk-test123 \
63+
-e HTTP_PROXY=http://squid:3128 \
64+
-e HTTPS_PROXY=http://squid:3128 \
65+
awf-api-proxy
66+
67+
# Test health endpoint
68+
curl http://localhost:10000/health
69+
```
70+
71+
## Implementation Details
72+
73+
- Built on Node.js 22 Alpine Linux
74+
- Uses Express for HTTP server
75+
- Uses http-proxy-middleware for proxying
76+
- Naturally respects HTTP_PROXY/HTTPS_PROXY environment variables
77+
- Simpler and more maintainable than Envoy configuration

containers/api-proxy/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "awf-api-proxy",
3+
"version": "1.0.0",
4+
"description": "API proxy sidecar for AWF - routes LLM API requests through Squid while injecting authentication headers",
5+
"main": "server.js",
6+
"scripts": {
7+
"start": "node server.js"
8+
},
9+
"dependencies": {
10+
"express": "^4.18.2",
11+
"http-proxy-middleware": "^2.0.6"
12+
},
13+
"engines": {
14+
"node": ">=18.0.0"
15+
}
16+
}

containers/api-proxy/server.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* AWF API Proxy Sidecar
5+
*
6+
* Node.js-based proxy that:
7+
* 1. Keeps LLM API credentials isolated from agent container
8+
* 2. Routes all traffic through Squid via HTTP_PROXY/HTTPS_PROXY
9+
* 3. Injects authentication headers (Authorization, x-api-key)
10+
* 4. Respects domain whitelisting enforced by Squid
11+
*/
12+
13+
const express = require('express');
14+
const { createProxyMiddleware } = require('http-proxy-middleware');
15+
16+
// Read API keys from environment (set by docker-compose)
17+
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
18+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
19+
20+
// Squid proxy configuration (set via HTTP_PROXY/HTTPS_PROXY in docker-compose)
21+
const HTTP_PROXY = process.env.HTTP_PROXY;
22+
const HTTPS_PROXY = process.env.HTTPS_PROXY;
23+
24+
console.log('[API Proxy] Starting AWF API proxy sidecar...');
25+
console.log(`[API Proxy] HTTP_PROXY: ${HTTP_PROXY}`);
26+
console.log(`[API Proxy] HTTPS_PROXY: ${HTTPS_PROXY}`);
27+
if (OPENAI_API_KEY) {
28+
console.log('[API Proxy] OpenAI API key configured');
29+
}
30+
if (ANTHROPIC_API_KEY) {
31+
console.log('[API Proxy] Anthropic API key configured');
32+
}
33+
34+
// Create Express app
35+
const app = express();
36+
37+
// Health check endpoint
38+
app.get('/health', (req, res) => {
39+
res.status(200).json({
40+
status: 'healthy',
41+
service: 'awf-api-proxy',
42+
squid_proxy: HTTP_PROXY || 'not configured',
43+
providers: {
44+
openai: !!OPENAI_API_KEY,
45+
anthropic: !!ANTHROPIC_API_KEY
46+
}
47+
});
48+
});
49+
50+
// OpenAI API proxy (port 10000)
51+
if (OPENAI_API_KEY) {
52+
app.use(createProxyMiddleware({
53+
target: 'https://api.openai.com',
54+
changeOrigin: true,
55+
secure: true,
56+
onProxyReq: (proxyReq, req, res) => {
57+
// Inject Authorization header
58+
proxyReq.setHeader('Authorization', `Bearer ${OPENAI_API_KEY}`);
59+
console.log(`[OpenAI Proxy] ${req.method} ${req.url}`);
60+
},
61+
onError: (err, req, res) => {
62+
console.error(`[OpenAI Proxy] Error: ${err.message}`);
63+
res.status(502).json({ error: 'Proxy error', message: err.message });
64+
}
65+
}));
66+
67+
app.listen(10000, '0.0.0.0', () => {
68+
console.log('[API Proxy] OpenAI proxy listening on port 10000');
69+
console.log('[API Proxy] Routing through Squid to api.openai.com');
70+
});
71+
}
72+
73+
// Anthropic API proxy (port 10001)
74+
if (ANTHROPIC_API_KEY) {
75+
const anthropicApp = express();
76+
77+
anthropicApp.get('/health', (req, res) => {
78+
res.status(200).json({ status: 'healthy', service: 'anthropic-proxy' });
79+
});
80+
81+
anthropicApp.use(createProxyMiddleware({
82+
target: 'https://api.anthropic.com',
83+
changeOrigin: true,
84+
secure: true,
85+
onProxyReq: (proxyReq, req, res) => {
86+
// Inject Anthropic authentication headers
87+
proxyReq.setHeader('x-api-key', ANTHROPIC_API_KEY);
88+
proxyReq.setHeader('anthropic-version', '2023-06-01');
89+
console.log(`[Anthropic Proxy] ${req.method} ${req.url}`);
90+
},
91+
onError: (err, req, res) => {
92+
console.error(`[Anthropic Proxy] Error: ${err.message}`);
93+
res.status(502).json({ error: 'Proxy error', message: err.message });
94+
}
95+
}));
96+
97+
anthropicApp.listen(10001, '0.0.0.0', () => {
98+
console.log('[API Proxy] Anthropic proxy listening on port 10001');
99+
console.log('[API Proxy] Routing through Squid to api.anthropic.com');
100+
});
101+
}
102+
103+
// Graceful shutdown
104+
process.on('SIGTERM', () => {
105+
console.log('[API Proxy] Received SIGTERM, shutting down gracefully...');
106+
process.exit(0);
107+
});
108+
109+
process.on('SIGINT', () => {
110+
console.log('[API Proxy] Received SIGINT, shutting down gracefully...');
111+
process.exit(0);
112+
});

0 commit comments

Comments
 (0)