fix(spawner): stage jail spawn files under daemon-owned home, not /var/run #136

Merged
clawdie merged 1 commit from stage-under-daemon-home into main 2026-06-21 17:38:05 +02:00
4 changed files with 26 additions and 17 deletions

View file

@ -121,7 +121,20 @@ impl PrivMode {
/// Default path to the setuid jail-spawn helper used in `PrivMode::Helper`. /// Default path to the setuid jail-spawn helper used in `PrivMode::Helper`.
pub const DEFAULT_JAIL_HELPER: &str = "/usr/local/libexec/colibri-jail-spawn"; pub const DEFAULT_JAIL_HELPER: &str = "/usr/local/libexec/colibri-jail-spawn";
const STAGED_JAIL_RUN_DIR: &str = "/var/run/colibri-stage";
/// Jail-relative directory under which the daemon stages per-spawn `launch.sh` /
/// `env.sh` files. It lives under the daemon user's home (`clawdie`) rather than
/// root-owned `/var/run`, so the daemon can create per-spawn subdirs itself with
/// no privileged pre-creation step — and `/home` is persistent, unlike a tmpfs
/// `/var/run`. Overridable via `COLIBRI_JAIL_STAGE_DIR`.
const DEFAULT_JAIL_STAGE_DIR: &str = "/home/clawdie/.cache/colibri/stage";
fn staged_jail_run_dir() -> String {
std::env::var("COLIBRI_JAIL_STAGE_DIR")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_JAIL_STAGE_DIR.to_string())
}
/// Optional FreeBSD jail confinement for a spawned agent. /// Optional FreeBSD jail confinement for a spawned agent.
/// ///
@ -383,7 +396,7 @@ pub async fn prepare_spawn_command(
) )
})?; })?;
let jail_stage_dir = format!("{STAGED_JAIL_RUN_DIR}/{stage_id}"); let jail_stage_dir = format!("{}/{stage_id}", staged_jail_run_dir());
let host_stage_dir = Path::new(stage_root).join(jail_stage_dir.trim_start_matches('/')); let host_stage_dir = Path::new(stage_root).join(jail_stage_dir.trim_start_matches('/'));
tokio::fs::create_dir_all(&host_stage_dir).await?; tokio::fs::create_dir_all(&host_stage_dir).await?;
@ -1030,12 +1043,12 @@ mod jail_tests {
assert_eq!(prepared.argv[1], "/bin/sh"); assert_eq!(prepared.argv[1], "/bin/sh");
assert_eq!( assert_eq!(
prepared.argv[2], prepared.argv[2],
"/var/run/colibri-stage/agent-test/launch.sh" "/home/clawdie/.cache/colibri/stage/agent-test/launch.sh"
); );
assert!(prepared.env.is_empty()); assert!(prepared.env.is_empty());
let launcher = root.join("var/run/colibri-stage/agent-test/launch.sh"); let launcher = root.join("home/clawdie/.cache/colibri/stage/agent-test/launch.sh");
let env_file = root.join("var/run/colibri-stage/agent-test/env.sh"); let env_file = root.join("home/clawdie/.cache/colibri/stage/agent-test/env.sh");
assert!(launcher.exists()); assert!(launcher.exists());
assert!(env_file.exists()); assert!(env_file.exists());
let env_raw = std::fs::read_to_string(env_file).unwrap(); let env_raw = std::fs::read_to_string(env_file).unwrap();

View file

@ -65,7 +65,9 @@ just `jexec`. We choose the escalation per host via `PrivMode`
When a jailed spawn needs env vars or a working dir, `prepare_spawn_command()` When a jailed spawn needs env vars or a working dir, `prepare_spawn_command()`
writes a 0600 `env.sh` (sorted, single-quoted exports) and a `launch.sh` wrapper writes a 0600 `env.sh` (sorted, single-quoted exports) and a `launch.sh` wrapper
into a staged directory under the jail's `root_path` at into a staged directory under the jail's `root_path` at
`/var/run/colibri-stage/<id>/`. The jail command runs `/bin/sh launch.sh`, which `/home/clawdie/.cache/colibri/stage/<id>/` (the daemon user's home, so the
daemon creates it with no privileged step; overridable via
`COLIBRI_JAIL_STAGE_DIR`). The jail command runs `/bin/sh launch.sh`, which
sources the env file and `cd`s to the working dir before `exec`-ing the agent sources the env file and `cd`s to the working dir before `exec`-ing the agent
binary. This bypasses the env-passthrough problem entirely — no reliance on binary. This bypasses the env-passthrough problem entirely — no reliance on
`jexec`/`mdo` inheriting env vars. `jexec`/`mdo` inheriting env vars.

View file

@ -30,9 +30,11 @@ For jailed spawns with environment variables, the daemon's
created by a previous run (as root) and was mode 755 root:wheel. created by a previous run (as root) and was mode 755 root:wheel.
The daemon runs as `clawdie` and could not write staging files there. The daemon runs as `clawdie` and could not write staging files there.
**Fix:** `chmod 777 <jail_root>/var/run/colibri-stage` (or, better: **Fix (history):** initially `chmod 777 <jail_root>/var/run/colibri-stage`, then
`agent-jail-bootstrap.sh` should pre-create this directory with appropriate `agent-jail-bootstrap.sh` pre-created it clawdie-owned `0700` (#134). **Final
ownership). (#135):** staging moved out of root-owned `/var/run` to the daemon user's home
at `/home/clawdie/.cache/colibri/stage/<id>/`, so the daemon creates it itself
with no privileged pre-creation step (overridable via `COLIBRI_JAIL_STAGE_DIR`).
## The Winning Spawn ## The Winning Spawn

View file

@ -11,7 +11,6 @@ set -eu
JAIL_NAME="${1:-}" JAIL_NAME="${1:-}"
PKG_CACHE_DIR="${PKG_CACHE_DIR:-/var/cache/pkg}" PKG_CACHE_DIR="${PKG_CACHE_DIR:-/var/cache/pkg}"
DAEMON_USER="${DAEMON_USER:-clawdie}"
# The jail name becomes a path component, so reject anything that could escape # The jail name becomes a path component, so reject anything that could escape
# /usr/local/bastille/jails/<name>/root (empty, traversal, odd characters). # /usr/local/bastille/jails/<name>/root (empty, traversal, odd characters).
@ -98,11 +97,4 @@ if ! grep -q '/etc/profile.d/clawdie-npm.sh' "${JAIL_ROOT}/etc/profile" 2>/dev/n
>> "${JAIL_ROOT}/etc/profile" >> "${JAIL_ROOT}/etc/profile"
fi fi
# Pre-create the daemon's per-spawn staging directory. The daemon runs as
# ${DAEMON_USER} and stages launch.sh/env.sh under <stage_id> subdirs here, so
# it must own this directory. Created clawdie-owned 0700 rather than left for a
# root-owned /var/run to block (the spawn EACCES) or patched world-writable.
install -d -o "${DAEMON_USER}" -g "${DAEMON_USER}" -m 0700 \
"${JAIL_ROOT}/var/run/colibri-stage"
echo "Done — ${JAIL_NAME} ready for vault provision." echo "Done — ${JAIL_NAME} ready for vault provision."