zeroclaw/docs/sandboxing.md
Argenis 0383a82a6f
feat(security): Add Phase 1 security features
* test: add comprehensive recovery tests for agent loop

Add recovery test coverage for all edge cases and failure scenarios
in the agentic loop, addressing the missing test coverage for
recovery use cases.

Tool Call Parsing Edge Cases:
- Empty tool_result tags
- Empty tool_calls arrays
- Whitespace-only tool names
- Empty string arguments

History Management:
- Trimming without system prompt
- Role ordering consistency after trim
- Only system prompt edge case

Arguments Parsing:
- Invalid JSON string fallback
- None arguments handling
- Null value handling

JSON Extraction:
- Empty input handling
- Whitespace only input
- Multiple JSON objects
- JSON arrays

Tool Call Value Parsing:
- Missing name field
- Non-OpenAI format
- Empty tool_calls array
- Missing tool_calls field fallback
- Top-level array format

Constants Validation:
- MAX_TOOL_ITERATIONS bounds (prevent runaway loops)
- MAX_HISTORY_MESSAGES bounds (prevent memory bloat)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(security): Add Phase 1 security features - sandboxing, resource limits, audit logging

Phase 1 security enhancements with zero impact on the quick setup wizard:
-  Pluggable sandbox trait system (traits.rs)
-  Landlock sandbox support (Linux kernel 5.13+)
-  Firejail sandbox support (Linux user-space)
-  Bubblewrap sandbox support (Linux/macOS user namespaces)
-  Docker sandbox support (container isolation)
-  No-op fallback (application-layer security only)
-  Auto-detection logic (detect.rs)
-  Audit logging with HMAC signing support (audit.rs)
-  SecurityConfig schema (SandboxConfig, ResourceLimitsConfig, AuditConfig)
-  Feature-gated implementation (sandbox-landlock, sandbox-bubblewrap)
-  1,265 tests passing

Key design principles:
- Silent auto-detection: no new prompts in wizard
- Graceful degradation: works on all platforms
- Feature flags: zero overhead when disabled
- Pluggable architecture: swap sandbox backends via config
- Backward compatible: existing configs work unchanged

Config usage:
```toml
[security.sandbox]
enabled = false  # Explicitly disable
backend = "auto"  # auto, landlock, firejail, bubblewrap, docker, none

[security.resources]
max_memory_mb = 512
max_cpu_time_seconds = 60

[security.audit]
enabled = true
log_path = "audit.log"
sign_events = false
```

Security documentation:
- docs/sandboxing.md: Sandbox implementation strategies
- docs/resource-limits.md: Resource limit approaches
- docs/audit-logging.md: Audit logging specification
- docs/security-roadmap.md: 3-phase implementation plan
- docs/frictionless-security.md: Zero-impact wizard design
- docs/agnostic-security.md: Platform/hardware agnostic approach

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 04:14:16 -05:00

4.9 KiB

ZeroClaw Sandboxing Strategies

Problem

ZeroClaw currently has application-layer security (allowlists, path blocking, command injection protection) but lacks OS-level containment. If an attacker is on the allowlist, they can run any allowed command with zeroclaw's user permissions.

Proposed Solutions

Firejail provides user-space sandboxing with minimal overhead.

// src/security/firejail.rs
use std::process::Command;

pub struct FirejailSandbox {
    enabled: bool,
}

impl FirejailSandbox {
    pub fn new() -> Self {
        let enabled = which::which("firejail").is_ok();
        Self { enabled }
    }

    pub fn wrap_command(&self, cmd: &mut Command) -> &mut Command {
        if !self.enabled {
            return cmd;
        }

        // Firejail wraps any command with sandboxing
        let mut jail = Command::new("firejail");
        jail.args([
            "--private=home",           // New home directory
            "--private-dev",            // Minimal /dev
            "--nosound",                // No audio
            "--no3d",                   // No 3D acceleration
            "--novideo",                // No video devices
            "--nowheel",                // No input devices
            "--notv",                   // No TV devices
            "--noprofile",              // Skip profile loading
            "--quiet",                  // Suppress warnings
        ]);

        // Append original command
        if let Some(program) = cmd.get_program().to_str() {
            jail.arg(program);
        }
        for arg in cmd.get_args() {
            if let Some(s) = arg.to_str() {
                jail.arg(s);
            }
        }

        // Replace original command with firejail wrapper
        *cmd = jail;
        cmd
    }
}

Config option:

[security]
enable_sandbox = true
sandbox_backend = "firejail"  # or "none", "bubblewrap", "docker"

Option 2: Bubblewrap (Portable, no root required)

Bubblewrap uses user namespaces to create containers.

# Install bubblewrap
sudo apt install bubblewrap

# Wrap command:
bwrap --ro-bind /usr /usr \
      --dev /dev \
      --proc /proc \
      --bind /workspace /workspace \
      --unshare-all \
      --share-net \
      --die-with-parent \
      -- /bin/sh -c "command"

Option 3: Docker-in-Docker (Heavyweight but complete isolation)

Run agent tools inside ephemeral containers.

pub struct DockerSandbox {
    image: String,
}

impl DockerSandbox {
    pub async fn execute(&self, command: &str, workspace: &Path) -> Result<String> {
        let output = Command::new("docker")
            .args([
                "run", "--rm",
                "--memory", "512m",
                "--cpus", "1.0",
                "--network", "none",
                "--volume", &format!("{}:/workspace", workspace.display()),
                &self.image,
                "sh", "-c", command
            ])
            .output()
            .await?;

        Ok(String::from_utf8_lossy(&output.stdout).to_string())
    }
}

Option 4: Landlock (Linux Kernel LSM, Rust native)

Landlock provides file system access control without containers.

use landlock::{Ruleset, AccessFS};

pub fn apply_landlock() -> Result<()> {
    let ruleset = Ruleset::new()
        .set_access_fs(AccessFS::read_file | AccessFS::write_file)
        .add_path(Path::new("/workspace"), AccessFS::read_file | AccessFS::write_file)?
        .add_path(Path::new("/tmp"), AccessFS::read_file | AccessFS::write_file)?
        .restrict_self()?;

    Ok(())
}

Priority Implementation Order

Phase Solution Effort Security Gain
P0 Landlock (Linux only, native) Low High (filesystem)
P1 Firejail integration Low Very High
P2 Bubblewrap wrapper Medium Very High
P3 Docker sandbox mode High Complete

Config Schema Extension

[security.sandbox]
enabled = true
backend = "auto"  # auto | firejail | bubblewrap | landlock | docker | none

# Firejail-specific
[security.sandbox.firejail]
extra_args = ["--seccomp", "--caps.drop=all"]

# Landlock-specific
[security.sandbox.landlock]
readonly_paths = ["/usr", "/bin", "/lib"]
readwrite_paths = ["$HOME/workspace", "/tmp/zeroclaw"]

Testing Strategy

#[cfg(test)]
mod tests {
    #[test]
    fn sandbox_blocks_path_traversal() {
        // Try to read /etc/passwd through sandbox
        let result = sandboxed_execute("cat /etc/passwd");
        assert!(result.is_err());
    }

    #[test]
    fn sandbox_allows_workspace_access() {
        let result = sandboxed_execute("ls /workspace");
        assert!(result.is_ok());
    }

    #[test]
    fn sandbox_no_network_isolation() {
        // Ensure network is blocked when configured
        let result = sandboxed_execute("curl http://example.com");
        assert!(result.is_err());
    }
}