Files
agent-sandbox/src/sandbox.rs
T

244 lines
6.7 KiB
Rust

use std::path::{Path, PathBuf};
use std::process::Command;
use crate::agents;
use crate::blacklist;
use crate::env;
use crate::seccomp;
use crate::{EnvEntry, SandboxConfig, SandboxError, SandboxMode};
pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
let mut cmd = Command::new("bwrap");
let hardened = config.hardened || matches!(config.mode, SandboxMode::Whitelist);
if hardened {
cmd.args(["--unshare-ipc", "--unshare-pid", "--unshare-uts"]);
cmd.args(["--hostname", "sandbox"]);
}
if config.unshare_net {
cmd.arg("--unshare-net");
}
match config.mode {
SandboxMode::Blacklist => add_blacklist_mode(&mut cmd)?,
SandboxMode::Whitelist => add_whitelist_mode(&mut cmd)?,
}
for path in agents::agent_rw_paths() {
cmd.arg("--bind-try").arg(&path).arg(&path);
}
for path in &config.extra_ro {
add_ro_bind(&mut cmd, path)?;
}
add_rw_bind(&mut cmd, &config.chdir)?;
for path in &config.extra_rw {
add_rw_bind(&mut cmd, path)?;
}
add_env_policy(&mut cmd, config);
add_user_env_overrides(&mut cmd, config);
cmd.args(["--remount-ro", "/"]);
cmd.arg("--die-with-parent");
cmd.arg("--chdir").arg(&config.chdir);
apply_masks(&mut cmd, &config.mask);
if config.seccomp {
add_seccomp_filter(&mut cmd)?;
}
cmd.args(&config.bwrap_args);
cmd.arg("--")
.arg(&config.command)
.args(&config.command_args);
Ok(cmd)
}
fn add_env_policy(cmd: &mut Command, config: &SandboxConfig) {
if !config.env_filter {
return;
}
let parent_env: Vec<(String, String)> = std::env::vars().collect();
let args = match config.mode {
SandboxMode::Blacklist => env::blacklist_env_args(&parent_env),
SandboxMode::Whitelist => env::whitelist_env_args(&parent_env),
};
cmd.args(args);
}
fn add_user_env_overrides(cmd: &mut Command, config: &SandboxConfig) {
let mut keep_keys: Vec<String> = Vec::new();
for entry in &config.env {
match entry {
EnvEntry::Set(key, value) => {
cmd.arg("--setenv").arg(key).arg(value);
}
EnvEntry::Keep(key) => keep_keys.push(key.clone()),
}
}
if !keep_keys.is_empty() {
let parent_env: Vec<(String, String)> = std::env::vars().collect();
cmd.args(env::keepenv_args(&keep_keys, &parent_env));
}
for key in &config.unsetenv {
cmd.arg("--unsetenv").arg(key);
}
}
fn apply_masks(cmd: &mut Command, masks: &[PathBuf]) {
for path in masks {
if path.is_file() {
cmd.arg("--ro-bind").arg("/dev/null").arg(path);
} else {
cmd.arg("--tmpfs").arg(path);
}
}
}
fn add_blacklist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
let ctx = blacklist::resolve_path_context()?;
cmd.args(["--ro-bind", "/", "/"]);
cmd.args(["--dev-bind", "/dev", "/dev"]);
cmd.args(["--proc", "/proc"]);
cmd.args(["--bind", "/tmp", "/tmp"]);
cmd.args(["--bind", "/var/tmp", "/var/tmp"]);
let overlays = blacklist::resolve_overlays(&ctx)?;
for dir in &overlays.tmpfs_dirs {
cmd.arg("--tmpfs").arg(dir);
}
for file in &overlays.null_bind_files {
cmd.arg("--ro-bind").arg("/dev/null").arg(file);
}
cmd.args(["--tmpfs", "/run"]);
ro_bind_under_tmpfs(
cmd,
"/run",
&[
"/run/dbus/system_bus_socket",
"/run/systemd/resolve",
"/run/systemd/journal",
"/run/log/journal",
"/run/udev",
"/run/NetworkManager/resolv.conf",
"/run/media",
],
);
ensure_parent_dirs(cmd, "/run", &ctx.run_user);
cmd.arg("--tmpfs").arg(&ctx.run_user);
let run_user_bus = format!("{}/bus", ctx.run_user);
ro_bind_under_tmpfs(cmd, &ctx.run_user, &[&run_user_bus]);
Ok(())
}
fn add_whitelist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
let home = crate::require_home()?;
cmd.args(["--ro-bind", "/usr", "/usr"]);
for path in ["/lib", "/lib64", "/lib32", "/bin", "/sbin"] {
cmd.args(["--ro-bind-try", path, path]);
}
for path in [
"/etc/ld.so.cache",
"/etc/ld.so.conf",
"/etc/ld.so.conf.d",
"/etc/alternatives",
"/etc/ssl",
"/etc/ca-certificates",
"/etc/resolv.conf",
"/etc/nsswitch.conf",
"/etc/passwd",
"/etc/group",
"/etc/hosts",
"/etc/gai.conf",
"/etc/services",
"/etc/protocols",
"/etc/hostname",
"/etc/localtime",
"/etc/machine-id",
] {
cmd.args(["--ro-bind-try", path, path]);
}
cmd.args(["--ro-bind-try", "/sys", "/sys"]);
let local_bin = format!("{home}/.local/bin");
cmd.arg("--ro-bind-try").arg(&local_bin).arg(&local_bin);
let cache_dir = format!("{home}/.cache");
cmd.arg("--tmpfs").arg(&cache_dir);
cmd.args(["--tmpfs", "/tmp"]);
cmd.args(["--tmpfs", "/var/tmp"]);
cmd.args(["--dev", "/dev"]);
cmd.args(["--tmpfs", "/dev/shm"]);
cmd.args(["--tmpfs", "/run"]);
cmd.args(["--proc", "/proc"]);
Ok(())
}
fn ensure_parent_dirs(cmd: &mut Command, base: &str, path: &str) {
let base = Path::new(base);
let ancestors: Vec<_> = Path::new(path)
.ancestors()
.skip(1)
.take_while(|a| *a != base)
.collect();
for dir in ancestors.into_iter().rev() {
cmd.arg("--dir").arg(dir);
}
}
fn ro_bind_under_tmpfs(cmd: &mut Command, base: &str, paths: &[&str]) {
let base = Path::new(base);
let mut dirs_created = std::collections::HashSet::new();
for path in paths {
let ancestors: Vec<_> = Path::new(path)
.ancestors()
.skip(1) // skip the path itself
.take_while(|a| *a != base)
.filter(|a| dirs_created.insert(a.to_path_buf()))
.collect();
for dir in ancestors.into_iter().rev() {
cmd.arg("--dir").arg(dir);
}
cmd.args(["--ro-bind-try", path, path]);
}
}
fn add_rw_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
let source = resolve_bind_source(path)?;
cmd.arg("--bind").arg(source).arg(path);
Ok(())
}
fn add_ro_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
let source = resolve_bind_source(path)?;
cmd.arg("--ro-bind").arg(source).arg(path);
Ok(())
}
fn resolve_bind_source(path: &Path) -> Result<PathBuf, SandboxError> {
std::fs::canonicalize(path).map_err(|_| SandboxError::PathMissing(path.to_path_buf()))
}
fn add_seccomp_filter(cmd: &mut Command) -> Result<(), SandboxError> {
let fd = seccomp::write_program_to_memfd()?;
cmd.arg("--seccomp").arg(fd.to_string());
Ok(())
}