The one-shot token library is an LD_PRELOAD shared library that provides cached 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 caches the value in memory and immediately unsets the environment variable. Subsequent getenv() calls return the cached value, allowing the process to read tokens multiple times while /proc/self/environ is cleared.
This protects against exfiltration via /proc/self/environ inspection while allowing legitimate multi-read access patterns that programs like the Copilot CLI require.
By default, the library protects these token variables:
GitHub:
COPILOT_GITHUB_TOKENGITHUB_TOKENGH_TOKENGITHUB_API_TOKENGITHUB_PATGH_ACCESS_TOKEN
OpenAI:
OPENAI_API_KEYOPENAI_KEY
Anthropic/Claude:
ANTHROPIC_API_KEYCLAUDE_API_KEY
Codex:
CODEX_API_KEY
You can configure a custom list of tokens to protect using the AWF_ONE_SHOT_TOKENS environment variable:
# Protect custom tokens instead of defaults
export AWF_ONE_SHOT_TOKENS="MY_API_KEY,MY_SECRET_TOKEN,CUSTOM_AUTH_KEY"
# Run your command with the library preloaded
LD_PRELOAD=/usr/local/lib/one-shot-token.so ./your-programImportant notes:
- When
AWF_ONE_SHOT_TOKENSis set with valid tokens, only those tokens are protected (defaults are not included) - If
AWF_ONE_SHOT_TOKENSis set but contains only whitespace or commas (e.g.," "or",,,"), the library falls back to the default token list to maintain protection - Use comma-separated token names (whitespace is automatically trimmed)
- Maximum of 100 tokens can be protected
- The configuration is read once at library initialization (first
getenv()call) - Uses
strtok_r()internally, which is thread-safe and won't interfere with application code usingstrtok()
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:
- Its symbols take precedence over symbols in subsequently loaded libraries
- This allows "interposing" or replacing standard library functions
- 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: cache value, unsetenv(), return cached value │
│ 4. If no: pass through to real getenv() │
└─────────────────────────────────────────────────────────────────┘
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: "ghp_..." │ (from in-memory cache)
└─────────────┘ └──────────────────────┘
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.
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.
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 - Cache the value in an in-memory array
- Call
unsetenv()to remove it from the environment (clears/proc/self/environ) - Return the cached value to the caller
We maintain an array of flags (token_accessed[]) and a parallel cache array (token_cache[]). On first access, the token value is cached and the environment variable is unset. Subsequent calls return the cached value directly.
When we retrieve a token value, we strdup() it into the cache before calling unsetenv(). This is necessary because:
getenv()returns a pointer to memory owned by the environmentunsetenv()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.
The library is built into the agent container image and loaded via:
export LD_PRELOAD=/usr/local/lib/one-shot-token.so
exec capsh --drop=$CAPS_TO_DROP -- -c "exec gosu awfuser $COMMAND"In chroot mode, the library must be accessible from within the chroot (host filesystem). The entrypoint:
- Copies the library from container to
/host/tmp/awf-lib/one-shot-token.so - Sets
LD_PRELOAD=/tmp/awf-lib/one-shot-token.soinside the chroot - Cleans up the library on exit
The Dockerfile compiles the Rust library during image build:
RUN cargo build --release && \
cp target/release/libone_shot_token.so /usr/local/lib/one-shot-token.soRequires Rust toolchain (install via rustup):
./build.shThis builds target/release/libone_shot_token.so and creates a symlink one-shot-token.so for backwards compatibility.
# 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_getenvExpected output:
[one-shot-token] Initialized with 11 default token(s)
[one-shot-token] Token GITHUB_TOKEN accessed and cached (value: test...)
First read: test-token-12345
Second read: test-token-12345
# Build the library
./build.sh
# Test with custom tokens
export AWF_ONE_SHOT_TOKENS="MY_API_KEY,SECRET_TOKEN"
export MY_API_KEY="secret-value-123"
export SECRET_TOKEN="another-secret"
LD_PRELOAD=./one-shot-token.so bash -c '
echo "First MY_API_KEY: $(printenv MY_API_KEY)"
echo "Second MY_API_KEY: $(printenv MY_API_KEY)"
echo "First SECRET_TOKEN: $(printenv SECRET_TOKEN)"
echo "Second SECRET_TOKEN: $(printenv SECRET_TOKEN)"
'Expected output:
[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...)
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...)
First SECRET_TOKEN: another-secret
Second SECRET_TOKEN: another-secret
When using the library with AWF (Agentic Workflow Firewall):
# Use default tokens
sudo awf --allow-domains github.com -- your-command
# Use custom tokens
export AWF_ONE_SHOT_TOKENS="MY_TOKEN,CUSTOM_API_KEY"
sudo -E awf --allow-domains github.com -- your-commandNote: The AWF_ONE_SHOT_TOKENS variable must be exported before running awf so it's available when the library initializes.
- Token leakage via environment inspection:
/proc/self/environand tools likeprintenv(in the same process) will not show the token after first access — the environment variable is unset - Token exfiltration via /proc: Other processes reading
/proc/<pid>/environcannot see the token
- Memory inspection: The token exists in process memory (in the cache array)
- Interception before first read: If malicious code runs before the legitimate code reads the token, it gets the value
- In-process getenv() calls: Since values are cached, any code in the same process can still call
getenv()and get the cached token - Static linking: Programs statically linked with libc bypass LD_PRELOAD
- Direct syscalls: Code that reads
/proc/self/environdirectly (without getenv) bypasses this protection
This library is one layer in AWF's security model:
- Network isolation: iptables rules redirect traffic through Squid proxy
- Domain allowlisting: Squid blocks requests to non-allowed domains
- Capability dropping: CAP_NET_ADMIN is dropped to prevent iptables modification
- Token environment cleanup: This library clears tokens from
/proc/self/environwhile caching for legitimate use
- Linux only: The library is compiled for Linux (x86_64 and potentially other architectures via Rust cross-compilation)
- 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 and cache (each starts fresh)
src/lib.rs- Library source code (Rust)Cargo.toml- Rust package configurationbuild.sh- Local build scriptone-shot-token.c- Legacy C implementation (for reference)README.md- This documentation