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:
- Threat Model - Security properties
- Custom Builtins - Extending the shell
- Credential Injection Spec - Design decisions
Two Modes
Mode 1: Injection (recommended)
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 (
httpsvshttp) - 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
| Attack | Why it fails |
|---|---|
| Read the real secret from env vars | Injection 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 header | Injected headers overwrite existing headers with the same name |
Log the credential via echo | Script 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_httphooks run alongside credential injection