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
94 changes: 90 additions & 4 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,39 @@ else
echo "[entrypoint] Dropping CAP_NET_ADMIN capability"
fi

# Function to unset sensitive tokens from the entrypoint's environment
# This prevents tokens from being accessible via /proc/1/environ after the agent has started
unset_sensitive_tokens() {
# List of sensitive token environment variables (matches one-shot-token library defaults)
local SENSITIVE_TOKENS=(
# GitHub tokens
"COPILOT_GITHUB_TOKEN"
"GITHUB_TOKEN"
"GH_TOKEN"
"GITHUB_API_TOKEN"
"GITHUB_PAT"
"GH_ACCESS_TOKEN"
"GITHUB_PERSONAL_ACCESS_TOKEN"
# OpenAI tokens
"OPENAI_API_KEY"
"OPENAI_KEY"
# Anthropic/Claude tokens
"ANTHROPIC_API_KEY"
"CLAUDE_API_KEY"
"CLAUDE_CODE_OAUTH_TOKEN"
# Codex tokens
"CODEX_API_KEY"
)

echo "[entrypoint] Unsetting sensitive tokens from parent shell environment..." >&2
for token in "${SENSITIVE_TOKENS[@]}"; do
if [ -n "${!token}" ]; then
unset "$token"
echo "[entrypoint] Unset $token from /proc/1/environ" >&2
fi
done
}

echo "[entrypoint] Switching to awfuser (UID: $(id -u awfuser), GID: $(id -g awfuser))"
echo "[entrypoint] Executing command: $@"
echo ""
Expand Down Expand Up @@ -413,12 +446,38 @@ AWFEOF
LD_PRELOAD_CMD="export LD_PRELOAD=${ONE_SHOT_TOKEN_LIB};"
fi

exec chroot /host /bin/bash -c "
# Setup signal handler to forward signals to agent process and perform cleanup
cleanup_and_exit() {
if [ -n "$AGENT_PID" ]; then
kill -TERM "$AGENT_PID" 2>/dev/null || true
wait "$AGENT_PID" 2>/dev/null || true
fi
exit 143 # Standard exit code for SIGTERM
}
trap cleanup_and_exit TERM INT

# SECURITY: Run agent command in background, then unset tokens from parent shell
# This prevents tokens from being accessible via /proc/1/environ after agent starts
# The one-shot-token library caches tokens in the agent process, so agent can still read them
chroot /host /bin/bash -c "
cd '${CHROOT_WORKDIR}' 2>/dev/null || cd /
trap '${CLEANUP_CMD}' EXIT
${LD_PRELOAD_CMD}
exec capsh --drop=${CAPS_TO_DROP} --user=${HOST_USER} -- -c 'exec ${SCRIPT_FILE}'
"
" &
AGENT_PID=$!

# Wait for agent to initialize and cache tokens (5 seconds)
sleep 5

# Unset all sensitive tokens from parent shell environment
unset_sensitive_tokens

# Wait for agent command to complete and capture its exit code
wait $AGENT_PID
EXIT_CODE=$?
trap - TERM INT
exit $EXIT_CODE
else
# Original behavior - run in container filesystem
# Drop capabilities and privileges, then execute the user command
Expand All @@ -428,10 +487,37 @@ else
# The order of operations:
# 1. capsh drops capabilities from the bounding set (cannot be regained)
# 2. gosu switches to awfuser (drops root privileges)
# 3. exec replaces the current process with the user command
# 3. Execute the user command (NOT using exec, so we can unset tokens after)
#
# Enable one-shot token protection - tokens are cached in memory and
# unset from the environment so /proc/self/environ is cleared
export LD_PRELOAD=/usr/local/lib/one-shot-token.so
exec capsh --drop=$CAPS_TO_DROP -- -c "exec gosu awfuser $(printf '%q ' "$@")"

# Setup signal handler to forward signals to agent process and perform cleanup
cleanup_and_exit() {
if [ -n "$AGENT_PID" ]; then
kill -TERM "$AGENT_PID" 2>/dev/null || true
wait "$AGENT_PID" 2>/dev/null || true
fi
exit 143 # Standard exit code for SIGTERM
}
trap cleanup_and_exit TERM INT

# SECURITY: Run agent command in background, then unset tokens from parent shell
# This prevents tokens from being accessible via /proc/1/environ after agent starts
# The one-shot-token library caches tokens in the agent process, so agent can still read them
capsh --drop=$CAPS_TO_DROP -- -c "exec gosu awfuser $(printf '%q ' "$@")" &
AGENT_PID=$!

# Wait for agent to initialize and cache tokens (5 seconds)
sleep 5

# Unset all sensitive tokens from parent shell environment
unset_sensitive_tokens

# Wait for agent command to complete and capture its exit code
wait $AGENT_PID
EXIT_CODE=$?
trap - TERM INT
exit $EXIT_CODE
fi
80 changes: 80 additions & 0 deletions docs/token-unsetting-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Token Unsetting Security Fix

## Problem

The entrypoint script (PID 1) in the agent container had sensitive tokens (GITHUB_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) in its environment. While the one-shot-token library successfully cached these tokens in the agent process and cleared them from `/proc/self/environ`, the entrypoint's environment at `/proc/1/environ` still contained the tokens, making them accessible to malicious code.

## Solution

Modified the entrypoint to unset all sensitive tokens from its own environment after the agent process has started and cached them. This is implemented in both chroot and non-chroot execution modes.

### Implementation Details

1. **Added `unset_sensitive_tokens()` function** (entrypoint.sh:145-176)
- Maintains a list of sensitive token environment variables
- Iterates through the list and unsets each token from the parent shell
- Logs which tokens were unset

2. **Modified chroot mode execution** (entrypoint.sh:449-468)
- Changed from `exec chroot ...` to `chroot ... &` (run in background)
- Added 5-second sleep to allow agent to initialize and cache tokens
- Call `unset_sensitive_tokens()` to clear tokens from parent shell
- Use `wait $AGENT_PID` to wait for agent completion
- Exit with agent's exit code

3. **Modified non-chroot mode execution** (entrypoint.sh:484-499)
- Changed from `exec capsh ...` to `capsh ... &` (run in background)
- Added 5-second sleep to allow agent to initialize and cache tokens
- Call `unset_sensitive_tokens()` to clear tokens from parent shell
- Use `wait $AGENT_PID` to wait for agent completion
- Exit with agent's exit code

4. **Updated one-shot-token library** (one-shot-token/src/lib.rs:32-50)
- Added `GITHUB_PERSONAL_ACCESS_TOKEN` to default token list
- Added `CLAUDE_CODE_OAUTH_TOKEN` to default token list
- Now matches the list in entrypoint.sh

### Token List

The following tokens are unset from the entrypoint's environment:

- **GitHub tokens**: COPILOT_GITHUB_TOKEN, GITHUB_TOKEN, GH_TOKEN, GITHUB_API_TOKEN, GITHUB_PAT, GH_ACCESS_TOKEN, GITHUB_PERSONAL_ACCESS_TOKEN
- **OpenAI tokens**: OPENAI_API_KEY, OPENAI_KEY
- **Anthropic/Claude tokens**: ANTHROPIC_API_KEY, CLAUDE_API_KEY, CLAUDE_CODE_OAUTH_TOKEN
- **Codex tokens**: CODEX_API_KEY

### Timeline

1. **t=0s**: Container starts, entrypoint receives tokens in environment
2. **t=0s**: Entrypoint starts agent command in background
3. **t=0-5s**: Agent initializes, reads tokens via getenv(), one-shot-token library caches them
4. **t=5s**: Entrypoint calls `unset_sensitive_tokens()`, clearing tokens from `/proc/1/environ`
5. **t=5s+**: Agent continues running with cached tokens, `/proc/1/environ` no longer contains tokens
6. **t=end**: Agent completes, entrypoint exits with agent's exit code

### Security Impact

- **Before**: Tokens accessible via `/proc/1/environ` throughout agent execution
- **After**: Tokens accessible via `/proc/1/environ` only for first 5 seconds, then cleared
- **Agent behavior**: Unchanged - agent can still read tokens via getenv() (cached by one-shot-token library)

### Testing

Integration test added at `tests/integration/token-unset.test.ts`:
- Verifies GITHUB_TOKEN cleared from `/proc/1/environ` after agent starts
- Verifies OPENAI_API_KEY cleared from `/proc/1/environ` after agent starts
- Verifies ANTHROPIC_API_KEY cleared from `/proc/1/environ` after agent starts
- Verifies multiple tokens cleared simultaneously
- Verifies behavior in both chroot and non-chroot modes
- Verifies agent can still read tokens via getenv() after unsetting

Manual test script at `test-token-unset.sh`:
- Can be run locally with `./test-token-unset.sh`
- Requires sudo and Docker
- Sets test tokens and verifies they are cleared from `/proc/1/environ`

## Notes

- The 5-second delay is necessary to give the agent process time to initialize and cache tokens via the one-shot-token library before the parent shell unsets them
- Both token lists (entrypoint.sh and one-shot-token library) must be kept in sync when adding new token types
- The exit code handling is preserved - the entrypoint exits with the agent's exit code
74 changes: 74 additions & 0 deletions test-token-unset.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/bash
# Test script to verify tokens are unset from /proc/1/environ after agent starts

set -e

echo "=== Testing token unsetting from entrypoint environ ==="

# Set test tokens
export GITHUB_TOKEN="ghp_test_token_12345"
export OPENAI_API_KEY="sk-test_openai_key_67890"
export ANTHROPIC_API_KEY="sk-ant-test_key_abcdef"

echo "Test tokens set in host environment"

# Run a simple command that waits 10 seconds (longer than the 5-second token unset delay)
# This gives us time to check /proc/1/environ inside the container
echo "Running awf with test tokens..."
sudo -E node dist/cli.js \
--allow-domains example.com \
--build-local \
--keep-containers \
-- bash -c '
echo "Agent started, checking /proc/1/environ in container..."
sleep 2

# Check if tokens are still in /proc/1/environ
echo "Checking /proc/1/environ for GITHUB_TOKEN..."
if cat /proc/1/environ | tr "\0" "\n" | grep -q "GITHUB_TOKEN="; then
echo "ERROR: GITHUB_TOKEN still in /proc/1/environ"
exit 1
else
echo "SUCCESS: GITHUB_TOKEN not in /proc/1/environ"
fi

echo "Checking /proc/1/environ for OPENAI_API_KEY..."
if cat /proc/1/environ | tr "\0" "\n" | grep -q "OPENAI_API_KEY="; then
echo "ERROR: OPENAI_API_KEY still in /proc/1/environ"
exit 1
else
echo "SUCCESS: OPENAI_API_KEY not in /proc/1/environ"
fi

echo "Checking /proc/1/environ for ANTHROPIC_API_KEY..."
if cat /proc/1/environ | tr "\0" "\n" | grep -q "ANTHROPIC_API_KEY="; then
echo "ERROR: ANTHROPIC_API_KEY still in /proc/1/environ"
exit 1
else
echo "SUCCESS: ANTHROPIC_API_KEY not in /proc/1/environ"
fi

# Verify agent can still read tokens via getenv (cached by one-shot-token library)
echo "Checking if agent can still read GITHUB_TOKEN via getenv..."
if [ -n "$GITHUB_TOKEN" ]; then
echo "SUCCESS: Agent can still read GITHUB_TOKEN (value: ${GITHUB_TOKEN:0:10}...)"
else
echo "WARNING: GITHUB_TOKEN not accessible to agent"
fi

echo "All checks passed!"
exit 0
'

EXIT_CODE=$?

# Cleanup
echo "Cleaning up containers..."
sudo docker compose -f /tmp/awf-*/docker-compose.yml down -v 2>/dev/null || true

if [ $EXIT_CODE -eq 0 ]; then
echo "=== TEST PASSED ==="
else
echo "=== TEST FAILED ==="
exit 1
fi
Loading
Loading