Security News

Cybersecurity news aggregator

🔓
MEDIUM Vulnerabilities Reddit r/netsec

Credential Protection for AI Agents: The Phantom Token Pattern

  • What: AI agents face credential leakage risks through environment variables
  • Impact: Sensitive API keys can be exposed via prompt injection attacks
Read Full Article →

AI coding agents need API keys to function. They call LLM providers, interact with cloud services, and authenticate against private APIs. The conventional approach is to pass these credentials as environment variables -- OPENAI_API_KEY , ANTHROPIC_API_KEY , and so on. The agent reads them, attaches them to HTTP requests, and everything works. The problem is that the agent now possesses your credentials. A prompt injection attack can trick the agent into printing its environment variables, posting them to an attacker-controlled endpoint, or embedding them in generated code. The credential is sitting in process memory and in /proc/PID/environ on Linux, readable by any same-user process. Once leaked, the blast radius is the full scope of that API key. nono solves this with a credential injection proxy that implements what we call the phantom token pattern : the agent never sees real credentials. Instead, it receives a per-session authentication token that only works with a localhost proxy. The proxy validates this token and swaps it for the real credential before forwarding the request upstream. Even if the agent is fully compromised, there is nothing to exfiltrate. Architecture The proxy runs as a separate component in the nono supervisor process, outside the sandbox. The sandboxed child process can only reach 127.0.0.1 on the proxy's port -- all other network access is filtered through the same proxy using domain allowlists. The flow works as follows: At startup, nono generates a cryptographically random 256-bit session token (32 bytes, hex-encoded to 64 characters). nono loads real API credentials from the system keystore (macOS Keychain or Linux Secret Service). The proxy starts on 127.0.0.1 with an OS-assigned ephemeral port. The child process receives environment variables that redirect SDK traffic through the proxy: OPENAI_BASE_URL=http://127.0.0.1:PORT/openai and OPENAI_API_KEY=<session-token> . The SDK sends requests to the proxy using the session token as its "API key". The proxy validates the token (constant-time comparison), strips the phantom token, injects the real credential, and forwards to the upstream over TLS. The session token is validated using constant-time comparison from the subtle crate, preventing timing side-channel attacks. Real credentials are stored in Zeroizing<String> from the zeroize crate, ensuring memory is wiped on drop. The Debug implementation on credential types outputs [REDACTED] instead of actual values, preventing accidental leakage through logging. Quick Start Step 1: Store your credentials in the system keystore Credentials are stored under the service name nono . The account name corresponds to the credential_key defined in nono's built-in network-policy.json . macOS: bash security add-generic-password -s "nono" -a "openai_api_key" -w "sk-proj-..." security add-generic-password -s "nono" -a "anthropic_api_key" -w "sk-ant-..." Linux: bash echo -n "sk-proj-..." | secret-tool store --label="nono: openai_api_key" \ service nono username openai_api_key target default echo -n "sk-ant-..." | secret-tool store --label="nono: anthropic_api_key" \ service nono username anthropic_api_key target default On Linux, the target default attribute is required for the keyring crate to locate the entry. Use username (not account ) as the attribute name. Step 2: Run with credential injection bash nono run --allow-cwd --network-profile claude-code \ --proxy-credential openai \ --proxy-credential anthropic \ -- my-agent That's it. The proxy sets OPENAI_BASE_URL and ANTHROPIC_BASE_URL in the child's environment. Most LLM SDKs (OpenAI Python, Anthropic Python, etc.) respect these variables and redirect API calls through the proxy automatically. Built-in Credential Services nono ships with four pre-configured credential services in network-policy.json : json { "credentials" : { "openai" : { "upstream" : "https://api.openai.com/v1" , "credential_key" : "openai_api_key" , "inject_header" : "Authorization" , "credential_format" : "Bearer {}" } , "anthropic" : { "upstream" : "https://api.anthropic.com" , "credential_key" : "anthropic_api_key" , "inject_header" : "x-api-key" , "credential_format" : "{}" } , "gemini" : { "upstream" : "https://generativelanguage.googleapis.com" , "credential_key" : "gemini_api_key" , "inject_header" : "x-goog-api-key" , "credential_format" : "{}" } , "google-ai" : { "upstream" : "https://generativelanguage.googleapis.com" , "credential_key" : "google_generative_ai_api_key" , "inject_header" : "x-goog-api-key" , "credential_format" : "{}" } } } Each service defines four things: upstream : The real API endpoint to forward requests to. Note that OpenAI's upstream includes /v1 because the OpenAI SDK expects the base URL to include the version prefix. Anthropic's SDK adds /v1/messages automatically, so its upstream is the root URL. credential_key : The account name in the system keystore (service name is always nono ). inject_header : The HTTP header where the real cred...

Share this article