Bind production cold start to installed Lazaret authorities

This commit is contained in:
ms2k 2026-07-16 20:41:40 +09:00
commit bc04112c77
14 changed files with 573 additions and 25 deletions

2
Cargo.lock generated
View file

@ -1245,6 +1245,7 @@ dependencies = [
"lazaret-core",
"lazaret-escalation-client",
"lazaret-linux",
"lazaret-prepared-lower",
"lazaret-runtime-supervisor",
"lazaret-state-anchor",
"lazaret-storage-catalog",
@ -1335,6 +1336,7 @@ dependencies = [
"lazaret-linux",
"postcard",
"redb",
"rustix",
"serde",
"tempfile",
]

View file

@ -19,6 +19,7 @@ ed25519-dalek.workspace = true
getrandom.workspace = true
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
lazaret-prepared-lower = { version = "=0.0.0", path = "../lazaret-prepared-lower" }
lazaret-runtime-supervisor = { version = "=0.0.0", path = "../lazaret-runtime-supervisor" }
lazaret-state-anchor = { version = "=0.0.0", path = "../lazaret-state-anchor" }
lazaret-storage-catalog = { version = "=0.0.0", path = "../lazaret-storage-catalog" }

View file

@ -7,5 +7,7 @@ pub mod escalation;
pub mod lifecycle;
pub mod network;
pub mod production;
pub mod production_selector;
pub mod runtime;
pub mod sandbox_init;
pub mod state_anchor;

View file

@ -1,5 +1,8 @@
fn main() {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
if lazaret_host_coordinator::sandbox_init::dispatch_private_sandbox_init(&arguments) {
return;
}
if let Some(result) = lazaret_linux::process::dispatch_private_process_role(&arguments) {
if let Err(error) = result {
eprintln!("private process role failed: {error}");

View file

@ -66,6 +66,7 @@ pub struct ProductionColdStartInputs<'a> {
pub container_id: ContainerId,
pub invoking_uid: u32,
pub escalation_client: &'a File,
pub sandbox_init: &'a File,
}
pub struct BootstrapOwnerGuard {
@ -138,6 +139,7 @@ impl<'a> ProductionColdStart<'a> {
inputs.configuration.runtime_domain_root(),
inputs.container_id,
inputs.escalation_client,
inputs.sandbox_init,
inputs.invoking_uid,
)
.map_err(|error| failure(format!("runtime staging: {error}")))?;
@ -293,6 +295,7 @@ impl RuntimeStaging {
runtime_domain: &ConfinedPathPlan,
container_id: ContainerId,
escalation_client: &File,
sandbox_init: &File,
invoking_uid: u32,
) -> Result<Self, ProductionColdStartError> {
let root = runtime_domain.open_authority().map_err(failure)?;
@ -325,11 +328,15 @@ impl RuntimeStaging {
bundle_identity,
control_identity,
};
staging.populate_control(escalation_client)?;
staging.populate_control(escalation_client, sandbox_init)?;
Ok(staging)
}
fn populate_control(&self, escalation_client: &File) -> Result<(), ProductionColdStartError> {
fn populate_control(
&self,
escalation_client: &File,
sandbox_init: &File,
) -> Result<(), ProductionColdStartError> {
let invoking_uid = fstat(&self.control).map_err(failure)?.st_uid;
drop(open_or_create_private_directory(
&self.control,
@ -352,15 +359,10 @@ impl RuntimeStaging {
true,
)?);
let bin = open_or_create_private_directory(&self.control, "bin", invoking_uid, true)?;
let source = escalation_client
.metadata()
.map_err(|error| failure(format!("inspecting escalation client: {error}")))?;
if !source.file_type().is_file() || source.permissions().mode() & 0o111 == 0 {
return Err(ProductionColdStartError(
"escalation client is not an executable regular file".into(),
));
}
require_executable_regular_file(escalation_client, "escalation client")?;
copy_descriptor(escalation_client, &bin, "lazaret-escalate", invoking_uid)?;
require_executable_regular_file(sandbox_init, "sandbox init")?;
copy_descriptor(sandbox_init, &bin, "lazaret-sandbox-init", invoking_uid)?;
Ok(())
}
@ -414,6 +416,21 @@ fn copy_descriptor(
require_private_file(&destination, invoking_uid, 0o700)
}
fn require_executable_regular_file(
source: &File,
label: &str,
) -> Result<(), ProductionColdStartError> {
let metadata = source
.metadata()
.map_err(|error| failure(format!("inspecting {label}: {error}")))?;
if !metadata.file_type().is_file() || metadata.permissions().mode() & 0o111 == 0 {
return Err(ProductionColdStartError(format!(
"{label} is not an executable regular file"
)));
}
Ok(())
}
#[derive(Clone, Copy)]
struct FileIdentity {
device: u64,

View file

@ -0,0 +1,405 @@
use std::collections::BTreeSet;
use std::fmt;
use std::fs::File;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::Arc;
use lazaret_core::configuration::ResolvedDomainConfiguration;
use lazaret_core::identity::{
ConfigurationFingerprint, ContainerId, CoordinatorId, DomainId, GenerationId, NetworkSidecarId,
PrepareOperationId, SandboxId,
};
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact};
use lazaret_linux::filesystem::namespace_identity;
use lazaret_prepared_lower::{
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedLowerStore, RegistryCredentials,
};
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
use lazaret_runtime_supervisor::sandbox::{RootProcess, SANDBOX_INIT_DESTINATION};
use lazaret_storage_catalog::{Catalog, CatalogError};
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
use crate::domain_store::HostCoordinatorDomainStore;
use crate::production::ProductionColdStartInputs;
use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
#[derive(Debug)]
pub struct ProductionSelectionError(String);
impl fmt::Display for ProductionSelectionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for ProductionSelectionError {}
fn failure(error: impl fmt::Display) -> ProductionSelectionError {
ProductionSelectionError(error.to_string())
}
pub struct InstalledProductPaths<'a> {
pub escalation_client: &'a Path,
pub sandbox_init: &'a Path,
}
pub struct ProductionSelection {
domain_id: DomainId,
configuration_fingerprint: ConfigurationFingerprint,
catalog: Catalog,
artifact: PreparedArtifact,
generation: Generation,
resolver_addresses: Vec<IpAddress>,
network_probe: NetworkProbeRequest,
reachability: ReachabilityExpectation,
container_id: ContainerId,
escalation_client: File,
sandbox_init: File,
}
impl ProductionSelection {
/// Borrows the selected resources as the existing production cold-start input.
///
/// # Errors
/// Refuses a configuration other than the exact one used for selection.
pub fn cold_start_inputs<'a>(
&'a self,
store: Arc<HostCoordinatorDomainStore>,
configuration: &'a ResolvedDomainConfiguration,
) -> Result<ProductionColdStartInputs<'a>, ProductionSelectionError> {
if configuration.domain_id() != self.domain_id
|| configuration.configuration_fingerprint() != self.configuration_fingerprint
{
return Err(failure("selected cold start does not match configuration"));
}
Ok(ProductionColdStartInputs {
store,
catalog: &self.catalog,
configuration,
generation: self.generation.clone(),
prepared_artifact: &self.artifact,
root_process: RootProcess::new(
SANDBOX_INIT_DESTINATION,
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
)
.map_err(failure)?,
resolver_addresses: self.resolver_addresses.clone(),
network_probe: self.network_probe.clone(),
reachability: self.reachability.clone(),
container_id: self.container_id,
invoking_uid: rustix::process::getuid().as_raw(),
escalation_client: &self.escalation_client,
sandbox_init: &self.sandbox_init,
})
}
}
/// Selects and prepares every Host Coordinator-owned cold-start input.
///
/// # Errors
/// Refuses invalid installed paths, unsupported architectures, registry or image failures,
/// changed Storage Root authority, exhausted identities, and configuration-policy mismatch.
pub async fn select_production_cold_start(
configuration: &ResolvedDomainConfiguration,
store: &HostCoordinatorDomainStore,
product_paths: InstalledProductPaths<'_>,
) -> Result<ProductionSelection, ProductionSelectionError> {
let escalation_client = open_installed_executable(product_paths.escalation_client)?;
let sandbox_init = open_installed_executable(product_paths.sandbox_init)?;
let catalog = Catalog::under_storage_root(configuration.storage_root()).map_err(failure)?;
match catalog.initialize() {
Ok(()) | Err(CatalogError::AlreadyInitialized) => {}
Err(error) => return Err(failure(error)),
}
let prepared =
PreparedLowerStore::under_storage_root(configuration.storage_root(), catalog.clone())
.map_err(failure)?;
let image = OciRegistryTransport
.pull_confined(
&configuration.fingerprint_record().image,
&RegistryCredentials::Anonymous,
&host_platform()?,
configuration.storage_root(),
random_id()?,
)
.await
.map_err(|error| failure(format!("pulling configured anonymous OCI image: {error}")))?;
let artifact = prepared
.prepare_transported(
PrepareOperationId::new(random_id()?),
&image,
rootless_id_map(),
)
.map_err(failure)?;
image.cleanup().map_err(failure)?;
let next_generation = store
.snapshot()
.map_err(failure)?
.generations
.iter()
.map(|generation| generation.generation_id.get())
.max()
.unwrap_or(0)
.checked_add(1)
.ok_or_else(|| failure("generation identity exhausted"))?;
let (coordinator_id, network_sidecar_id, sandbox_id) = distinct_generation_ids()?;
let generation = Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(next_generation),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id,
network_sidecar_id,
sandbox_id,
durable_decision: DurableDecision::None,
};
let namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH)).map_err(failure)?;
let (resolver_addresses, network_probe, reachability) =
select_network_readiness(configuration, &generation, namespace);
Ok(ProductionSelection {
domain_id: configuration.domain_id(),
configuration_fingerprint: configuration.configuration_fingerprint(),
catalog,
artifact,
generation,
resolver_addresses,
network_probe,
reachability,
container_id: ContainerId::new(random_id()?),
escalation_client,
sandbox_init,
})
}
fn select_network_readiness(
configuration: &ResolvedDomainConfiguration,
generation: &Generation,
enforcement_namespace: lazaret_core::identity::NamespaceIdentity,
) -> (Vec<IpAddress>, NetworkProbeRequest, ReachabilityExpectation) {
let readiness = configuration.network_readiness();
let denied = readiness
.denied
.iter()
.map(|target| TcpProbeTarget {
address: IpAddress::V4(target.address),
port: target.port,
})
.collect::<Vec<_>>();
let permitted = readiness
.permitted
.iter()
.map(|target| TcpProbeTarget {
address: IpAddress::V4(target.address),
port: target.port,
})
.collect::<Vec<_>>();
let dns = readiness
.dns
.iter()
.map(|target| DnsProbeTarget {
resolver: IpAddress::V4(target.resolver),
query_name: target.query.clone(),
})
.collect::<Vec<_>>();
let reachability = ReachabilityExpectation {
denied_destinations: denied.iter().map(|target| target.address).collect(),
permitted_destinations: permitted.iter().map(|target| target.address).collect(),
dns_queries: dns
.iter()
.map(|target| DnsQueryExpectation {
resolver: target.resolver,
query_name: target.query_name.clone(),
})
.collect(),
};
let resolvers = dns
.iter()
.map(|target| target.resolver)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
(
resolvers,
NetworkProbeRequest {
domain_id: generation.domain_id,
generation_id: generation.generation_id,
enforcement_namespace,
denied,
permitted,
dns,
},
reachability,
)
}
fn open_installed_executable(path: &Path) -> Result<File, ProductionSelectionError> {
let descriptor = openat2(
rustix::fs::CWD,
path,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
)
.map_err(failure)?;
let file = File::from(descriptor);
let metadata = file.metadata().map_err(failure)?;
if !metadata.is_file() || metadata.permissions().mode() & 0o111 == 0 {
return Err(failure(
"installed product path is not an executable regular file",
));
}
Ok(file)
}
fn rootless_id_map() -> IdMap {
IdMap {
uid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getuid().as_raw(),
size: 1,
}],
gid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getgid().as_raw(),
size: 1,
}],
}
}
fn host_platform() -> Result<Platform, ProductionSelectionError> {
match std::env::consts::ARCH {
"x86_64" => Ok(Platform::linux("amd64")),
"aarch64" => Ok(Platform::linux("arm64")),
architecture => Err(failure(format!(
"host architecture {architecture} has no selected OCI platform mapping"
))),
}
}
fn distinct_generation_ids()
-> Result<(CoordinatorId, NetworkSidecarId, SandboxId), ProductionSelectionError> {
for _ in 0..8 {
let coordinator = CoordinatorId::new(random_id()?);
let network = NetworkSidecarId::new(random_id()?);
let sandbox = SandboxId::new(random_id()?);
if coordinator.as_bytes() != network.as_bytes()
&& coordinator.as_bytes() != sandbox.as_bytes()
&& network.as_bytes() != sandbox.as_bytes()
{
return Ok((coordinator, network, sandbox));
}
}
Err(failure(
"fresh generation role identities repeatedly collided",
))
}
fn random_id() -> Result<[u8; 32], ProductionSelectionError> {
let mut bytes = [0; 32];
getrandom::fill(&mut bytes).map_err(failure)?;
Ok(bytes)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::fs;
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
use lazaret_core::identity::{DeviceId, InodeId, NamespaceIdentity};
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
use super::*;
#[test]
fn resolved_readiness_maps_exactly_and_rootless_id_map_matches_builder_policy() {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let generation = Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(9),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([1; 32]),
network_sidecar_id: NetworkSidecarId::new([2; 32]),
sandbox_id: SandboxId::new([3; 32]),
durable_decision: DurableDecision::None,
};
let namespace = NamespaceIdentity {
device: DeviceId::new(4),
inode: InodeId::new(5),
};
let (resolvers, request, expectation) =
select_network_readiness(&configuration, &generation, namespace);
assert_eq!(resolvers, [IpAddress::V4([198, 51, 100, 53])]);
assert_eq!(request.denied[0].port, 8443);
assert_eq!(request.permitted[0].port, 443);
assert_eq!(request.dns[0].query_name, "example.invalid");
assert_eq!(expectation.denied_destinations, [request.denied[0].address]);
assert_eq!(
expectation.permitted_destinations,
[request.permitted[0].address]
);
assert_eq!(expectation.dns_queries[0].resolver, request.dns[0].resolver);
let id_map = rootless_id_map();
assert_eq!(id_map.uid[0].container_id, 0);
assert_eq!(
id_map.uid[0].namespace_id,
rustix::process::getuid().as_raw()
);
assert_eq!(
id_map.gid[0].namespace_id,
rustix::process::getgid().as_raw()
);
}
#[test]
fn installed_product_selection_requires_exact_executable_and_fresh_roles() {
let tree = tempfile::tempdir().unwrap();
let executable = tree.path().join("product");
fs::copy("/bin/true", &executable).unwrap();
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
assert!(open_installed_executable(&executable).is_ok());
fs::set_permissions(&executable, fs::Permissions::from_mode(0o600)).unwrap();
assert!(open_installed_executable(&executable).is_err());
let (coordinator, network, sandbox) = distinct_generation_ids().unwrap();
assert_ne!(coordinator.as_bytes(), network.as_bytes());
assert_ne!(coordinator.as_bytes(), sandbox.as_bytes());
assert_ne!(network.as_bytes(), sandbox.as_bytes());
assert!(host_platform().is_ok());
}
fn configuration(root: &Path) -> ResolvedDomainConfiguration {
let configuration_directory = root.join("configuration");
let workspace = root.join("workspace");
let runtime = root.join("runtime");
fs::create_dir(&configuration_directory).unwrap();
fs::create_dir(&workspace).unwrap();
fs::create_dir(&runtime).unwrap();
let configuration_path = configuration_directory.join("lazaret.toml");
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"Example.Invalid.\" }}]\n",
root.join("data").display()
),
)
.unwrap();
load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(configuration_path),
selected_profile: "default".into(),
workspace,
home: root.join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment: BTreeMap::new(),
sandbox_layout: production_sandbox_layout().unwrap(),
})
.unwrap()
}
}

View file

@ -0,0 +1,38 @@
use std::ffi::{OsStr, OsString};
pub const PRIVATE_SANDBOX_INIT_ROLE: &str = "--lazaret-private-sandbox-init-v1";
#[must_use]
pub fn is_private_sandbox_init_role(arguments: &[OsString]) -> bool {
arguments.len() == 1 && arguments[0] == OsStr::new(PRIVATE_SANDBOX_INIT_ROLE)
}
#[must_use]
pub fn dispatch_private_sandbox_init(arguments: &[OsString]) -> bool {
if !is_private_sandbox_init_role(arguments) {
return false;
}
loop {
std::thread::park();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_role_dispatch_is_exact_and_closed() {
assert!(is_private_sandbox_init_role(&[OsString::from(
PRIVATE_SANDBOX_INIT_ROLE
)]));
assert!(!is_private_sandbox_init_role(&[]));
assert!(!is_private_sandbox_init_role(&[
OsString::from(PRIVATE_SANDBOX_INIT_ROLE),
OsString::from("extra"),
]));
assert!(!is_private_sandbox_init_role(&[OsString::from(
"--private-sandbox-init"
)]));
}
}

View file

@ -577,7 +577,7 @@ fn configuration(tree: &TestTree) -> lazaret_core::configuration::ResolvedDomain
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]\n",
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
tree.path.join("data").display()
),
)

View file

@ -199,6 +199,7 @@ impl ProductionFixture {
container_id: self.container_id,
invoking_uid: getuid().as_raw(),
escalation_client,
sandbox_init: escalation_client,
}
}
@ -242,7 +243,7 @@ fn configuration(root: &Path) -> lazaret_core::configuration::ResolvedDomainConf
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\npersistent_dirs = []\n",
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\npersistent_dirs = []\n\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
root.join("data").display()
),
)

View file

@ -629,7 +629,7 @@ impl Fixture {
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[profiles.default]\nimage = \"example.invalid/runtime-fixture:v1\"\npersistent_dirs = [\"/tooling\", \"/fixtures\"]\nenv = {{ PATH = \"env:BENCHMARK_PATH\", CARGO_HOME = \"env:BENCHMARK_CARGO_HOME\", RUSTUP_HOME = \"env:BENCHMARK_RUSTUP_HOME\", CARGO_BUILD_JOBS = \"env:BENCHMARK_CARGO_BUILD_JOBS\" }}\nvolumes = [\n {{ host = \"{}\", container = \"/tooling/usr\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/fixtures/host\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/fixtures/output\", kind = \"directory\", access = \"read-write\" }},\n {{ host = \"{}\", container = \"/fixtures/repository\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/tooling/cargo\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/tooling/rustup\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/tooling/uv-bin\", kind = \"directory\", access = \"read-only\" }},\n]\n",
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n\n[profiles.default]\nimage = \"example.invalid/runtime-fixture:v1\"\npersistent_dirs = [\"/tooling\", \"/fixtures\"]\nenv = {{ PATH = \"env:BENCHMARK_PATH\", CARGO_HOME = \"env:BENCHMARK_CARGO_HOME\", RUSTUP_HOME = \"env:BENCHMARK_RUSTUP_HOME\", CARGO_BUILD_JOBS = \"env:BENCHMARK_CARGO_BUILD_JOBS\" }}\nvolumes = [\n {{ host = \"{}\", container = \"/tooling/usr\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/fixtures/host\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/fixtures/output\", kind = \"directory\", access = \"read-write\" }},\n {{ host = \"{}\", container = \"/fixtures/repository\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/tooling/cargo\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/tooling/rustup\", kind = \"directory\", access = \"read-only\" }},\n {{ host = \"{}\", container = \"/tooling/uv-bin\", kind = \"directory\", access = \"read-only\" }},\n]\n\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
root.join("data").display(),
Path::new("/usr").display(),
host_root.display(),
@ -708,7 +708,7 @@ impl Fixture {
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[profiles.default]\nimage = \"example.invalid/runtime-fixture:v1\"\npersistent_dirs = []\n",
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n\n[profiles.default]\nimage = \"example.invalid/runtime-fixture:v1\"\npersistent_dirs = []\n\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
root.join("data").display()
),
)?;
@ -1026,7 +1026,7 @@ fn network_configuration(
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[profiles.default]\nimage = \"example.invalid/network-fixture:v1\"\npersistent_dirs = []\n",
"schema = \"lazaret-trusted-host-config-v1\"\n\n[storage]\ndata_root = \"{}\"\n\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n\n[profiles.default]\nimage = \"example.invalid/network-fixture:v1\"\npersistent_dirs = []\n\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
root.join("data").display()
),
)?;

View file

@ -24,6 +24,7 @@ pub const CREDENTIALS_DESTINATION: &str = "/run/lazaret/credentials";
pub const MAILBOX_REQUESTS_DESTINATION: &str = "/run/lazaret/mailbox/requests";
pub const MAILBOX_RESULTS_DESTINATION: &str = "/run/lazaret/mailbox/results";
pub const ESCALATION_CLIENT_DESTINATION: &str = "/run/lazaret/bin/lazaret-escalate";
pub const SANDBOX_INIT_DESTINATION: &str = "/run/lazaret/bin/lazaret-sandbox-init";
pub const TERMINFO_ROOT_DESTINATION: &str = "/run/lazaret/terminfo";
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
@ -678,6 +679,10 @@ fn handoff_runtime_sources(
container.join("bin/lazaret-escalate"),
HandoffPathType::RegularFile,
),
(
container.join("bin/lazaret-sandbox-init"),
HandoffPathType::RegularFile,
),
]
.into_iter()
.map(|(relative_path, expected_type)| {
@ -851,16 +856,22 @@ fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
.map_err(oci_error),
))
.collect::<Result<Vec<_>, _>>()?;
let identity_mapping = LinuxIdMappingBuilder::default()
let uid_mapping = LinuxIdMappingBuilder::default()
.container_id(0_u32)
.host_id(0_u32)
.host_id(rustix::process::getuid().as_raw())
.size(1_u32)
.build()
.map_err(oci_error)?;
let gid_mapping = LinuxIdMappingBuilder::default()
.container_id(0_u32)
.host_id(rustix::process::getgid().as_raw())
.size(1_u32)
.build()
.map_err(oci_error)?;
LinuxBuilder::default()
.namespaces(private_namespaces)
.uid_mappings(vec![identity_mapping])
.gid_mappings(vec![identity_mapping])
.uid_mappings(vec![uid_mapping])
.gid_mappings(vec![gid_mapping])
.rootfs_propagation("rprivate")
.build()
.map_err(oci_error)
@ -1021,6 +1032,12 @@ fn control_mounts(control_sources: &Path) -> Result<Vec<Mount>, SandboxBuildErro
true,
false,
),
(
"bin/lazaret-sandbox-init",
SANDBOX_INIT_DESTINATION,
true,
false,
),
]
.into_iter()
.map(|(source, destination, read_only, no_exec)| {

View file

@ -11,8 +11,9 @@ use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
use lazaret_runtime_supervisor::sandbox::{
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, HostTerminal,
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH, RootProcess,
SandboxBuildErrorKind, SandboxBuildRequest, TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
build_sandbox_bundle, production_sandbox_layout, stage_host_terminal,
SANDBOX_INIT_DESTINATION, SandboxBuildErrorKind, SandboxBuildRequest,
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION, build_sandbox_bundle,
production_sandbox_layout, stage_host_terminal,
};
use serde_json::Value;
use tempfile::TempDir;
@ -41,6 +42,9 @@ impl Fixture {
[storage]
data_root = "{}"
[network]
default_allow = ["192.0.2.10/32"]
[profiles.default]
image = "example.invalid/agent:v1"
persistent_dirs = ["/root"]
@ -50,6 +54,11 @@ volumes = [
{{ host = "{{profile_dir}}/read-write", container = "/root/read-write", kind = "directory", access = "read-write", create = true }},
]
[profiles.default.readiness]
denied = [{{ address = "10.20.30.40", port = 443 }}]
permitted = [{{ address = "192.0.2.10", port = 443 }}]
dns = [{{ resolver = "198.51.100.53", query = "example.invalid" }}]
[profiles.unselected]
image = "example.invalid/unselected:v1"
env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
@ -172,6 +181,13 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
b"executable",
)
.unwrap();
fs::write(
runtime.join(format!(
"sandbox-control/{container}/bin/lazaret-sandbox-init"
)),
b"executable",
)
.unwrap();
}
fn bundle_config_path(&self) -> PathBuf {
@ -274,11 +290,15 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
assert_eq!(namespaces[5]["path"], NETWORK_NAMESPACE_PATH);
assert_eq!(
specification["linux"]["uidMappings"],
serde_json::json!([{ "hostID": 0, "containerID": 0, "size": 1 }])
serde_json::json!([{
"hostID": rustix::process::getuid().as_raw(), "containerID": 0, "size": 1
}])
);
assert_eq!(
specification["linux"]["gidMappings"],
serde_json::json!([{ "hostID": 0, "containerID": 0, "size": 1 }])
serde_json::json!([{
"hostID": rustix::process::getgid().as_raw(), "containerID": 0, "size": 1
}])
);
assert_eq!(first.command().program(), OsStr::new("runsc"));
@ -328,6 +348,48 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
}));
}
#[test]
fn product_init_role_has_exact_terminal_neutral_rootless_bundle_shape() {
let fixture = Fixture::new();
let root = RootProcess::new(
SANDBOX_INIT_DESTINATION,
["--lazaret-private-sandbox-init-v1".to_owned()],
)
.unwrap();
let specification = value(&build_sandbox_bundle(fixture.request(root)).unwrap());
assert_eq!(specification["process"]["terminal"], false);
assert_eq!(
specification["process"]["args"],
serde_json::json!([
SANDBOX_INIT_DESTINATION,
"--lazaret-private-sandbox-init-v1"
])
);
assert_eq!(specification["process"]["user"]["uid"], 0);
assert_eq!(specification["process"]["user"]["gid"], 0);
assert_eq!(
specification["linux"]["uidMappings"],
serde_json::json!([{
"hostID": rustix::process::getuid().as_raw(), "containerID": 0, "size": 1
}])
);
assert_eq!(
mounts(&specification)[SANDBOX_INIT_DESTINATION]["source"],
host_path(
fixture
.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
)
.join("sandbox-control")
.join("11".repeat(32))
.join("bin/lazaret-sandbox-init")
.display()
.to_string()
);
}
#[test]
fn selected_environment_volume_modes_and_host_terminal_are_preserved() {
let fixture = Fixture::new();

View file

@ -129,7 +129,7 @@ fn configuration(tree: &TestTree) -> lazaret_core::configuration::ResolvedDomain
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]\n",
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
tree.path.join("data").display()
),
)

View file

@ -271,7 +271,7 @@ fn configuration(
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\", \"/etc\"]\n",
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\", \"/etc\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
tree.path.join("data").display()
),
)?;