mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
fix(security): sandbox the PowerShell module install step under nsjail
The PowerShell module-install step ran `pwsh -Command <install_string>` directly via start_child_process with no nsjail wrapping, unlike the run step which gates on is_sandboxing_enabled(). Defense-in-depth: wrap the install step in nsjail when sandboxing is enabled, using a dedicated install proto modeled on run.powershell.config.proto but with the module cache mounted rw (so Save-PSResource can still populate it) and the per-job script/result mounts dropped. Smoke-tested locally: nsjail starts, pwsh runs inside the jail (jailed /proc/1), and writes to the rw cache mount succeed. NOT yet validated end-to-end is module download from PSGallery / a private repo through the cloud tracing proxy — hence draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
name: "powershell install"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "powershell"
|
||||
log_level: ERROR
|
||||
time_limit: {TIMEOUT}
|
||||
|
||||
disable_rl: true
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
|
||||
skip_setsid: true
|
||||
keep_caps: false
|
||||
keep_env: true
|
||||
mount_proc: true
|
||||
|
||||
mount {
|
||||
src: "/bin"
|
||||
dst: "/bin"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/proc/self/fd"
|
||||
dst: "/dev/fd"
|
||||
is_symlink: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/opt/microsoft"
|
||||
dst: "/opt/microsoft"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/lib"
|
||||
dst: "/lib"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "/lib64"
|
||||
dst: "/lib64"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "/usr"
|
||||
dst: "/usr"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/null"
|
||||
dst: "/dev/null"
|
||||
is_bind: true
|
||||
rw: true
|
||||
}
|
||||
|
||||
{TMP_MOUNT_BLOCK}
|
||||
|
||||
mount {
|
||||
src: "/etc"
|
||||
dst: "/etc"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
# Container runtimes bind exactly these 3 files as separate submounts over
|
||||
# /etc; nsjail's ro remount of /etc is non-recursive so they stay writable.
|
||||
# Load-bearing -- do not remove as redundant with the /etc bind above.
|
||||
mount {
|
||||
src: "/etc/resolv.conf"
|
||||
dst: "/etc/resolv.conf"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/etc/hosts"
|
||||
dst: "/etc/hosts"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/etc/hostname"
|
||||
dst: "/etc/hostname"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/random"
|
||||
dst: "/dev/random"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/urandom"
|
||||
dst: "/dev/urandom"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
iface_no_lo: true
|
||||
|
||||
# Writable, unlike the run step: the install step downloads modules into the
|
||||
# shared PowerShell cache here.
|
||||
mount {
|
||||
src: "{CACHE_DIR}"
|
||||
dst: "{CACHE_DIR}"
|
||||
is_bind: true
|
||||
rw: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
envar: "HOME=/tmp"
|
||||
@@ -18,6 +18,8 @@ use crate::SYSTEM_ROOT;
|
||||
|
||||
const NSJAIL_CONFIG_RUN_POWERSHELL_CONTENT: &str =
|
||||
include_str!("../nsjail/run.powershell.config.proto");
|
||||
const NSJAIL_CONFIG_INSTALL_POWERSHELL_CONTENT: &str =
|
||||
include_str!("../nsjail/install.powershell.config.proto");
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE_POWERSHELL_IMPORTS: Regex = Regex::new(r#"^\s*Import-Module\s+(?:-Name\s+)?"?([^\s"]+)"?(?:\s+-RequiredVersion\s+"?([^\s"]+)"?)?"#).unwrap();
|
||||
@@ -563,12 +565,51 @@ pub async fn handle_powershell_job(
|
||||
&powershell_repo_pat.unwrap_or_default(),
|
||||
)
|
||||
.replace("{modules}", &modules_list);
|
||||
let mut cmd = Command::new(POWERSHELL_PATH.as_str());
|
||||
cmd.args(&["-Command", &install_string])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
// Sandbox the install step under nsjail when sandboxing is enabled, matching
|
||||
// the run step. The install step previously always ran with full worker
|
||||
// privileges; the install nsjail config mounts the module cache rw so
|
||||
// Save-PSResource can still write to it.
|
||||
let install_jailed = is_sandboxing_enabled();
|
||||
let mut cmd = if install_jailed {
|
||||
let nsjail_timeout =
|
||||
resolve_nsjail_timeout(db, &job.workspace_id, job.id, job.timeout).await;
|
||||
let nsjail_proto = format!("{}.pwsh_install.config.proto", job.id);
|
||||
let config_content = NSJAIL_CONFIG_INSTALL_POWERSHELL_CONTENT
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
.replace("{CACHE_DIR}", &*POWERSHELL_CACHE_DIR)
|
||||
.replace(
|
||||
"{TMP_MOUNT_BLOCK}",
|
||||
&resolve_nsjail_tmp_mount_block(job_dir).await,
|
||||
)
|
||||
.replace("{TIMEOUT}", &nsjail_timeout);
|
||||
write_file(job_dir, &nsjail_proto, &config_content)?;
|
||||
let mut cmd = Command::new(NSJAIL_PATH.as_str());
|
||||
cmd.current_dir(job_dir).args(&[
|
||||
"--config",
|
||||
&nsjail_proto,
|
||||
"--",
|
||||
POWERSHELL_PATH.as_str(),
|
||||
"-Command",
|
||||
&install_string,
|
||||
]);
|
||||
cmd
|
||||
} else {
|
||||
let mut cmd = Command::new(POWERSHELL_PATH.as_str());
|
||||
cmd.args(&["-Command", &install_string]);
|
||||
cmd
|
||||
};
|
||||
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
|
||||
let child = start_child_process(cmd, POWERSHELL_PATH.as_str(), false).await?;
|
||||
let child = start_child_process(
|
||||
cmd,
|
||||
if install_jailed {
|
||||
NSJAIL_PATH.as_str()
|
||||
} else {
|
||||
POWERSHELL_PATH.as_str()
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
handle_child(
|
||||
&job.id,
|
||||
|
||||
Reference in New Issue
Block a user