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
45 changes: 38 additions & 7 deletions containers/agent/one-shot-token/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ This protects against exfiltration via `/proc/self/environ` inspection while all

## Configuration

### Debug Logging

By default, the library operates **silently** with no output to stderr. To enable debug logging, set the `AWF_ONE_SHOT_TOKEN_DEBUG` environment variable:

```bash
# Enable debug logging
export AWF_ONE_SHOT_TOKEN_DEBUG=1
# or
export AWF_ONE_SHOT_TOKEN_DEBUG=true

# Run your command with the library preloaded
LD_PRELOAD=/usr/local/lib/one-shot-token.so ./your-program
```

**Important notes:**
- Debug logging is **off by default** to reduce noise in production environments
- When enabled, the library logs initialization messages and token access events to stderr
- The `AWF_ONE_SHOT_TOKEN_DEBUG` variable is never cached or cleared (prevents infinite recursion)

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

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

This bullet says AWF_ONE_SHOT_TOKEN_DEBUG is “never cached or cleared”, but neither the C nor Rust implementation currently prevents a user from including it in AWF_ONE_SHOT_TOKENS (in which case it would be treated like a protected token). Either enforce the exclusion in code or soften this statement to reflect the actual behavior.

Suggested change
- The `AWF_ONE_SHOT_TOKEN_DEBUG` variable is never cached or cleared (prevents infinite recursion)
- By default, the `AWF_ONE_SHOT_TOKEN_DEBUG` variable is not cached or cleared (to prevent infinite recursion when checking it)
- If you explicitly add `AWF_ONE_SHOT_TOKEN_DEBUG` to `AWF_ONE_SHOT_TOKENS`, it will be treated like any other protected token and may be cached/cleared

Copilot uses AI. Check for mistakes.
- Set to `"1"` or `"true"` (case-insensitive) to enable debug logging

### Default Protected Tokens

By default, the library protects these token variables:
Expand Down Expand Up @@ -223,27 +243,32 @@ EOF
# Compile the test program
gcc -o test_getenv test_getenv.c

# Test with the one-shot token library preloaded
# Test with the one-shot token library preloaded (with debug logging)
export GITHUB_TOKEN="test-token-12345"
export AWF_ONE_SHOT_TOKEN_DEBUG=1
LD_PRELOAD=./one-shot-token.so ./test_getenv
```

Expected output:
Expected output (with debug logging enabled):
```
[one-shot-token] Initialized with 11 default token(s)
[one-shot-token] Token GITHUB_TOKEN accessed and cached (value: test...)
[one-shot-token] INFO: Token GITHUB_TOKEN cleared from process environment

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

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

The expected output includes an INFO: Token ... cleared from process environment line, but the agent Docker image builds the C implementation (one-shot-token.c), which does not emit these INFO lines. Update the example to match the C library’s actual output, or add equivalent logging to the C implementation when debug is enabled.

This issue also appears on line 285 of the same file.

Suggested change
[one-shot-token] INFO: Token GITHUB_TOKEN cleared from process environment

Copilot uses AI. Check for mistakes.
First read: test-token-12345
Second read: test-token-12345
```

**Note:** Without `AWF_ONE_SHOT_TOKEN_DEBUG=1`, the library operates silently with no stderr output.

### Custom Token Test

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

# Test with custom tokens
# Test with custom tokens (with debug logging)
export AWF_ONE_SHOT_TOKENS="MY_API_KEY,SECRET_TOKEN"
export AWF_ONE_SHOT_TOKEN_DEBUG=1
export MY_API_KEY="secret-value-123"
export SECRET_TOKEN="another-secret"

Expand All @@ -255,13 +280,15 @@ LD_PRELOAD=./one-shot-token.so bash -c '
'
```

Expected output:
Expected output (with debug logging enabled):
```
[one-shot-token] Initialized with 2 custom token(s) from AWF_ONE_SHOT_TOKENS
[one-shot-token] Token MY_API_KEY accessed and cached (value: secr...)
[one-shot-token] INFO: Token MY_API_KEY cleared from process environment
First MY_API_KEY: secret-value-123
Second MY_API_KEY: secret-value-123
[one-shot-token] Token SECRET_TOKEN accessed and cached (value: anot...)
[one-shot-token] INFO: Token SECRET_TOKEN cleared from process environment
First SECRET_TOKEN: another-secret
Second SECRET_TOKEN: another-secret
```
Expand All @@ -271,15 +298,19 @@ Second SECRET_TOKEN: another-secret
When using the library with AWF (Agentic Workflow Firewall):

```bash
# Use default tokens
# Use default tokens (silent mode)
sudo awf --allow-domains github.com -- your-command

# Use custom tokens
# Use custom tokens with debug logging
export AWF_ONE_SHOT_TOKENS="MY_TOKEN,CUSTOM_API_KEY"
export AWF_ONE_SHOT_TOKEN_DEBUG=1
sudo -E awf --allow-domains github.com -- your-command
```

Note: The `AWF_ONE_SHOT_TOKENS` variable must be exported before running `awf` so it's available when the library initializes.
**Important notes:**
- The `AWF_ONE_SHOT_TOKENS` variable must be exported before running `awf` so it's available when the library initializes
- Set `AWF_ONE_SHOT_TOKEN_DEBUG=1` to enable debug logging; otherwise the library operates silently
- Use `sudo -E` to preserve environment variables when running with sudo

## Security Considerations

Expand Down
63 changes: 55 additions & 8 deletions containers/agent/one-shot-token/one-shot-token.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
* AWF_ONE_SHOT_TOKENS - Comma-separated list of token names to protect
* If not set, uses built-in defaults
*
* AWF_ONE_SHOT_TOKEN_DEBUG - Enable debug logging output (default: off)
* Set to "1" or "true" to enable logging. Logging is silent by default.
*
* Build hardening:
* Default token names are XOR-obfuscated to prevent cleartext extraction
* via strings(1) or objdump. Internal symbols use hidden visibility.
Expand Down Expand Up @@ -126,6 +129,9 @@ static __thread int in_getenv = 0;
/* Initialization flag */
static int tokens_initialized = 0;

/* Debug logging flag (controlled by AWF_ONE_SHOT_TOKEN_DEBUG environment variable) */
static int debug_enabled = 0;

/* Pointer to the real getenv function */
static char *(*real_getenv)(const char *name) = NULL;

Expand All @@ -149,6 +155,34 @@ static void ensure_real_secure_getenv(void) {
/* secure_getenv may not be available on all systems - that's OK */
}

/**
* Check if debug logging is enabled via AWF_ONE_SHOT_TOKEN_DEBUG environment variable.
* Returns 1 if AWF_ONE_SHOT_TOKEN_DEBUG is set to "1" or "true" (case-insensitive), 0 otherwise.
*
* CRITICAL: This function must call the real getenv directly to avoid infinite recursion
* when checking the debug flag during initialization. The AWF_ONE_SHOT_TOKEN_DEBUG variable
* is never cached or cleared by this library.
*/
static int is_debug_enabled(void) {
const char *debug_value = real_getenv("AWF_ONE_SHOT_TOKEN_DEBUG");

if (debug_value == NULL || debug_value[0] == '\0') {
return 0;
}

/* Check if value is "1" */
if (strcmp(debug_value, "1") == 0) {
return 1;
}

/* Check if value is "true" (case-insensitive) */
if (strcasecmp(debug_value, "true") == 0) {
return 1;
}

return 0;
}

/**
* Initialize the token list from AWF_ONE_SHOT_TOKENS environment variable
* or use defaults if not set. This is called once at first getenv() call.
Expand All @@ -159,6 +193,9 @@ static void init_token_list(void) {
return;
}

/* Check if debug logging is enabled */
debug_enabled = is_debug_enabled();

/* Get the configuration from environment */
const char *config = real_getenv("AWF_ONE_SHOT_TOKENS");
Comment on lines +196 to 200

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

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

AWF_ONE_SHOT_TOKEN_DEBUG is documented as never being cached/cleared, but the custom token list parsing will currently accept it like any other token name. Consider explicitly skipping AWF_ONE_SHOT_TOKEN_DEBUG when parsing AWF_ONE_SHOT_TOKENS to make that guarantee true.

Copilot uses AI. Check for mistakes.

Expand Down Expand Up @@ -208,12 +245,16 @@ static void init_token_list(void) {
/* If AWF_ONE_SHOT_TOKENS was set but resulted in zero tokens (e.g., ",,," or whitespace only),
* fall back to defaults to avoid silently disabling all protection */
if (num_tokens == 0) {
fprintf(stderr, "[one-shot-token] WARNING: AWF_ONE_SHOT_TOKENS was set but parsed to zero tokens\n");
fprintf(stderr, "[one-shot-token] WARNING: Falling back to default token list to maintain protection\n");
if (debug_enabled) {
fprintf(stderr, "[one-shot-token] WARNING: AWF_ONE_SHOT_TOKENS was set but parsed to zero tokens\n");
fprintf(stderr, "[one-shot-token] WARNING: Falling back to default token list to maintain protection\n");
}
/* num_tokens is already 0 here; assignment is defensive programming for future refactoring */
num_tokens = 0;
} else {
fprintf(stderr, "[one-shot-token] Initialized with %d custom token(s) from AWF_ONE_SHOT_TOKENS\n", num_tokens);
if (debug_enabled) {
fprintf(stderr, "[one-shot-token] Initialized with %d custom token(s) from AWF_ONE_SHOT_TOKENS\n", num_tokens);
}
tokens_initialized = 1;
return;
}
Expand All @@ -234,7 +275,9 @@ static void init_token_list(void) {
num_tokens++;
}

fprintf(stderr, "[one-shot-token] Initialized with %d default token(s)\n", num_tokens);
if (debug_enabled) {
fprintf(stderr, "[one-shot-token] Initialized with %d default token(s)\n", num_tokens);
}

tokens_initialized = 1;
}
Expand Down Expand Up @@ -348,8 +391,10 @@ char *getenv(const char *name) {
/* Unset the variable from the environment so /proc/self/environ is cleared */
unsetenv(name);

fprintf(stderr, "[one-shot-token] Token %s accessed and cached (value: %s)\n",
name, format_token_value(token_cache[token_idx]));
if (debug_enabled) {
fprintf(stderr, "[one-shot-token] Token %s accessed and cached (value: %s)\n",
name, format_token_value(token_cache[token_idx]));
}

result = token_cache[token_idx];
}
Expand Down Expand Up @@ -412,8 +457,10 @@ char *secure_getenv(const char *name) {
/* Unset the variable from the environment so /proc/self/environ is cleared */
unsetenv(name);

fprintf(stderr, "[one-shot-token] Token %s accessed and cached (value: %s) (via secure_getenv)\n",
name, format_token_value(token_cache[token_idx]));
if (debug_enabled) {
fprintf(stderr, "[one-shot-token] Token %s accessed and cached (value: %s) (via secure_getenv)\n",
name, format_token_value(token_cache[token_idx]));
}
Comment on lines 459 to +463

Copilot AI Feb 15, 2026

Copy link

Choose a reason for hiding this comment

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

secure_getenv() does not initialize the token list (and debug_enabled is only set inside init_token_list). If secure_getenv is the first call in a process, num_tokens will still be 0 so no tokens are protected and debug logging will never enable. Fix by ensuring token initialization (under the mutex) happens in secure_getenv() before checking get_token_index() / using debug_enabled.

Copilot uses AI. Check for mistakes.

result = token_cache[token_idx];
}
Expand Down
Loading
Loading