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