Skip to content
Closed
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
1 change: 1 addition & 0 deletions _codeql_detected_source_root
11 changes: 11 additions & 0 deletions containers/agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY pid-logger.sh /usr/local/bin/pid-logger.sh
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh

# Build one-shot-token LD_PRELOAD library for single-use token access
# This prevents tokens from being read multiple times (e.g., by malicious code)
COPY one-shot-token/one-shot-token.c /tmp/one-shot-token.c
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libc6-dev && \
gcc -shared -fPIC -O2 -Wall -o /usr/local/lib/one-shot-token.so /tmp/one-shot-token.c -ldl -lpthread && \
rm /tmp/one-shot-token.c && \
apt-get remove -y gcc libc6-dev && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*

# Install Docker stub script that shows helpful error message
# Docker-in-Docker support was removed in v0.9.1
COPY docker-stub.sh /usr/bin/docker
Expand Down
37 changes: 37 additions & 0 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,29 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
exit 1
fi

# Copy one-shot-token library to host filesystem for LD_PRELOAD in chroot
# This prevents tokens from being read multiple times by malicious code
# Note: /tmp is always writable in chroot mode (mounted from host /tmp as rw)
ONE_SHOT_TOKEN_LIB=""
if [ -f /usr/local/lib/one-shot-token.so ]; then
# Create the library directory in /tmp (always writable)
if mkdir -p /host/tmp/awf-lib 2>/dev/null; then
# Copy the library and verify it exists after copying
if cp /usr/local/lib/one-shot-token.so /host/tmp/awf-lib/one-shot-token.so 2>/dev/null && \
[ -f /host/tmp/awf-lib/one-shot-token.so ]; then
ONE_SHOT_TOKEN_LIB="/tmp/awf-lib/one-shot-token.so"
echo "[entrypoint] One-shot token library copied to chroot at ${ONE_SHOT_TOKEN_LIB}"
else
echo "[entrypoint][WARN] Could not copy one-shot-token library to /tmp/awf-lib"
echo "[entrypoint][WARN] Token protection will be disabled (tokens may be readable multiple times)"
fi
else
echo "[entrypoint][ERROR] Could not create /tmp/awf-lib directory"
Comment on lines +174 to +187

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a fixed /tmp/awf-lib path (and rm -rf on exit) can collide across concurrent runs on the same host and risks deleting another run’s library directory mid-execution. Prefer creating a per-run unique directory (e.g., include $$/timestamp/UUID in the path), store that exact path in ONE_SHOT_TOKEN_LIB, and only remove that unique directory during cleanup.

Suggested change
if [ -f /usr/local/lib/one-shot-token.so ]; then
# Create the library directory in /tmp (always writable)
if mkdir -p /host/tmp/awf-lib 2>/dev/null; then
# Copy the library and verify it exists after copying
if cp /usr/local/lib/one-shot-token.so /host/tmp/awf-lib/one-shot-token.so 2>/dev/null && \
[ -f /host/tmp/awf-lib/one-shot-token.so ]; then
ONE_SHOT_TOKEN_LIB="/tmp/awf-lib/one-shot-token.so"
echo "[entrypoint] One-shot token library copied to chroot at ${ONE_SHOT_TOKEN_LIB}"
else
echo "[entrypoint][WARN] Could not copy one-shot-token library to /tmp/awf-lib"
echo "[entrypoint][WARN] Token protection will be disabled (tokens may be readable multiple times)"
fi
else
echo "[entrypoint][ERROR] Could not create /tmp/awf-lib directory"
ONE_SHOT_TOKEN_LIB_DIR=""
if [ -f /usr/local/lib/one-shot-token.so ]; then
# Create a per-run unique library directory in /tmp (always writable)
ONE_SHOT_TOKEN_LIB_DIR_HOST="$(mktemp -d /host/tmp/awf-lib.XXXXXX 2>/dev/null || true)"
if [ -n "${ONE_SHOT_TOKEN_LIB_DIR_HOST}" ] && [ -d "${ONE_SHOT_TOKEN_LIB_DIR_HOST}" ]; then
# Copy the library and verify it exists after copying
if cp /usr/local/lib/one-shot-token.so "${ONE_SHOT_TOKEN_LIB_DIR_HOST}/one-shot-token.so" 2>/dev/null && \
[ -f "${ONE_SHOT_TOKEN_LIB_DIR_HOST}/one-shot-token.so" ]; then
# Derive the in-chroot path from the host directory basename
_awf_lib_basename="$(basename "${ONE_SHOT_TOKEN_LIB_DIR_HOST}")"
ONE_SHOT_TOKEN_LIB_DIR="/tmp/${_awf_lib_basename}"
ONE_SHOT_TOKEN_LIB="${ONE_SHOT_TOKEN_LIB_DIR}/one-shot-token.so"
echo "[entrypoint] One-shot token library copied to chroot at ${ONE_SHOT_TOKEN_LIB}"
else
echo "[entrypoint][WARN] Could not copy one-shot-token library to ${ONE_SHOT_TOKEN_LIB_DIR_HOST}"
echo "[entrypoint][WARN] Token protection will be disabled (tokens may be readable multiple times)"
fi
else
echo "[entrypoint][ERROR] Could not create per-run /tmp/awf-lib.* directory"

Copilot uses AI. Check for mistakes.
echo "[entrypoint][ERROR] This should not happen - /tmp is mounted read-write in chroot mode"
echo "[entrypoint][WARN] Token protection will be disabled (tokens may be readable multiple times)"
fi
fi

# Verify capsh is available on the host (required for privilege drop)
if ! chroot /host which capsh >/dev/null 2>&1; then
echo "[entrypoint][ERROR] capsh not found on host system"
Expand Down Expand Up @@ -355,10 +378,21 @@ AWFEOF
CLEANUP_CMD="${CLEANUP_CMD}; sed -i '/^[0-9.]\\+[[:space:]]\\+host\\.docker\\.internal\$/d' /etc/hosts 2>/dev/null || true"
echo "[entrypoint] host.docker.internal will be removed from /etc/hosts on exit"
fi
# Clean up the one-shot-token library if it was copied
if [ -n "${ONE_SHOT_TOKEN_LIB}" ]; then
CLEANUP_CMD="${CLEANUP_CMD}; rm -rf /tmp/awf-lib 2>/dev/null || true"

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a fixed /tmp/awf-lib path (and rm -rf on exit) can collide across concurrent runs on the same host and risks deleting another run’s library directory mid-execution. Prefer creating a per-run unique directory (e.g., include $$/timestamp/UUID in the path), store that exact path in ONE_SHOT_TOKEN_LIB, and only remove that unique directory during cleanup.

Suggested change
CLEANUP_CMD="${CLEANUP_CMD}; rm -rf /tmp/awf-lib 2>/dev/null || true"
ONE_SHOT_TOKEN_LIB_DIR="${ONE_SHOT_TOKEN_LIB%/*}"
if [ -n "${ONE_SHOT_TOKEN_LIB_DIR}" ] && [ "${ONE_SHOT_TOKEN_LIB_DIR}" != "${ONE_SHOT_TOKEN_LIB}" ]; then
CLEANUP_CMD="${CLEANUP_CMD}; rm -rf '${ONE_SHOT_TOKEN_LIB_DIR}' 2>/dev/null || true"
fi

Copilot uses AI. Check for mistakes.
fi

# Build LD_PRELOAD command for one-shot token protection
LD_PRELOAD_CMD=""
if [ -n "${ONE_SHOT_TOKEN_LIB}" ]; then
LD_PRELOAD_CMD="export LD_PRELOAD=${ONE_SHOT_TOKEN_LIB};"
fi

exec 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}'
"
else
Expand All @@ -371,5 +405,8 @@ else
# 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
#
# Enable one-shot token protection to prevent tokens from being read multiple times
export LD_PRELOAD=/usr/local/lib/one-shot-token.so
exec capsh --drop=$CAPS_TO_DROP -- -c "exec gosu awfuser $(printf '%q ' "$@")"
fi
224 changes: 224 additions & 0 deletions containers/agent/one-shot-token/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# One-Shot Token Library

## Overview

The one-shot token library is an `LD_PRELOAD` shared library that provides **single-use access** to sensitive environment variables containing GitHub, OpenAI, Anthropic/Claude, and Codex API tokens. When a process reads a protected token via `getenv()`, the library returns the value once and immediately unsets the environment variable, preventing subsequent reads.

This protects against malicious code that might attempt to exfiltrate tokens after the legitimate application has already consumed them.

## Protected Environment Variables

The library intercepts access to these token variables:

**GitHub:**
- `COPILOT_GITHUB_TOKEN`
- `GITHUB_TOKEN`
- `GH_TOKEN`
- `GITHUB_API_TOKEN`
- `GITHUB_PAT`
- `GH_ACCESS_TOKEN`

**OpenAI:**
- `OPENAI_API_KEY`
- `OPENAI_KEY`

**Anthropic/Claude:**
- `ANTHROPIC_API_KEY`
- `CLAUDE_API_KEY`

**Codex:**
- `CODEX_API_KEY`

## How It Works

### The LD_PRELOAD Mechanism

Linux's dynamic linker (`ld.so`) supports an environment variable called `LD_PRELOAD` that specifies shared libraries to load **before** all others. When a library is preloaded:

1. Its symbols take precedence over symbols in subsequently loaded libraries
2. This allows "interposing" or replacing standard library functions
3. The original function remains accessible via `dlsym(RTLD_NEXT, ...)`

```
┌─────────────────────────────────────────────────────────────────┐
│ Process Memory │
│ │
│ ┌──────────────────────┐ │
│ │ one-shot-token.so │ ← Loaded first via LD_PRELOAD │
│ │ getenv() ──────────┼──┐ │
│ └──────────────────────┘ │ │
│ │ dlsym(RTLD_NEXT, "getenv") │
│ ┌──────────────────────┐ │ │
│ │ libc.so │ │ │
│ │ getenv() ←─────────┼──┘ │
│ └──────────────────────┘ │
│ │
│ Application calls getenv("GITHUB_TOKEN"): │
│ 1. Resolves to one-shot-token.so's getenv() │
│ 2. We check if it's a sensitive token │
│ 3. If yes: call real getenv(), copy value, unsetenv(), return │
│ 4. If no: pass through to real getenv() │
└─────────────────────────────────────────────────────────────────┘
```

### Token Access Flow

```
First getenv("GITHUB_TOKEN") call:
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Application │────→│ one-shot-token.so │────→│ Real getenv │
│ │ │ │ │ │
│ │←────│ Returns: "ghp_..." │←────│ "ghp_..." │
└─────────────┘ │ │ └─────────────┘
│ Then: unsetenv() │
│ Mark as accessed │
└──────────────────────┘

Second getenv("GITHUB_TOKEN") call:
┌─────────────┐ ┌──────────────────┐
│ Application │────→│ one-shot-token.so │
│ │ │ │
│ │←────│ Returns: NULL │ (token already accessed)
└─────────────┘ └──────────────────────┘
```

### Thread Safety

The library uses a pthread mutex to ensure thread-safe access to the token state. Multiple threads calling `getenv()` simultaneously will be serialized for sensitive tokens, ensuring only one thread receives the actual value.

## Why This Works

### 1. Symbol Interposition

When `LD_PRELOAD=/usr/local/lib/one-shot-token.so` is set, the dynamic linker loads our library first. Any subsequent call to `getenv()` from the application or its libraries resolves to **our** implementation, not libc's.

### 2. Access to Original Function

We use `dlsym(RTLD_NEXT, "getenv")` to get a pointer to the **next** `getenv` in the symbol search order (libc's implementation). This allows us to:
- Call the real `getenv()` to retrieve the actual value
- Return that value to the caller
- Then call `unsetenv()` to remove it from the environment

### 3. State Tracking

We maintain an array of flags (`token_accessed[]`) to track which tokens have been read. Once a token is marked as accessed, subsequent calls return `NULL` without consulting the environment.

### 4. Memory Management

When we retrieve a token value, we `strdup()` it before calling `unsetenv()`. This is necessary because:
- `getenv()` returns a pointer to memory owned by the environment
- `unsetenv()` invalidates that pointer
- The caller expects a valid string, so we must copy it first

Note: This memory is intentionally never freed—it must remain valid for the lifetime of the caller's use.

## Integration with AWF

### Container Mode (non-chroot)

The library is built into the agent container image and loaded via:

```bash
export LD_PRELOAD=/usr/local/lib/one-shot-token.so
exec capsh --drop=$CAPS_TO_DROP -- -c "exec gosu awfuser $COMMAND"
```

### Chroot Mode

In chroot mode, the library must be accessible from within the chroot (host filesystem). The entrypoint:

1. Copies the library from container to `/host/tmp/awf-lib/one-shot-token.so`
2. Sets `LD_PRELOAD=/tmp/awf-lib/one-shot-token.so` inside the chroot
3. Cleans up the library on exit

## Building

### In Docker (automatic)

The Dockerfile compiles the library during image build:

```dockerfile
RUN gcc -shared -fPIC -O2 -Wall \
-o /usr/local/lib/one-shot-token.so \
/tmp/one-shot-token.c \
-ldl -lpthread
```

### Locally (for testing)

```bash
./build.sh
```

This produces `one-shot-token.so` in the current directory.

## Testing

```bash
# Build the library
./build.sh

# Create a simple C program that calls getenv twice
cat > test_getenv.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>

int main(void) {
const char *token1 = getenv("GITHUB_TOKEN");
printf("First read: %s\n", token1 ? token1 : "");

const char *token2 = getenv("GITHUB_TOKEN");
printf("Second read: %s\n", token2 ? token2 : "");

return 0;
}
EOF

# Compile the test program
gcc -o test_getenv test_getenv.c

# Test with the one-shot token library preloaded
export GITHUB_TOKEN="test-token-12345"
LD_PRELOAD=./one-shot-token.so ./test_getenv
```

Expected output:
```
[one-shot-token] Token GITHUB_TOKEN accessed and cleared
First read: test-token-12345
Second read:
```

## Security Considerations

### What This Protects Against

- **Token reuse by injected code**: If malicious code runs after the legitimate application has read its token, it cannot retrieve the token again
- **Token leakage via environment inspection**: Tools like `printenv` or reading `/proc/self/environ` will not show the token after first access

### What This Does NOT Protect Against

- **Memory inspection**: The token exists in process memory (as the returned string)
- **Interception before first read**: If malicious code runs before the legitimate code reads the token, it gets the value
- **Static linking**: Programs statically linked with libc bypass LD_PRELOAD
- **Direct syscalls**: Code that reads `/proc/self/environ` directly (without getenv) bypasses this protection

### Defense in Depth

This library is one layer in AWF's security model:
1. **Network isolation**: iptables rules redirect traffic through Squid proxy
2. **Domain allowlisting**: Squid blocks requests to non-allowed domains
3. **Capability dropping**: CAP_NET_ADMIN is dropped to prevent iptables modification
4. **One-shot tokens**: This library prevents token reuse

## Limitations

- **x86_64 Linux only**: The library is compiled for x86_64 Ubuntu
- **glibc programs only**: Programs using musl libc or statically linked programs are not affected
- **Single process**: Child processes inherit the LD_PRELOAD but have their own token state (each can read once)

## Files

- `one-shot-token.c` - Library source code
- `build.sh` - Local build script
- `README.md` - This documentation
34 changes: 34 additions & 0 deletions containers/agent/one-shot-token/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
# Build the one-shot-token LD_PRELOAD library
# This script compiles the shared library for x86_64 Ubuntu

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_FILE="${SCRIPT_DIR}/one-shot-token.c"
OUTPUT_FILE="${SCRIPT_DIR}/one-shot-token.so"

echo "[build] Compiling one-shot-token.so..."

# Compile as a shared library with position-independent code
# -shared: create a shared library
# -fPIC: position-independent code (required for shared libs)
# -ldl: link with libdl for dlsym
# -lpthread: link with pthread for mutex
# -O2: optimize for performance
# -Wall -Wextra: enable warnings
gcc -shared -fPIC \
-O2 -Wall -Wextra \
-o "${OUTPUT_FILE}" \
"${SOURCE_FILE}" \
-ldl -lpthread

echo "[build] Successfully built: ${OUTPUT_FILE}"

# Verify it's a valid shared library
if file "${OUTPUT_FILE}" | grep -q "shared object"; then
echo "[build] Verified: valid shared object"
else
echo "[build] ERROR: Output is not a valid shared object"
exit 1
fi
Loading
Loading