zeroclaw/docs/sandboxing.md

5.2 KiB

ZeroClaw Sandboxing Strategies

⚠️ Status: Proposal / Roadmap

This document describes proposed approaches and may include hypothetical commands or config. For current runtime behavior, see config-reference.md, operations-runbook.md, and troubleshooting.md.

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());
    }
}