diff --git a/containers/agent/one-shot-token/README.md b/containers/agent/one-shot-token/README.md index 2dff7c587..5280348fd 100644 --- a/containers/agent/one-shot-token/README.md +++ b/containers/agent/one-shot-token/README.md @@ -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) +- Set to `"1"` or `"true"` (case-insensitive) to enable debug logging + ### Default Protected Tokens By default, the library protects these token variables: @@ -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 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" @@ -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 ``` @@ -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 diff --git a/containers/agent/one-shot-token/one-shot-token.c b/containers/agent/one-shot-token/one-shot-token.c index 48a1afd8c..35f434cf2 100644 --- a/containers/agent/one-shot-token/one-shot-token.c +++ b/containers/agent/one-shot-token/one-shot-token.c @@ -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. @@ -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; @@ -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. @@ -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"); @@ -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; } @@ -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; } @@ -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]; } @@ -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])); + } result = token_cache[token_idx]; } diff --git a/containers/agent/one-shot-token/src/lib.rs b/containers/agent/one-shot-token/src/lib.rs index 1472c5fbd..880fc5753 100644 --- a/containers/agent/one-shot-token/src/lib.rs +++ b/containers/agent/one-shot-token/src/lib.rs @@ -9,6 +9,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. +//! //! Compile: cargo build --release //! Usage: LD_PRELOAD=/path/to/libone_shot_token.so ./your-program @@ -58,6 +61,8 @@ struct TokenState { cache: HashMap, /// Whether initialization has completed initialized: bool, + /// Whether debug logging is enabled (controlled by AWF_ONE_SHOT_TOKEN_DEBUG) + debug_enabled: bool, } // SAFETY: TokenState is only accessed through a Mutex, ensuring thread safety @@ -70,6 +75,7 @@ impl TokenState { tokens: Vec::new(), cache: HashMap::new(), initialized: false, + debug_enabled: false, } } } @@ -99,6 +105,9 @@ static REAL_SECURE_GETENV: Lazy> = Lazy::new(|| { unsafe { let symbol = libc::dlsym(libc::RTLD_NEXT, c"secure_getenv".as_ptr()); if symbol.is_null() { + // Note: We can't check debug flag here because it would cause infinite recursion + // during initialization. This is a rare case (secure_getenv unavailable) so we + // always log it. eprintln!("[one-shot-token] WARNING: secure_getenv not available, falling back to getenv"); None } else { @@ -126,6 +135,31 @@ unsafe fn call_real_secure_getenv(name: *const c_char) -> *mut c_char { } } +/// Check if debug logging is enabled via AWF_ONE_SHOT_TOKEN_DEBUG environment variable +/// +/// Returns true if AWF_ONE_SHOT_TOKEN_DEBUG is set to "1" or "true" (case-insensitive) +/// This function must NOT be called through the intercepted getenv to avoid infinite recursion +fn is_debug_enabled() -> bool { + // CRITICAL: We must call the real getenv directly here to avoid infinite recursion + // when checking the debug flag during initialization + let debug_var = CString::new("AWF_ONE_SHOT_TOKEN_DEBUG").unwrap(); + // SAFETY: We're calling the real getenv with a valid C string + let debug_ptr = unsafe { call_real_getenv(debug_var.as_ptr()) }; + + if debug_ptr.is_null() { + return false; + } + + // SAFETY: debug_ptr is valid if not null + let debug_value = unsafe { CStr::from_ptr(debug_ptr) }; + if let Ok(debug_str) = debug_value.to_str() { + let debug_str_lower = debug_str.to_lowercase(); + return debug_str_lower == "1" || debug_str_lower == "true"; + } + + false +} + /// Initialize the token list from AWF_ONE_SHOT_TOKENS or defaults /// /// # Safety @@ -135,6 +169,9 @@ fn init_token_list(state: &mut TokenState) { return; } + // Check if debug logging is enabled + state.debug_enabled = is_debug_enabled(); + // Get configuration from environment let config_cstr = CString::new("AWF_ONE_SHOT_TOKENS").unwrap(); // SAFETY: We're calling the real getenv with a valid C string @@ -154,17 +191,21 @@ fn init_token_list(state: &mut TokenState) { } if !state.tokens.is_empty() { - eprintln!( - "[one-shot-token] Initialized with {} custom token(s) from AWF_ONE_SHOT_TOKENS", - state.tokens.len() - ); + if state.debug_enabled { + eprintln!( + "[one-shot-token] Initialized with {} custom token(s) from AWF_ONE_SHOT_TOKENS", + state.tokens.len() + ); + } state.initialized = true; return; } // Config was set but parsed to zero tokens - fall back to defaults - eprintln!("[one-shot-token] WARNING: AWF_ONE_SHOT_TOKENS was set but parsed to zero tokens"); - eprintln!("[one-shot-token] WARNING: Falling back to default token list to maintain protection"); + if state.debug_enabled { + eprintln!("[one-shot-token] WARNING: AWF_ONE_SHOT_TOKENS was set but parsed to zero tokens"); + eprintln!("[one-shot-token] WARNING: Falling back to default token list to maintain protection"); + } } } } @@ -177,10 +218,12 @@ fn init_token_list(state: &mut TokenState) { state.tokens.push((*token).to_string()); } - eprintln!( - "[one-shot-token] Initialized with {} default token(s)", - state.tokens.len() - ); + if state.debug_enabled { + eprintln!( + "[one-shot-token] Initialized with {} default token(s)", + state.tokens.len() + ); + } state.initialized = true; } @@ -208,14 +251,16 @@ fn format_token_value(value: &str) -> String { /// by directly checking the process's environ pointer. This works correctly /// in both chroot and non-chroot modes (reading /proc/self/environ fails in /// chroot because it shows the host's procfs, not the chrooted process's state). -fn check_task_environ_exposure(token_name: &str) { +fn check_task_environ_exposure(token_name: &str, debug_enabled: bool) { // SAFETY: environ is a standard POSIX global that points to the process's environment. // It's safe to read as long as we don't hold references across modifications. // We're only reading it after unsetenv() has completed, so the pointer is stable. unsafe { let mut env_ptr = environ; if env_ptr.is_null() { - eprintln!("[one-shot-token] INFO: Token {} cleared (environ is null)", token_name); + if debug_enabled { + eprintln!("[one-shot-token] INFO: Token {} cleared (environ is null)", token_name); + } return; } @@ -230,10 +275,12 @@ fn check_task_environ_exposure(token_name: &str) { // Check if this entry starts with our token name if env_bytes.len() >= token_prefix_bytes.len() && &env_bytes[..token_prefix_bytes.len()] == token_prefix_bytes { - eprintln!( - "[one-shot-token] WARNING: Token {} still exposed in process environment", - token_name - ); + if debug_enabled { + eprintln!( + "[one-shot-token] WARNING: Token {} still exposed in process environment", + token_name + ); + } return; } @@ -241,10 +288,12 @@ fn check_task_environ_exposure(token_name: &str) { } // Token not found in environment - success! - eprintln!( - "[one-shot-token] INFO: Token {} cleared from process environment", - token_name - ); + if debug_enabled { + eprintln!( + "[one-shot-token] INFO: Token {} cleared from process environment", + token_name + ); + } } } @@ -317,6 +366,9 @@ unsafe fn handle_getenv_impl( // Copy the value ptr::copy_nonoverlapping(value_bytes.as_ptr(), cached as *mut u8, value_bytes.len()); + // Get debug flag before dropping the state + let debug_enabled = state.debug_enabled; + // Cache the pointer so subsequent reads return the same value state.cache.insert(name_str.to_string(), cached); @@ -324,13 +376,15 @@ unsafe fn handle_getenv_impl( libc::unsetenv(name); // Verify the token was cleared from the process environment - check_task_environ_exposure(name_str); + check_task_environ_exposure(name_str, debug_enabled); - let suffix = if via_secure { " (via secure_getenv)" } else { "" }; - eprintln!( - "[one-shot-token] Token {} accessed and cached (value: {}){}", - name_str, format_token_value(value_str), suffix - ); + if debug_enabled { + let suffix = if via_secure { " (via secure_getenv)" } else { "" }; + eprintln!( + "[one-shot-token] Token {} accessed and cached (value: {}){}", + name_str, format_token_value(value_str), suffix + ); + } cached } diff --git a/tests/integration/one-shot-tokens.test.ts b/tests/integration/one-shot-tokens.test.ts index 64b8c2f1b..fe5b395e6 100644 --- a/tests/integration/one-shot-tokens.test.ts +++ b/tests/integration/one-shot-tokens.test.ts @@ -26,6 +26,9 @@ * process environment is unaffected by child unsetenv() calls, so both * `printenv` reads succeed. The caching is most relevant for programs that * call getenv() multiple times within the same process (e.g., Python, Node.js). + * + * Debug Logging: Tests set AWF_ONE_SHOT_TOKEN_DEBUG=1 to enable debug logging + * for verification. Without this flag, the library operates silently. */ /// @@ -66,6 +69,7 @@ describe('One-Shot Token Protection', () => { buildLocal: true, // Build container locally to include one-shot-token.so env: { GITHUB_TOKEN: 'ghp_test_token_12345', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -95,6 +99,7 @@ describe('One-Shot Token Protection', () => { buildLocal: true, env: { COPILOT_GITHUB_TOKEN: 'copilot_test_token_67890', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -122,6 +127,7 @@ describe('One-Shot Token Protection', () => { buildLocal: true, env: { OPENAI_API_KEY: 'sk-test-openai-key', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -158,6 +164,7 @@ describe('One-Shot Token Protection', () => { env: { GITHUB_TOKEN: 'ghp_multi_token_1', OPENAI_API_KEY: 'sk-multi-key-2', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -190,6 +197,7 @@ describe('One-Shot Token Protection', () => { buildLocal: true, env: { NORMAL_VAR: 'not_a_token', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -225,6 +233,7 @@ print(f"Second: [{second}]") buildLocal: true, env: { GITHUB_TOKEN: 'ghp_python_test_token', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -267,6 +276,7 @@ print(f"Second getenv: [{second}]") buildLocal: true, env: { GITHUB_TOKEN: 'ghp_environ_check', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -297,6 +307,7 @@ print(f"Second getenv: [{second}]") buildLocal: true, env: { GITHUB_TOKEN: 'ghp_chroot_token_12345', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -327,6 +338,7 @@ print(f"Second getenv: [{second}]") buildLocal: true, env: { COPILOT_GITHUB_TOKEN: 'copilot_chroot_token_67890', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -355,6 +367,7 @@ print(f"Second: [{second}]") buildLocal: true, env: { GITHUB_TOKEN: 'ghp_chroot_python_token', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -384,6 +397,7 @@ print(f"Second: [{second}]") buildLocal: true, env: { NORMAL_VAR: 'chroot_not_a_token', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -417,6 +431,7 @@ print(f"Second: [{second}]") env: { GITHUB_TOKEN: 'ghp_chroot_multi_1', OPENAI_API_KEY: 'sk-chroot-multi-2', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -447,6 +462,7 @@ print(f"Second: [{second}]") buildLocal: true, env: { GITHUB_TOKEN: '', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } ); @@ -498,6 +514,7 @@ print(f"Second: [{second}]") buildLocal: true, env: { GITHUB_TOKEN: 'ghp_test-with-special_chars@#$%', + AWF_ONE_SHOT_TOKEN_DEBUG: '1', }, } );