Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions containers/api-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ RUN addgroup -S apiproxy && adduser -S apiproxy -G apiproxy
USER apiproxy

# Expose ports
# 10000 - OpenAI API proxy
# 10000 - OpenAI API proxy (also serves as health check endpoint)
# 10001 - Anthropic API proxy
EXPOSE 10000 10001
# 10002 - GitHub Copilot API proxy
EXPOSE 10000 10001 10002

# Redirect stdout/stderr to log file for persistence
# Use shell form to enable redirection and tee for both file and console
Expand Down
60 changes: 55 additions & 5 deletions docs/api-proxy-sidecar.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,68 @@ sudo awf --enable-api-proxy \

## Environment variables

When API keys are provided, AWF sets these environment variables in the agent container:
AWF manages environment variables differently across the three containers (squid, api-proxy, agent) to ensure secure credential isolation.

### Squid container

The Squid proxy container runs with minimal environment variables:

| Variable | Value | Description |
|----------|-------|-------------|
| `HTTP_PROXY` | Not set | Squid is the proxy, not a client |
| `HTTPS_PROXY` | Not set | Squid is the proxy, not a client |

### API proxy container

The API proxy sidecar receives **real credentials** and routing configuration:

| Variable | Value | When set | Description |
|----------|-------|----------|-------------|
| `OPENAI_API_KEY` | Real API key | `--enable-api-proxy` and env set | OpenAI API key (injected into requests) |
| `ANTHROPIC_API_KEY` | Real API key | `--enable-api-proxy` and env set | Anthropic API key (injected into requests) |
| `COPILOT_GITHUB_TOKEN` | Real token | `--enable-api-proxy` and env set | GitHub Copilot token (injected into requests) |
| `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering |
| `HTTPS_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering |

:::danger[Real credentials in api-proxy]
The api-proxy container holds **real, unredacted credentials**. These are used to authenticate requests to LLM providers. This container is isolated from the agent and has all capabilities dropped for security.
:::

### Agent container

The agent container receives **redacted placeholders** and proxy URLs:

| Variable | Value | When set | Description |
|----------|-------|----------|-------------|
| `OPENAI_BASE_URL` | `http://172.30.0.30:10000/v1` | `OPENAI_API_KEY` is set | OpenAI API proxy endpoint |
| `ANTHROPIC_BASE_URL` | `http://172.30.0.30:10001` | `ANTHROPIC_API_KEY` is set | Anthropic API proxy endpoint |
| `OPENAI_BASE_URL` | `http://172.30.0.30:10000/v1` | `OPENAI_API_KEY` provided to host | Redirects OpenAI SDK to proxy |
| `ANTHROPIC_BASE_URL` | `http://172.30.0.30:10001` | `ANTHROPIC_API_KEY` provided to host | Redirects Anthropic SDK to proxy |
| `ANTHROPIC_AUTH_TOKEN` | `placeholder-token-for-credential-isolation` | `ANTHROPIC_API_KEY` provided to host | Placeholder token (real auth via BASE_URL) |
| `CLAUDE_CODE_API_KEY_HELPER` | `/usr/local/bin/get-claude-key.sh` | `ANTHROPIC_API_KEY` provided to host | Helper script for Claude Code CLI |
| `COPILOT_API_URL` | `http://172.30.0.30:10002` | `COPILOT_GITHUB_TOKEN` provided to host | Redirects Copilot CLI to proxy |
| `COPILOT_TOKEN` | `placeholder-token-for-credential-isolation` | `COPILOT_GITHUB_TOKEN` provided to host | Placeholder token (real auth via API_URL) |
| `COPILOT_GITHUB_TOKEN` | `placeholder-token-for-credential-isolation` | `COPILOT_GITHUB_TOKEN` provided to host | Placeholder token protected by one-shot-token |
| `OPENAI_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) |
| `ANTHROPIC_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) |
| `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid proxy |
| `HTTPS_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid proxy |
| `NO_PROXY` | `localhost,127.0.0.1,172.30.0.30` | `--enable-api-proxy` | Bypass proxy for localhost and api-proxy |
| `AWF_API_PROXY_IP` | `172.30.0.30` | `--enable-api-proxy` | Used by iptables setup script |
| `AWF_ONE_SHOT_TOKENS` | `COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,...` | Always | Tokens protected by one-shot-token library |

:::tip[Placeholder tokens]
Token variables in the agent are set to `placeholder-token-for-credential-isolation` instead of real values. This ensures:
- Agent code cannot exfiltrate credentials
- CLI tools that check for token presence still work
- Real authentication happens via the `*_BASE_URL` or `*_API_URL` environment variables
- The one-shot-token library protects placeholder values from being read more than once
:::

These are standard environment variables recognized by:
These environment variables are recognized by:
- OpenAI Python SDK (`openai`)
- OpenAI Node.js SDK (`openai`)
- Anthropic Python SDK (`anthropic`)
- Anthropic TypeScript SDK (`@anthropic-ai/sdk`)
- GitHub Copilot CLI (`@github/copilot`)
- Codex CLI
- Claude Code CLI

Expand Down Expand Up @@ -218,7 +268,7 @@ The sidecar container:
- **Image**: `ghcr.io/github/gh-aw-firewall/api-proxy:latest`
- **Base**: `node:22-alpine`
- **Network**: `awf-net` at `172.30.0.30`
- **Ports**: 10000 (OpenAI), 10001 (Anthropic)
- **Ports**: 10000 (OpenAI), 10001 (Anthropic), 10002 (GitHub Copilot)
- **Proxy**: Routes via Squid at `http://172.30.0.10:3128`

### Health check
Expand Down
16 changes: 8 additions & 8 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from 'path';
import * as os from 'os';
import * as yaml from 'js-yaml';
import execa from 'execa';
import { DockerComposeConfig, WrapperConfig, BlockedTarget } from './types';
import { DockerComposeConfig, WrapperConfig, BlockedTarget, API_PROXY_PORTS, API_PROXY_HEALTH_PORT } from './types';
import { logger } from './logger';
import { generateSquidConfig } from './squid-config';
import { generateSessionCa, initSslDb, CaFiles, parseUrlPatterns } from './ssl-bump';
Expand Down Expand Up @@ -968,7 +968,7 @@ export function generateDockerCompose(
HTTPS_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`,
},
healthcheck: {
test: ['CMD', 'curl', '-f', 'http://localhost:10000/health'],
test: ['CMD', 'curl', '-f', `http://localhost:${API_PROXY_HEALTH_PORT}/health`],
interval: '5s',
timeout: '3s',
retries: 5,
Expand Down Expand Up @@ -1009,12 +1009,12 @@ export function generateDockerCompose(
// container names in chroot mode
environment.AWF_API_PROXY_IP = networkConfig.proxyIp;
if (config.openaiApiKey) {
environment.OPENAI_BASE_URL = `http://${networkConfig.proxyIp}:10000/v1`;
logger.debug(`OpenAI API will be proxied through sidecar at http://${networkConfig.proxyIp}:10000/v1`);
environment.OPENAI_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.OPENAI}/v1`;
logger.debug(`OpenAI API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.OPENAI}/v1`);
}
if (config.anthropicApiKey) {
environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:10001`;
logger.debug(`Anthropic API will be proxied through sidecar at http://${networkConfig.proxyIp}:10001`);
environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.ANTHROPIC}`;
logger.debug(`Anthropic API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.ANTHROPIC}`);

// Set placeholder token for Claude Code CLI compatibility
// Real authentication happens via ANTHROPIC_BASE_URL pointing to api-proxy
Expand All @@ -1027,8 +1027,8 @@ export function generateDockerCompose(
logger.debug('Claude Code API key helper configured: /usr/local/bin/get-claude-key.sh');
}
if (config.copilotGithubToken) {
environment.COPILOT_API_URL = `http://${networkConfig.proxyIp}:10002`;
logger.debug(`GitHub Copilot API will be proxied through sidecar at http://${networkConfig.proxyIp}:10002`);
environment.COPILOT_API_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.COPILOT}`;
logger.debug(`GitHub Copilot API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.COPILOT}`);

// Set placeholder token for GitHub Copilot CLI compatibility
// Real authentication happens via COPILOT_API_URL pointing to api-proxy
Expand Down
9 changes: 6 additions & 3 deletions src/host-iptables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import execa from 'execa';
import { logger } from './logger';
import { isIPv6 } from 'net';
import { API_PROXY_PORTS } from './types';

const NETWORK_NAME = 'awf-net';
const CHAIN_NAME = 'FW_WRAPPER';
Expand Down Expand Up @@ -442,13 +443,15 @@ export async function setupHostIptables(squidIp: string, squidPort: number, dnsS
]);

// 5b. Allow traffic to API proxy sidecar (when enabled)
// Only allow ports 10000 (OpenAI) and 10001 (Anthropic) — nothing else.
// Allow all API proxy ports (OpenAI, Anthropic, GitHub Copilot).
// The sidecar itself routes through Squid, so domain whitelisting is still enforced.
if (apiProxyIp) {
logger.debug(`Allowing traffic to API proxy sidecar at ${apiProxyIp}:10000-10001`);
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}`);
await execa('iptables', [
'-t', 'filter', '-A', CHAIN_NAME,
'-p', 'tcp', '-d', apiProxyIp, '--dport', '10000:10001',
'-p', 'tcp', '-d', apiProxyIp, '--dport', `${minPort}:${maxPort}`,
'-j', 'ACCEPT',
]);
}
Expand Down
45 changes: 42 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@
* Configuration types for the agentic workflow firewall
*/

/**
* API Proxy port configuration
*
* These ports are used by the api-proxy sidecar container to expose
* authentication-injecting proxies for different LLM providers.
*
* All ports must be allowed in:
* - containers/api-proxy/Dockerfile (EXPOSE directive)
* - src/host-iptables.ts (firewall rules)
* - containers/agent/setup-iptables.sh (NAT rules)
*/
export const API_PROXY_PORTS = {
/**
* OpenAI API proxy port
* Also serves as the health check endpoint for Docker healthcheck
* @see containers/api-proxy/server.js
*/
OPENAI: 10000,

/**
* Anthropic (Claude) API proxy port
* @see containers/api-proxy/server.js
*/
ANTHROPIC: 10001,

/**
* GitHub Copilot API proxy port
* @see containers/api-proxy/server.js
*/
COPILOT: 10002,
} as const;

/**
* Health check port for the API proxy sidecar
* Always uses the OpenAI port (10000) for Docker healthcheck
*/
export const API_PROXY_HEALTH_PORT = API_PROXY_PORTS.OPENAI;

/**
* Main configuration interface for the firewall wrapper
*
Expand Down Expand Up @@ -391,9 +429,9 @@ export interface WrapperConfig {
* - Proxies requests to LLM providers
*
* The sidecar exposes three endpoints accessible from the agent container:
* - http://api-proxy:10000 - OpenAI API proxy (for Codex)
* - http://api-proxy:10001 - Anthropic API proxy (for Claude)
* - http://api-proxy:10002 - GitHub Copilot API proxy
* - http://api-proxy:10000 - OpenAI API proxy (for Codex) {@link API_PROXY_PORTS.OPENAI}
* - http://api-proxy:10001 - Anthropic API proxy (for Claude) {@link API_PROXY_PORTS.ANTHROPIC}
* - http://api-proxy:10002 - GitHub Copilot API proxy {@link API_PROXY_PORTS.COPILOT}
*
* When the corresponding API key is provided, the following environment
* variables are set in the agent container:
Expand All @@ -416,6 +454,7 @@ export interface WrapperConfig {
* export COPILOT_GITHUB_TOKEN="ghp_..."
* awf --enable-api-proxy --allow-domains api.openai.com,api.anthropic.com,api.githubcopilot.com -- command
* ```
* @see API_PROXY_PORTS for port configuration
*/
enableApiProxy?: boolean;

Expand Down
Loading