$_ bashkit

Credential Injection

Bashkit supports transparent credential injection for outbound HTTP requests. Secrets are injected at the transport layer โ€” sandboxed scripts never see the real credentials, preventing exfiltration.

See also:

Two Modes

The script has no knowledge of credentials. It makes plain requests; bashkit adds authentication headers automatically based on the URL.

use bashkit::{Bash, Credential, NetworkAllowlist};

let mut bash = Bash::builder()
    .network(NetworkAllowlist::new()
        .allow("https://api.github.com"))
    .credential("https://api.github.com",
        Credential::bearer("ghp_xxxx"))
    .build();

let result = bash.exec("curl -s https://api.github.com/repos/foo/bar").await?;
// Authorization: Bearer ghp_xxxx was added transparently.
// The script never referenced any token.

Mode 2: Placeholder

The script sees an opaque placeholder string in an environment variable. It uses the placeholder like a real API key. Bashkit replaces the placeholder with the real credential in outbound HTTP headers.

use bashkit::{Bash, Credential, NetworkAllowlist};

let mut bash = Bash::builder()
    .network(NetworkAllowlist::new()
        .allow("https://api.openai.com"))
    .credential_placeholder("OPENAI_API_KEY",
        "https://api.openai.com",
        Credential::bearer("sk-real-key"))
    .build();

// Inside the sandbox, $OPENAI_API_KEY = "bk_placeholder_a8f3c9e1..."
let result = bash.exec(r#"
    curl -H "Authorization: Bearer $OPENAI_API_KEY" \
         https://api.openai.com/v1/chat/completions \
         -d '{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}'
"#).await?;
// The placeholder was replaced with "sk-real-key" in the Authorization header.

When to use placeholder mode:

  • Agent-generated scripts that read env vars (e.g., $OPENAI_API_KEY)
  • SDKs that require a non-empty API key to initialize
  • Compatibility with existing code patterns

Credential Types

use bashkit::Credential;

// Bearer token โ†’ Authorization: Bearer <token>
let cred = Credential::bearer("ghp_xxxx");

// Custom header
let cred = Credential::header("X-Api-Key", "secret123");

// Multiple headers
let cred = Credential::headers(vec![
    ("X-Api-Key".into(), "key".into()),
    ("X-Api-Secret".into(), "secret".into()),
]);

URL Pattern Matching

Credential patterns use the same matching rules as [NetworkAllowlist]:

  • Scheme: Must match exactly (https vs http)
  • Host: Must match exactly (no wildcards)
  • Port: Must match (defaults: 443 for HTTPS, 80 for HTTP)
  • Path: Pattern path is treated as a prefix
use bashkit::{Bash, Credential, NetworkAllowlist};

let bash = Bash::builder()
    .network(NetworkAllowlist::new()
        .allow("https://api.example.com"))
    // Only inject for /v1/ paths
    .credential("https://api.example.com/v1/",
        Credential::bearer("v1_token"))
    // Different token for /v2/
    .credential("https://api.example.com/v2/",
        Credential::bearer("v2_token"))
    .build();

Security Properties

What the script cannot do

AttackWhy it fails
Read the real secret from env varsInjection mode: no env var exists. Placeholder mode: env var contains random placeholder
Exfiltrate placeholder to evil.com[NetworkAllowlist] blocks unapproved hosts. Placeholder only replaced for matching URL patterns
Set a fake Authorization headerInjected headers overwrite existing headers with the same name
Log the credential via echoScript only has access to the placeholder string, not the real secret

Header overwrite

When injecting credentials, bashkit removes any existing headers with the same name before adding the credential header. This prevents a script from setting Authorization: Basic evil and having it forwarded alongside the injected Authorization: Bearer real.

Non-blocking failures

If credential injection fails (e.g., callback error), the request is sent without credentials. This follows the same principle as bot-auth signing (request-signing spec): tool availability is never sacrificed for authentication.

Multiple Credentials

You can configure credentials for multiple hosts:

use bashkit::{Bash, Credential, NetworkAllowlist};

let bash = Bash::builder()
    .network(NetworkAllowlist::new()
        .allow("https://api.github.com")
        .allow("https://api.openai.com")
        .allow("https://registry.npmjs.org"))
    .credential("https://api.github.com",
        Credential::bearer("ghp_xxxx"))
    .credential("https://api.openai.com",
        Credential::bearer("sk-xxxx"))
    .credential("https://registry.npmjs.org",
        Credential::header("Authorization", "Bearer npm_xxxx"))
    .build();

Mixing Modes

Injection and placeholder modes can be used together:

use bashkit::{Bash, Credential, NetworkAllowlist};

let bash = Bash::builder()
    .network(NetworkAllowlist::new()
        .allow("https://api.github.com")
        .allow("https://api.openai.com"))
    // GitHub: pure injection (script doesn't know about auth)
    .credential("https://api.github.com",
        Credential::bearer("ghp_xxxx"))
    // OpenAI: placeholder (script uses $OPENAI_API_KEY)
    .credential_placeholder("OPENAI_API_KEY",
        "https://api.openai.com",
        Credential::bearer("sk-xxxx"))
    .build();

How It Works

Credential injection is built on the [hooks] system. At build time, BashBuilder converts credential rules into a before_http interceptor hook. The hook fires after the URL allowlist check but before the request is sent:

1. Allowlist check         (security gate)
2. Private IP / SSRF check (SSRF protection)
3. before_http hooks       (credential injection happens here)
4. Bot-auth signing        (Ed25519 signatures, if configured)
5. Request sent

This means:

  • Credentials are only injected for allowed URLs
  • Credentials are never sent to private IPs
  • Credentials compose with bot-auth signing
  • Custom before_http hooks run alongside credential injection