Skip to content

Commit 3fa55d3

Browse files
Claudelpcoxclaude
authored
feat: proxy claude api calls to secure auth token (#849)
* Initial plan * docs: create comprehensive authentication architecture guide Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * docs: link to authentication architecture guide Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * docs: clarify that Codex/OpenAI uses same credential isolation as Claude Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: exclude OpenAI/Codex keys from agent when api-proxy enabled When --env-all is used (as in smoke-codex test), OPENAI_API_KEY and CODEX_API_KEY were being passed to the agent container, bypassing the credential isolation provided by the api-proxy sidecar. Changes: - Add OPENAI_API_KEY, OPENAI_KEY, CODEX_API_KEY to EXCLUDED_ENV_VARS when enableApiProxy is true - Add similar exclusion logic for non-envAll case (selective env passing) - API keys remain correctly passed to api-proxy container for credential injection - Add 4 new tests verifying keys are excluded from agent with/without envAll This ensures both OpenAI/Codex and Anthropic/Claude credentials are properly isolated in the api-proxy sidecar, matching the documented architecture. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: enable api-proxy for smoke-codex workflow Removes CODEX_API_KEY and OPENAI_API_KEY from agent environment by: - Adding --enable-api-proxy flag to AWF command - Removing API keys from env block in smoke-codex.lock.yml This ensures Codex uses the same credential isolation architecture as Claude, where API keys are held exclusively in the api-proxy sidecar container and never exposed to the agent execution environment. With this change: - Agent receives OPENAI_BASE_URL pointing to api-proxy:10000 - API keys passed to api-proxy container which injects auth headers - One-shot-token library no longer detects keys in agent environment Resolves feedback from comment 3901985142. * feat: add api-proxy health checks for credential isolation Adds pre-flight connectivity and credential isolation checks before agent execution. Health check verifies: - API keys NOT present in agent environment (ANTHROPIC_API_KEY, CLAUDE_API_KEY, OPENAI_API_KEY, CODEX_API_KEY, OPENAI_KEY) - API proxy reachable via ANTHROPIC_BASE_URL and OPENAI_BASE_URL - TCP connectivity test with 5 second timeout Implementation: - Created api-proxy-health-check.sh with detailed credential checks - Integrated into agent entrypoint.sh (runs after iptables, before agent) - Added to Dockerfile build process - Fails fast if credential isolation broken or proxy unreachable This ensures both Claude and Codex agents cannot access API keys directly, confirming the credential isolation architecture is working as designed. * fix: restore API keys to workflow env for AWF CLI The API keys must be present in the workflow step's env block so the AWF CLI process (running on the host) can read them via process.env.OPENAI_API_KEY. The AWF CLI needs these environment variables to: 1. Detect that api-proxy should be enabled (checks for openaiApiKey/anthropicApiKey) 2. Pass API keys to the api-proxy container 3. Set OPENAI_BASE_URL and ANTHROPIC_BASE_URL for the agent container The credential isolation still works because: - API keys are in the EXCLUDED_ENV_VARS set when enableApiProxy is true - Keys are excluded from the agent container environment (commit abedf83) - Keys only go to the api-proxy sidecar container - Agent receives only BASE_URL environment variables This fixes the issue where OPENAI_BASE_URL was not being set because config.openaiApiKey was undefined (AWF CLI couldn't read the env var). * feat: add claude code api key helper for credential isolation This commit implements dynamic API key retrieval for Claude Code using a helper script following the LLM Gateway pattern, ensuring credential isolation where only the api-proxy container has access to real tokens. Changes: - Created /containers/agent/get-claude-key.sh: Helper script that outputs a placeholder API key (sk-ant-placeholder-key-for-credential-isolation) - Updated containers/agent/Dockerfile: Added get-claude-key.sh to container image and made it executable - Modified .github/workflows/smoke-claude.lock.yml: Configured Claude Code to use the apiKeyHelper by creating ~/.claude/config.json and unsetting ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN environment variables before running the claude command The api-proxy intercepts requests and injects the real ANTHROPIC_API_KEY, so the placeholder key never reaches the actual Anthropic API. This ensures: 1. Claude Code agent never has access to the real API key 2. Only api-proxy container holds the real credentials 3. Health checks verify keys are NOT in agent environment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add logging to api key helper and api-proxy for observability Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: add CLAUDE_CODE_API_KEY_HELPER env var for helper detection Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: unset HTTP_PROXY for claude to use api-proxy as gateway Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * feat(logs): add api-proxy log persistence and preservation - Add volume mount for api-proxy logs in docker-compose - Redirect api-proxy stdout/stderr to /var/log/api-proxy/api-proxy.log - Create api-proxy logs directory during writeConfigs() - Preserve api-proxy logs after cleanup (similar to squid logs) - Support both proxyLogsDir (workflow mode) and default mode - When proxyLogsDir is set, write logs to sibling directory - When proxyLogsDir is not set, move logs to /tmp/api-proxy-logs-<timestamp> - Fix permissions on preserved logs for GitHub Actions artifact upload This ensures api-proxy logs are accessible after smoke-claude and smoke-codex workflows finish, as requested in comment 3902135209. * fix: add claude code api key helper validation and ttl config - Add validation in entrypoint.sh to verify apiKeyHelper is in config file - Check config file exists at ~/.claude/config.json - Verify apiKeyHelper field matches CLAUDE_CODE_API_KEY_HELPER env var - Exit with error if validation fails to prevent using wrong credentials - Add CLAUDE_CODE_API_KEY_HELPER_TTL_MS=3600000 to smoke-claude workflow - Ensures Claude Code properly detects and uses the API key helper script Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: make claude code api key helper validation conditional The validation was failing in smoke-claude workflow because: - Entrypoint validation runs before the user command executes - The config file (~/.claude/config.json) is created by the user command - Previous implementation required config file to exist, causing exit 1 Changes: - Made validation conditional: only validate if config file exists - If config file doesn't exist, log informative message and continue - This allows user commands to create the config file after entrypoint runs - Validation still occurs when config file exists (e.g., mounted from host) Fixes comment 3902171070 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add post-step to display final claude code config Added diagnostic step to cat both ~/.claude/config.json and ~/.claude.json after Claude Code execution completes. This helps verify that config updates preserve previous values and shows the final state for debugging. The step runs with if: always() to ensure config is shown even if tests fail. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: set CLAUDE_CODE_API_KEY_HELPER env var for credential isolation (#851) * Initial plan * fix: set CLAUDE_CODE_API_KEY_HELPER env var for credential isolation When api-proxy is enabled with an Anthropic key, set the CLAUDE_CODE_API_KEY_HELPER environment variable to point to the get-claude-key.sh script. This ensures Claude Code CLI properly uses the API key helper for credential isolation. Previously, only ANTHROPIC_BASE_URL was set, but Claude Code requires either a config file with apiKeyHelper or the environment variable to actually use the helper script. Without this, Claude Code would not read the config and authentication would fail. This fix: - Sets CLAUDE_CODE_API_KEY_HELPER=/usr/local/bin/get-claude-key.sh when api-proxy is enabled with Anthropic key - Adds comprehensive tests for the new environment variable - Updates type documentation to reflect the new env var Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: change claude code config path to ~/.claude.json Changed the config file path from ~/.claude/config.json to ~/.claude.json in both the entrypoint validation and the smoke-claude post-step diagnostic. This aligns with where Claude Code actually writes its config file (as shown in the debug logs). The post-step now checks ~/.claude.json first and shows ~/.claude/config.json as legacy for backwards compatibility. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: create ~/.claude.json with apiKeyHelper before claude starts Changed entrypoint.sh to CREATE the ~/.claude.json config file with apiKeyHelper configuration if it doesn't exist, rather than just skipping validation. This fixes authentication errors where Claude Code couldn't find the apiKeyHelper because the config file didn't exist yet when Claude Code needed to authenticate. The file is now created during container initialization, before the user command runs. The file is created with mode 600 for security and contains: {"apiKeyHelper":"/usr/local/bin/get-claude-key.sh"} Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: mount ~/.claude.json for chroot mode accessibility Fixed authentication error in chroot mode where Claude Code couldn't find ~/.claude.json because it was created in the container filesystem instead of the mounted /host path. Changes: - Mount ~/.claude.json file explicitly in docker-manager (create if missing) - Update entrypoint to write to /host$HOME/.claude.json in chroot mode - File now accessible both before and after chroot transition This fixes the ENOENT error: "no such file or directory, lstat '/home/runner/.claude.json'" Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: handle empty ~/.claude.json created by docker-manager The entrypoint validation was failing when docker-manager created an empty {} file for mounting. Now the entrypoint handles three cases: 1. File exists with apiKeyHelper → validate it matches 2. File exists without apiKeyHelper → write it (empty {} from mount) 3. File doesn't exist → create it with apiKeyHelper This fixes the validation error where entrypoint rejected the empty file instead of populating it with the apiKeyHelper configuration. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: use 0o666 mode for ~/.claude.json to fix permissions (#852) * Initial plan * fix: use 0o666 mode for ~/.claude.json to fix permissions Container root writes to this file, changing ownership. Using 0o666 ensures host user can still read it after modifications. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: use 666 permissions for ~/.claude.json to allow host user writes Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix(ci): add api-proxy logs to smoke-claude artifacts Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * feat: add ANTHROPIC_AUTH_TOKEN placeholder to agent environment This commit adds ANTHROPIC_AUTH_TOKEN with a placeholder value to the agent environment when api-proxy is enabled with Anthropic credentials. This ensures Claude Code CLI compatibility while maintaining credential isolation (real auth happens via ANTHROPIC_BASE_URL). Changes: - src/docker-manager.ts: Set ANTHROPIC_AUTH_TOKEN to placeholder when anthropicApiKey is configured with api-proxy - src/docker-manager.test.ts: Updated all Anthropic test cases to verify ANTHROPIC_AUTH_TOKEN is set to placeholder value - containers/agent/api-proxy-health-check.sh: Added validation that ANTHROPIC_AUTH_TOKEN (if present) is the placeholder value, not a real token - tests/integration/api-proxy.test.ts: Added integration test to verify ANTHROPIC_AUTH_TOKEN is set to placeholder in agent container Security model: - Real ANTHROPIC_API_KEY stays in api-proxy container only - Agent gets placeholder ANTHROPIC_AUTH_TOKEN for CLI compatibility - Agent gets ANTHROPIC_BASE_URL pointing to api-proxy (http://172.30.0.30:10001) - Real authentication happens when api-proxy injects the real key - Health checks verify no real credentials leak to agent environment Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: restore CODEX_API_KEY env var passing for codex agent compatibility This commit restores the behavior where CODEX_API_KEY is passed directly to the agent environment, even when api-proxy is enabled. This ensures Codex agent compatibility while keeping Claude Code authentication unchanged (using api-proxy pattern with placeholder tokens). Changes: - src/docker-manager.ts: Remove CODEX_API_KEY from EXCLUDED_ENV_VARS when api-proxy is enabled (line 334 removed) - src/docker-manager.ts: Remove api-proxy check for CODEX_API_KEY, allowing it to be passed unconditionally (line 423) - src/docker-manager.test.ts: Update test to expect CODEX_API_KEY to be present in agent environment when api-proxy is enabled Authentication model: - Claude/Anthropic: Uses api-proxy pattern (no real keys in agent, placeholder ANTHROPIC_AUTH_TOKEN, BASE_URL pointing to api-proxy) - Codex: Uses direct credential passing (CODEX_API_KEY in agent env) - OpenAI: Uses api-proxy pattern (excluded from agent when enabled) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: disable CODEX_API_KEY health check for direct credential passing The health check was failing because CODEX_API_KEY is now intentionally passed to the agent environment for Codex compatibility. This commit comments out the CODEX_API_KEY validation in the health check while keeping the OPENAI_API_KEY and OPENAI_KEY checks intact. Changes: - containers/agent/api-proxy-health-check.sh: Remove CODEX_API_KEY from the credential isolation check (line 68) - containers/agent/api-proxy-health-check.sh: Comment out CODEX_API_KEY error message (line 72) - Added explanatory comments noting that CODEX_API_KEY is intentionally passed through for Codex agent compatibility Health check now validates: - ✓ ANTHROPIC_API_KEY still excluded (Claude Code uses api-proxy) - ✓ OPENAI_API_KEY still excluded (uses api-proxy when enabled) - ✓ CODEX_API_KEY validation disabled (Codex uses direct credentials) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: disable OPENAI_BASE_URL for codex agent (temporary) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * docs: update OPENAI_BASE_URL to include /v1 path suffix Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- 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: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9da1ba7 commit 3fa55d3

18 files changed

+1195
-149
lines changed

.github/workflows/smoke-claude.lock.yml

Lines changed: 146 additions & 124 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/smoke-claude.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@ safe-outputs:
4949
run-failure: "💫 **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges..."
5050
timeout-minutes: 10
5151
post-steps:
52+
- name: Show final Claude Code config
53+
if: always()
54+
run: |
55+
echo "=== Final Claude Code Config ==="
56+
if [ -f ~/.claude.json ]; then
57+
echo "File: ~/.claude.json"
58+
cat ~/.claude.json
59+
else
60+
echo "~/.claude.json not found"
61+
fi
62+
if [ -f ~/.claude/config.json ]; then
63+
echo ""
64+
echo "File: ~/.claude/config.json (legacy)"
65+
cat ~/.claude/config.json
66+
else
67+
echo "~/.claude/config.json not found"
68+
fi
5269
- name: Validate safe outputs were invoked
5370
run: |
5471
OUTPUTS_FILE="${GH_AW_SAFE_OUTPUTS:-/opt/gh-aw/safeoutputs/outputs.jsonl}"

.github/workflows/smoke-codex.lock.yml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The `--` separator divides firewall options from the command to run.
3535
- [Usage guide](docs/usage.md) — CLI flags, domain allowlists, examples
3636
- [Chroot mode](docs/chroot-mode.md) — use host binaries with network isolation
3737
- [API proxy sidecar](docs/api-proxy-sidecar.md) — secure credential management for LLM APIs
38+
- [Authentication architecture](docs/authentication-architecture.md) — deep dive into token handling and credential isolation
3839
- [SSL Bump](docs/ssl-bump.md) — HTTPS content inspection for URL path filtering
3940
- [GitHub Actions](docs/github_actions.md) — CI/CD integration and MCP server setup
4041
- [Environment variables](docs/environment.md) — passing environment variables to containers

containers/agent/Dockerfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,13 @@ RUN if ! getent group awfuser >/dev/null 2>&1; then \
6262
mkdir -p /home/awfuser/.copilot/logs && \
6363
chown -R awfuser:awfuser /home/awfuser
6464

65-
# Copy iptables setup script and PID logger
65+
# Copy iptables setup script, PID logger, API proxy health check, and Claude key helper
6666
COPY setup-iptables.sh /usr/local/bin/setup-iptables.sh
6767
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
6868
COPY pid-logger.sh /usr/local/bin/pid-logger.sh
69-
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh
69+
COPY api-proxy-health-check.sh /usr/local/bin/api-proxy-health-check.sh
70+
COPY get-claude-key.sh /usr/local/bin/get-claude-key.sh
71+
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh
7072

7173
# Copy pre-built one-shot-token library from rust-builder stage
7274
# This prevents tokens from being read multiple times (e.g., by malicious code)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/bin/bash
2+
# api-proxy-health-check.sh
3+
# Pre-flight health check to verify API proxy credential isolation
4+
# This script ensures:
5+
# 1. API keys are NOT present in agent environment (credential isolation working)
6+
# 2. API proxy is reachable and healthy (connectivity established)
7+
#
8+
# Usage: source this script before running agent commands
9+
# Returns: 0 if checks pass, 1 if checks fail (prevents agent from running)
10+
11+
set -e
12+
13+
echo "[health-check] API Proxy Pre-flight Check"
14+
echo "[health-check] =========================================="
15+
16+
# Track if any API proxy is configured
17+
API_PROXY_CONFIGURED=false
18+
19+
# Check Claude/Anthropic configuration
20+
if [ -n "$ANTHROPIC_BASE_URL" ]; then
21+
API_PROXY_CONFIGURED=true
22+
echo "[health-check] Checking Anthropic API proxy configuration..."
23+
24+
# Verify credentials are NOT in agent environment
25+
if [ -n "$ANTHROPIC_API_KEY" ] || [ -n "$CLAUDE_API_KEY" ]; then
26+
echo "[health-check][ERROR] Anthropic API key found in agent environment!"
27+
echo "[health-check][ERROR] Credential isolation failed - keys should only be in api-proxy container"
28+
echo "[health-check][ERROR] ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:+<present>}"
29+
echo "[health-check][ERROR] CLAUDE_API_KEY=${CLAUDE_API_KEY:+<present>}"
30+
exit 1
31+
fi
32+
echo "[health-check] ✓ Anthropic credentials NOT in agent environment (correct)"
33+
34+
# Verify ANTHROPIC_AUTH_TOKEN is placeholder (if present)
35+
if [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then
36+
if [ "$ANTHROPIC_AUTH_TOKEN" != "placeholder-token-for-credential-isolation" ]; then
37+
echo "[health-check][ERROR] ANTHROPIC_AUTH_TOKEN contains non-placeholder value!"
38+
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
39+
exit 1
40+
fi
41+
echo "[health-check] ✓ ANTHROPIC_AUTH_TOKEN is placeholder value (correct)"
42+
fi
43+
44+
# Perform health check using BASE_URL
45+
echo "[health-check] Testing connectivity to Anthropic API proxy at $ANTHROPIC_BASE_URL..."
46+
47+
# Extract host and port from BASE_URL (format: http://IP:PORT)
48+
PROXY_HOST=$(echo "$ANTHROPIC_BASE_URL" | sed -E 's|^https?://([^:]+):.*|\1|')
49+
PROXY_PORT=$(echo "$ANTHROPIC_BASE_URL" | sed -E 's|^https?://[^:]+:([0-9]+).*|\1|')
50+
51+
# Test TCP connectivity with timeout
52+
if timeout 5 bash -c "cat < /dev/null > /dev/tcp/$PROXY_HOST/$PROXY_PORT" 2>/dev/null; then
53+
echo "[health-check] ✓ Anthropic API proxy is reachable at $ANTHROPIC_BASE_URL"
54+
else
55+
echo "[health-check][ERROR] Cannot connect to Anthropic API proxy at $ANTHROPIC_BASE_URL"
56+
echo "[health-check][ERROR] Proxy may not be running or network is blocked"
57+
exit 1
58+
fi
59+
fi
60+
61+
# Check OpenAI/Codex configuration
62+
if [ -n "$OPENAI_BASE_URL" ]; then
63+
API_PROXY_CONFIGURED=true
64+
echo "[health-check] Checking OpenAI API proxy configuration..."
65+
66+
# Verify credentials are NOT in agent environment
67+
# Note: CODEX_API_KEY check is temporarily disabled - Codex receives credentials directly
68+
if [ -n "$OPENAI_API_KEY" ] || [ -n "$OPENAI_KEY" ]; then
69+
echo "[health-check][ERROR] OpenAI API key found in agent environment!"
70+
echo "[health-check][ERROR] Credential isolation failed - keys should only be in api-proxy container"
71+
echo "[health-check][ERROR] OPENAI_API_KEY=${OPENAI_API_KEY:+<present>}"
72+
# echo "[health-check][ERROR] CODEX_API_KEY=${CODEX_API_KEY:+<present>}" # Temporarily disabled - Codex uses direct credentials
73+
echo "[health-check][ERROR] OPENAI_KEY=${OPENAI_KEY:+<present>}"
74+
exit 1
75+
fi
76+
echo "[health-check] ✓ OpenAI credentials NOT in agent environment (correct)"
77+
# Note: CODEX_API_KEY is intentionally passed through for Codex agent compatibility
78+
79+
# Perform health check using BASE_URL
80+
echo "[health-check] Testing connectivity to OpenAI API proxy at $OPENAI_BASE_URL..."
81+
82+
# Extract host and port from BASE_URL (format: http://IP:PORT)
83+
PROXY_HOST=$(echo "$OPENAI_BASE_URL" | sed -E 's|^https?://([^:]+):.*|\1|')
84+
PROXY_PORT=$(echo "$OPENAI_BASE_URL" | sed -E 's|^https?://[^:]+:([0-9]+).*|\1|')
85+
86+
# Test TCP connectivity with timeout
87+
if timeout 5 bash -c "cat < /dev/null > /dev/tcp/$PROXY_HOST/$PROXY_PORT" 2>/dev/null; then
88+
echo "[health-check] ✓ OpenAI API proxy is reachable at $OPENAI_BASE_URL"
89+
else
90+
echo "[health-check][ERROR] Cannot connect to OpenAI API proxy at $OPENAI_BASE_URL"
91+
echo "[health-check][ERROR] Proxy may not be running or network is blocked"
92+
exit 1
93+
fi
94+
fi
95+
96+
# Summary
97+
if [ "$API_PROXY_CONFIGURED" = "true" ]; then
98+
echo "[health-check] =========================================="
99+
echo "[health-check] ✓ All API proxy health checks passed"
100+
echo "[health-check] ✓ Credential isolation verified"
101+
echo "[health-check] ✓ Connectivity established"
102+
echo "[health-check] =========================================="
103+
else
104+
echo "[health-check] No API proxy configured (ANTHROPIC_BASE_URL and OPENAI_BASE_URL not set)"
105+
echo "[health-check] Skipping health checks"
106+
fi
107+
108+
exit 0

containers/agent/entrypoint.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,54 @@ fi
114114
# Setup iptables rules
115115
/usr/local/bin/setup-iptables.sh
116116

117+
# Run API proxy health checks (verifies credential isolation and connectivity)
118+
# This must run AFTER iptables setup (which allows api-proxy traffic) but BEFORE user command
119+
# If health check fails, the script exits with non-zero code and prevents agent from running
120+
/usr/local/bin/api-proxy-health-check.sh || exit 1
121+
122+
# Configure Claude Code API key helper
123+
# This ensures the apiKeyHelper is properly configured in the config file
124+
# The config file must exist before Claude Code starts for authentication to work
125+
# In chroot mode, we write to /host$HOME/.claude.json so it's accessible after chroot
126+
if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then
127+
echo "[entrypoint] Claude Code API key helper configured: $CLAUDE_CODE_API_KEY_HELPER"
128+
129+
# In chroot mode, write to /host path so file is accessible after chroot transition
130+
if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
131+
CONFIG_FILE="/host$HOME/.claude.json"
132+
else
133+
CONFIG_FILE="$HOME/.claude.json"
134+
fi
135+
136+
if [ -f "$CONFIG_FILE" ]; then
137+
# File exists - check if it has apiKeyHelper
138+
if grep -q '"apiKeyHelper"' "$CONFIG_FILE"; then
139+
# apiKeyHelper exists - validate it matches the environment variable
140+
echo "[entrypoint] Claude Code config file exists with apiKeyHelper, validating..."
141+
CONFIGURED_HELPER=$(grep -o '"apiKeyHelper":"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
142+
if [ "$CONFIGURED_HELPER" != "$CLAUDE_CODE_API_KEY_HELPER" ]; then
143+
echo "[entrypoint][ERROR] apiKeyHelper mismatch:"
144+
echo "[entrypoint][ERROR] Environment variable: $CLAUDE_CODE_API_KEY_HELPER"
145+
echo "[entrypoint][ERROR] Config file value: $CONFIGURED_HELPER"
146+
exit 1
147+
fi
148+
echo "[entrypoint] ✓ Claude Code API key helper validated: $CLAUDE_CODE_API_KEY_HELPER"
149+
else
150+
# File exists but no apiKeyHelper - write it (overwrites empty {} created by docker-manager)
151+
echo "[entrypoint] Claude Code config file exists but missing apiKeyHelper, writing..."
152+
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE"
153+
chmod 666 "$CONFIG_FILE"
154+
echo "[entrypoint] ✓ Wrote apiKeyHelper to $CONFIG_FILE"
155+
fi
156+
else
157+
# File doesn't exist - create it
158+
echo "[entrypoint] Creating Claude Code config file with apiKeyHelper..."
159+
echo "{\"apiKeyHelper\":\"$CLAUDE_CODE_API_KEY_HELPER\"}" > "$CONFIG_FILE"
160+
chmod 666 "$CONFIG_FILE"
161+
echo "[entrypoint] ✓ Created $CONFIG_FILE with apiKeyHelper: $CLAUDE_CODE_API_KEY_HELPER"
162+
fi
163+
fi
164+
117165
# Print proxy environment
118166
echo "[entrypoint] Proxy configuration:"
119167
echo "[entrypoint] HTTP_PROXY=$HTTP_PROXY"

containers/agent/get-claude-key.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
# API Key Helper for Claude Code
3+
# This script outputs a placeholder API key since the real key is held
4+
# exclusively in the api-proxy sidecar container for credential isolation.
5+
#
6+
# The api-proxy intercepts requests and injects the real ANTHROPIC_API_KEY,
7+
# so this placeholder key will never reach the actual Anthropic API.
8+
#
9+
# This approach ensures:
10+
# 1. Claude Code agent never has access to the real API key
11+
# 2. Only api-proxy container holds the real credentials
12+
# 3. Health checks verify keys are NOT in agent environment
13+
14+
# Log helper invocation to stderr (stdout is reserved for the API key)
15+
echo "[get-claude-key.sh] API key helper invoked at $(date -Iseconds)" >&2
16+
echo "[get-claude-key.sh] Returning placeholder key for credential isolation" >&2
17+
echo "[get-claude-key.sh] Real authentication via ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-not set}" >&2
18+
19+
# Output a placeholder key (will be replaced by api-proxy)
20+
echo "sk-ant-placeholder-key-for-credential-isolation"

containers/api-proxy/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ USER apiproxy
2828
# 10001 - Anthropic API proxy
2929
EXPOSE 10000 10001
3030

31-
# Start the proxy server
32-
CMD ["node", "server.js"]
31+
# Redirect stdout/stderr to log file for persistence
32+
# Use shell form to enable redirection and tee for both file and console
33+
CMD node server.js 2>&1 | tee -a /var/log/api-proxy/api-proxy.log

containers/api-proxy/server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ if (OPENAI_API_KEY) {
175175
}
176176

177177
console.log(`[OpenAI Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`);
178+
console.log(`[OpenAI Proxy] Injecting Authorization header with OPENAI_API_KEY`);
178179
proxyRequest(req, res, 'api.openai.com', {
179180
'Authorization': `Bearer ${OPENAI_API_KEY}`,
180181
});
@@ -216,6 +217,7 @@ if (ANTHROPIC_API_KEY) {
216217
}
217218

218219
console.log(`[Anthropic Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`);
220+
console.log(`[Anthropic Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY`);
219221
// Only set anthropic-version as default; preserve agent-provided version
220222
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
221223
if (!req.headers['anthropic-version']) {

0 commit comments

Comments
 (0)