Skip to content

Commit 143742e

Browse files
authored
Merge pull request #127 from dzerik/feat/credential-injection
feat(net): credential injection primitives — inject an auth secret in the proxy (RFC #66)
2 parents c4dfd04 + c4c3fd6 commit 143742e

12 files changed

Lines changed: 1289 additions & 21 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,17 @@ sandlock run \
183183
--http-ca ca.pem --http-key ca-key.pem \
184184
-r /usr -r /lib -r /etc -- python3 agent.py
185185

186+
# Credential injection: the secret lives in the supervisor, the child never sees
187+
# it. sandlock attaches it in the proxy AFTER the ACL check. Over HTTPS the value
188+
# is encrypted to the upstream; over cleartext HTTP sandlock warns (the secret
189+
# would be exposed on the wire).
190+
sandlock run \
191+
--http-allow "POST api.openai.com/v1/*" \
192+
--http-inject-ca /etc/ssl/certs/ca-certificates.crt \
193+
--credential openai=env:OPENAI_API_KEY \
194+
--http-auth "POST api.openai.com/* bearer openai" \
195+
-r /usr -r /lib -r /etc -- python3 agent.py
196+
186197
# Server listening on ports (Landlock --net-allow-bind, separate from --net-allow;
187198
# accepts comma-separated ports and lo-hi ranges, repeatable)
188199
sandlock run --net-allow-bind 8080,9000-9005 -r /usr -r /lib -r /etc -- python3 server.py

crates/sandlock-cli/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,8 @@ async fn run_command(args: RunArgs) -> Result<i32> {
461461
if !pb.extra_deny_syscalls.is_empty() { builder = builder.extra_deny_syscalls(pb.extra_deny_syscalls.clone()); }
462462
for rule in &pb.http_allow { builder = builder.http_allow(rule); }
463463
for rule in &pb.http_deny { builder = builder.http_deny(rule); }
464+
for c in &pb.credentials { builder = builder.credential_spec(c); }
465+
for rule in &pb.http_auth { builder = builder.http_auth(rule); }
464466
for port in &pb.http_ports { builder = builder.http_port(*port); }
465467
if let Some(ref ca) = pb.http_ca { builder = builder.http_ca(ca); }
466468
if let Some(ref key) = pb.http_key { builder = builder.http_key(key); }

crates/sandlock-core/src/checkpoint/capture.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,18 @@ fn parse_fdinfo(pid: i32, fd: i32) -> io::Result<(i32, u64)> {
297297
/// Capture a checkpoint from a running, stopped sandbox.
298298
/// The sandbox must already be frozen (SIGSTOP'd and fork-held).
299299
pub(crate) fn capture(pid: i32, policy: &Sandbox) -> Result<Checkpoint, SandlockError> {
300+
// Credential-injection rules hold supervisor-only secrets that are
301+
// deliberately not serialized (`#[serde(skip)]`). A checkpoint image would
302+
// therefore restore with no injection rules and send every request
303+
// uncredentialed — reject rather than silently drop them.
304+
if !policy.inject.is_empty() {
305+
return Err(SandlockError::Runtime(SandboxRuntimeError::Child(
306+
"checkpoint is not supported with credential injection (--http-auth); \
307+
the injected secrets cannot be serialized into the image"
308+
.into(),
309+
)));
310+
}
311+
300312
// Seize via ptrace (PTRACE_SEIZE + PTRACE_INTERRUPT -- doesn't auto-SIGSTOP)
301313
ptrace_seize(pid).map_err(|e| {
302314
SandlockError::Runtime(SandboxRuntimeError::Child(format!("ptrace seize: {}", e)))

crates/sandlock-core/src/context.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,15 @@ pub(crate) fn confine_child(args: ChildSpawnArgs<'_>) -> ! {
498498
std::env::remove_var(&key);
499499
}
500500
}
501+
// Remove env vars whose value was loaded into the supervisor as a credential
502+
// source, so the agent can't read the real secret straight from its own
503+
// environment (this is the child; the mutation is process-local and post-fork).
504+
// This runs *before* applying `sandbox.env`, so the inherited real secret is
505+
// dropped but a deliberate placeholder the user passes (e.g.
506+
// `--env OPENAI_API_KEY=dummy`, which SDKs need set to start) still survives.
507+
for name in &sandbox.inject_env_strip {
508+
std::env::remove_var(name);
509+
}
501510
for (key, value) in &sandbox.env {
502511
std::env::set_var(key, value);
503512
}

0 commit comments

Comments
 (0)