use crate::filesystem::{
normalize_container_destination, normalize_provider_config_destination,
normalize_volume_destination, validate_production_rootfs_path, validate_provider_config_source,
validate_workspace_host_path,
};
use crate::isolation::{NamespaceConfig, UserNamespaceConfig};
use crate::network::{CredentialBrokerConfig, EgressPolicy};
use crate::resources::ResourceLimits;
use crate::security::GVisorPlatform;
use std::fs::OpenOptions;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::RawFd;
use std::path::PathBuf;
use std::time::Duration;
pub const DEFAULT_HOME_PATH: &str = "/home/agent";
pub const CREDENTIAL_BROKER_CONTAINER_ID_ENV: &str = "NUCLEUS_CONTAINER_ID";
pub const CREDENTIAL_BROKER_TOKEN_ENV: &str = "NUCLEUS_CREDENTIAL_BROKER_TOKEN";
#[must_use]
fn is_credential_broker_identity_env(key: &str) -> bool {
key == CREDENTIAL_BROKER_CONTAINER_ID_ENV || key == CREDENTIAL_BROKER_TOKEN_ENV
}
fn open_dev_urandom() -> crate::error::Result<std::fs::File> {
let file = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open("/dev/urandom")
.map_err(|e| {
crate::error::NucleusError::ConfigError(format!(
"Failed to open /dev/urandom for container ID generation: {}",
e
))
})?;
let metadata = file.metadata().map_err(|e| {
crate::error::NucleusError::ConfigError(format!("Failed to stat /dev/urandom: {}", e))
})?;
if !metadata.file_type().is_char_device() {
return Err(crate::error::NucleusError::ConfigError(
"/dev/urandom is not a character device".to_string(),
));
}
Ok(file)
}
pub fn generate_container_id() -> crate::error::Result<String> {
use std::io::Read;
let mut buf = [0u8; 16];
let mut file = open_dev_urandom()?;
file.read_exact(&mut buf).map_err(|e| {
crate::error::NucleusError::ConfigError(format!(
"Failed to read secure random bytes for container ID generation: {}",
e
))
})?;
Ok(hex::encode(buf))
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum TrustLevel {
Trusted,
#[default]
Untrusted,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum ServiceMode {
#[default]
Agent,
#[value(name = "strict-agent", alias = "mitos-agent")]
#[serde(alias = "mitos-agent")]
StrictAgent,
Production,
}
impl ServiceMode {
pub fn label(self) -> &'static str {
match self {
Self::Agent => "Agent mode",
Self::StrictAgent => "Strict agent mode",
Self::Production => "Production mode",
}
}
pub fn enforces_strict_isolation(self) -> bool {
matches!(self, Self::StrictAgent | Self::Production)
}
pub fn requires_user_namespace_mapping(self) -> bool {
self.enforces_strict_isolation()
}
pub fn requires_cgroup_enforcement(self) -> bool {
self.enforces_strict_isolation()
}
pub fn requires_explicit_bridge_dns(self) -> bool {
self.enforces_strict_isolation()
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]
pub enum RuntimeSelection {
#[default]
#[value(name = "gvisor")]
#[serde(rename = "gvisor")]
GVisor,
#[value(name = "native")]
#[serde(rename = "native")]
Native,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize,
)]
pub enum NetworkModeArg {
#[value(name = "none")]
#[serde(rename = "none")]
None,
#[value(name = "host")]
#[serde(rename = "host")]
Host,
#[value(name = "gvisor-host")]
#[serde(rename = "gvisor-host")]
GVisorHost,
#[value(name = "bridge")]
#[serde(rename = "bridge")]
Bridge,
}
#[allow(clippy::derivable_impls)]
impl Default for NetworkModeArg {
fn default() -> Self {
Self::None
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize,
)]
pub enum UsernsModeArg {
#[value(name = "nomap")]
#[serde(rename = "nomap")]
NoMap,
#[value(name = "keep-id")]
#[serde(rename = "keep-id")]
KeepId,
#[value(name = "auto")]
#[serde(rename = "auto")]
Auto,
}
#[allow(clippy::derivable_impls)]
impl Default for UsernsModeArg {
fn default() -> Self {
Self::NoMap
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum KernelLockdownMode {
Integrity,
Confidentiality,
}
impl KernelLockdownMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Integrity => "integrity",
Self::Confidentiality => "confidentiality",
}
}
pub fn accepts(self, active: Self) -> bool {
match self {
Self::Integrity => matches!(active, Self::Integrity | Self::Confidentiality),
Self::Confidentiality => matches!(active, Self::Confidentiality),
}
}
}
#[derive(Debug, Clone)]
pub struct HealthCheck {
pub command: Vec<String>,
pub interval: Duration,
pub retries: u32,
pub start_period: Duration,
pub timeout: Duration,
}
impl Default for HealthCheck {
fn default() -> Self {
Self {
command: Vec::new(),
interval: Duration::from_secs(30),
retries: 3,
start_period: Duration::from_secs(5),
timeout: Duration::from_secs(5),
}
}
}
#[derive(Debug, Clone)]
pub struct SecretMount {
pub source: PathBuf,
pub dest: PathBuf,
pub mode: u32,
}
#[derive(Debug, Clone)]
pub struct ProviderConfigMount {
pub source: PathBuf,
pub dest: PathBuf,
pub read_only: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcessIdentity {
pub uid: u32,
pub gid: u32,
pub additional_gids: Vec<u32>,
}
impl ProcessIdentity {
pub fn root() -> Self {
Self {
uid: 0,
gid: 0,
additional_gids: Vec::new(),
}
}
pub fn is_root(&self) -> bool {
self.uid == 0 && self.gid == 0 && self.additional_gids.is_empty()
}
}
impl Default for ProcessIdentity {
fn default() -> Self {
Self::root()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConsoleSize {
pub width: u16,
pub height: u16,
}
impl ConsoleSize {
pub const DEFAULT_WIDTH: u16 = 80;
pub const DEFAULT_HEIGHT: u16 = 24;
pub fn detect() -> Self {
Self::from_fd(libc::STDIN_FILENO)
.or_else(Self::from_env)
.unwrap_or_default()
}
fn from_fd(fd: RawFd) -> Option<Self> {
let mut winsize = libc::winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
let ret = unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, &mut winsize) };
if ret == 0 && winsize.ws_col > 0 && winsize.ws_row > 0 {
Some(Self {
width: winsize.ws_col,
height: winsize.ws_row,
})
} else {
None
}
}
fn from_env() -> Option<Self> {
let width = std::env::var("COLUMNS").ok()?.parse::<u16>().ok()?;
let height = std::env::var("LINES").ok()?.parse::<u16>().ok()?;
if width > 0 && height > 0 {
Some(Self { width, height })
} else {
None
}
}
}
impl Default for ConsoleSize {
fn default() -> Self {
Self {
width: Self::DEFAULT_WIDTH,
height: Self::DEFAULT_HEIGHT,
}
}
}
#[derive(Debug, Clone)]
pub enum VolumeSource {
Bind { source: PathBuf },
Tmpfs { size: Option<String> },
}
#[derive(Debug, Clone)]
pub struct VolumeMount {
pub source: VolumeSource,
pub dest: PathBuf,
pub read_only: bool,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum RootfsMode {
#[default]
Bind,
Overlay,
}
#[derive(Debug, Clone)]
pub struct RootfsOverlayConfig {
pub upperdir: PathBuf,
pub workdir: PathBuf,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum WorkspaceMode {
#[default]
#[value(name = "bind-rw")]
BindRw,
#[value(name = "bind-ro")]
BindRo,
#[value(name = "copy-in-out")]
CopyInOut,
}
#[derive(Debug, Clone)]
pub struct WorkspaceConfig {
pub host_path: Option<PathBuf>,
pub container_path: PathBuf,
pub mode: WorkspaceMode,
pub allow_execute: bool,
pub staging_path: Option<PathBuf>,
}
impl WorkspaceConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_host_path(mut self, host_path: PathBuf) -> Self {
self.host_path = Some(host_path);
self
}
pub fn with_mode(mut self, mode: WorkspaceMode) -> Self {
self.mode = mode;
self
}
pub fn with_allow_execute(mut self, allow_execute: bool) -> Self {
self.allow_execute = allow_execute;
self
}
pub fn with_staging_path(mut self, staging_path: PathBuf) -> Self {
self.staging_path = Some(staging_path);
self
}
pub fn is_read_only(&self) -> bool {
matches!(self.mode, WorkspaceMode::BindRo)
}
pub fn is_writable(&self) -> bool {
!self.is_read_only()
}
pub fn effective_host_path(&self) -> Option<&PathBuf> {
if self.mode == WorkspaceMode::CopyInOut {
self.staging_path.as_ref()
} else {
self.host_path.as_ref()
}
}
}
impl Default for WorkspaceConfig {
fn default() -> Self {
Self {
host_path: None,
container_path: PathBuf::from("/workspace"),
mode: WorkspaceMode::default(),
allow_execute: false,
staging_path: None,
}
}
}
#[derive(Debug, Clone)]
pub enum ReadinessProbe {
Exec { command: Vec<String> },
TcpPort(u16),
SdNotify,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]#[serde(rename_all = "kebab-case")]
pub enum GpuVendor {
#[default]
Auto,
Nvidia,
Amd,
Intel,
All,
}
impl GpuVendor {
pub fn includes_nvidia(self) -> bool {
matches!(self, Self::Auto | Self::Nvidia | Self::All)
}
pub fn includes_amd(self) -> bool {
matches!(self, Self::Auto | Self::Amd | Self::All)
}
pub fn includes_intel(self) -> bool {
matches!(self, Self::Auto | Self::Intel | Self::All)
}
}
pub const DEFAULT_GPU_DRIVER_CAPABILITIES: &str = "compute,utility";
pub const DEFAULT_GPU_VISIBLE_DEVICES: &str = "all";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GpuPassthroughConfig {
#[serde(default)]
pub vendor: GpuVendor,
#[serde(default)]
pub devices: Vec<PathBuf>,
pub driver_capabilities: String,
pub visible_devices: String,
#[serde(default = "default_bind_driver_libraries")]
pub bind_driver_libraries: bool,
}
fn default_bind_driver_libraries() -> bool {
true
}
impl Default for GpuPassthroughConfig {
fn default() -> Self {
Self {
vendor: GpuVendor::Auto,
devices: Vec::new(),
driver_capabilities: DEFAULT_GPU_DRIVER_CAPABILITIES.to_string(),
visible_devices: DEFAULT_GPU_VISIBLE_DEVICES.to_string(),
bind_driver_libraries: true,
}
}
}
impl GpuPassthroughConfig {
pub fn is_enabled(&self) -> bool {
true
}
}
pub fn gpu_environment(gpu: &GpuPassthroughConfig) -> Vec<(&'static str, String)> {
let mut env = vec![
("NVIDIA_VISIBLE_DEVICES", gpu.visible_devices.clone()),
("NVIDIA_DRIVER_CAPABILITIES", gpu.driver_capabilities.clone()),
];
if gpu.vendor.includes_nvidia() {
env.push((
"__EGL_VENDOR_LIBRARY_FILENAMES",
"/usr/share/glvnd/egl_vendor.d/10_nvidia.json".to_string(),
));
}
env
}
#[derive(Debug, Clone)]
pub struct ContainerConfig {
pub id: String,
pub name: String,
pub command: Vec<String>,
pub context_dir: Option<PathBuf>,
pub limits: ResourceLimits,
pub namespaces: NamespaceConfig,
pub user_ns_config: Option<UserNamespaceConfig>,
pub hostname: Option<String>,
pub use_gvisor: bool,
pub trust_level: TrustLevel,
pub network: crate::network::NetworkMode,
pub context_mode: crate::filesystem::ContextMode,
pub allow_degraded_security: bool,
pub allow_chroot_fallback: bool,
pub allow_host_network: bool,
pub proc_readonly: bool,
pub service_mode: ServiceMode,
pub rootfs_path: Option<PathBuf>,
pub rootfs_mode: RootfsMode,
pub rootfs_overlay: Option<RootfsOverlayConfig>,
pub egress_policy: Option<EgressPolicy>,
pub credential_broker: Option<CredentialBrokerConfig>,
pub credential_broker_token: String,
pub health_check: Option<HealthCheck>,
pub readiness_probe: Option<ReadinessProbe>,
pub secrets: Vec<SecretMount>,
pub volumes: Vec<VolumeMount>,
pub workspace: WorkspaceConfig,
pub home: PathBuf,
pub provider_configs: Vec<ProviderConfigMount>,
pub workdir: PathBuf,
pub environment: Vec<(String, String)>,
pub derived_environment: Vec<(String, String)>,
pub process_identity: ProcessIdentity,
pub config_hash: Option<u64>,
pub sd_notify: bool,
pub required_kernel_lockdown: Option<KernelLockdownMode>,
pub verify_context_integrity: bool,
pub verify_rootfs_attestation: bool,
pub seccomp_log_denied: bool,
pub gvisor_platform: GVisorPlatform,
pub seccomp_profile: Option<PathBuf>,
pub seccomp_profile_sha256: Option<String>,
pub seccomp_mode: SeccompMode,
pub seccomp_trace_log: Option<PathBuf>,
pub seccomp_allow_syscalls: Vec<String>,
pub caps_policy: Option<PathBuf>,
pub caps_policy_sha256: Option<String>,
pub landlock_policy: Option<PathBuf>,
pub landlock_policy_sha256: Option<String>,
pub hooks: Option<crate::security::OciHooks>,
pub pid_file: Option<PathBuf>,
pub console_socket: Option<PathBuf>,
pub terminal: bool,
pub console_size: ConsoleSize,
pub bundle_dir: Option<PathBuf>,
pub state_root: Option<PathBuf>,
pub gpu: Option<GpuPassthroughConfig>,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
clap::ValueEnum,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum SeccompMode {
#[default]
Enforce,
Trace,
}
impl ContainerConfig {
pub fn try_new(name: Option<String>, command: Vec<String>) -> crate::error::Result<Self> {
Self::try_new_with_id(None, name, command)
}
pub fn try_new_with_id(
preset_id: Option<String>,
name: Option<String>,
command: Vec<String>,
) -> crate::error::Result<Self> {
let id = match preset_id {
Some(id) => {
if id.len() != 32 || !id.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(crate::error::NucleusError::ConfigError(format!(
"Invalid preset container ID '{}': must be 32 hex characters",
id
)));
}
id
}
None => generate_container_id()?,
};
let credential_broker_token = generate_container_id()?;
let name = name.unwrap_or_else(|| id.clone());
Ok(Self {
id,
name: name.clone(),
command,
context_dir: None,
limits: ResourceLimits::default(),
namespaces: NamespaceConfig::default(),
user_ns_config: None,
hostname: Some(name),
use_gvisor: true,
trust_level: TrustLevel::default(),
network: crate::network::NetworkMode::None,
context_mode: crate::filesystem::ContextMode::Copy,
allow_degraded_security: false,
allow_chroot_fallback: false,
allow_host_network: false,
proc_readonly: true,
service_mode: ServiceMode::default(),
rootfs_path: None,
rootfs_mode: RootfsMode::default(),
rootfs_overlay: None,
egress_policy: None,
credential_broker: None,
credential_broker_token,
health_check: None,
readiness_probe: None,
secrets: Vec::new(),
volumes: Vec::new(),
workspace: WorkspaceConfig::default(),
home: PathBuf::from(DEFAULT_HOME_PATH),
provider_configs: Vec::new(),
workdir: PathBuf::from("/workspace"),
environment: Vec::new(),
derived_environment: Vec::new(),
process_identity: ProcessIdentity::default(),
config_hash: None,
sd_notify: false,
required_kernel_lockdown: None,
verify_context_integrity: false,
verify_rootfs_attestation: false,
seccomp_log_denied: false,
gvisor_platform: GVisorPlatform::default(),
seccomp_profile: None,
seccomp_profile_sha256: None,
seccomp_mode: SeccompMode::default(),
seccomp_trace_log: None,
seccomp_allow_syscalls: Vec::new(),
caps_policy: None,
caps_policy_sha256: None,
landlock_policy: None,
landlock_policy_sha256: None,
hooks: None,
pid_file: None,
console_socket: None,
terminal: false,
console_size: ConsoleSize::default(),
bundle_dir: None,
state_root: None,
gpu: None,
})
}
#[must_use]
pub fn with_rootless(mut self) -> Self {
self.namespaces.user = true;
self.user_ns_config = Some(UserNamespaceConfig::rootless());
self
}
#[must_use]
pub fn with_user_namespace(mut self, config: UserNamespaceConfig) -> Self {
self.namespaces.user = true;
self.user_ns_config = Some(config);
self
}
#[must_use]
pub fn with_context(mut self, dir: PathBuf) -> Self {
self.context_dir = Some(dir);
self
}
#[must_use]
pub fn with_limits(mut self, limits: ResourceLimits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub fn with_namespaces(mut self, namespaces: NamespaceConfig) -> Self {
self.namespaces = namespaces;
self
}
#[must_use]
pub fn with_hostname(mut self, hostname: Option<String>) -> Self {
self.hostname = hostname;
self
}
#[must_use]
pub fn with_gvisor(mut self, enabled: bool) -> Self {
self.use_gvisor = enabled;
self
}
#[must_use]
pub fn with_trust_level(mut self, level: TrustLevel) -> Self {
self.trust_level = level;
self
}
#[must_use]
pub fn with_oci_bundle(mut self) -> Self {
self.use_gvisor = true;
self
}
#[must_use]
pub fn with_network(mut self, mode: crate::network::NetworkMode) -> Self {
self.network = mode;
self
}
#[must_use]
pub fn with_context_mode(mut self, mode: crate::filesystem::ContextMode) -> Self {
self.context_mode = mode;
self
}
#[must_use]
pub fn with_allow_degraded_security(mut self, allow: bool) -> Self {
self.allow_degraded_security = allow;
self
}
#[must_use]
pub fn with_allow_chroot_fallback(mut self, allow: bool) -> Self {
self.allow_chroot_fallback = allow;
self
}
#[must_use]
pub fn with_allow_host_network(mut self, allow: bool) -> Self {
self.allow_host_network = allow;
self
}
#[must_use]
pub fn with_proc_readonly(mut self, proc_readonly: bool) -> Self {
self.proc_readonly = proc_readonly;
self
}
#[must_use]
pub fn with_service_mode(mut self, mode: ServiceMode) -> Self {
self.service_mode = mode;
self
}
#[must_use]
pub fn with_rootfs_path(mut self, path: PathBuf) -> Self {
self.rootfs_path = Some(path);
self
}
#[must_use]
pub fn with_rootfs_mode(mut self, mode: RootfsMode) -> Self {
self.rootfs_mode = mode;
self
}
#[must_use]
pub fn with_rootfs_overlay(mut self, upperdir: PathBuf, workdir: PathBuf) -> Self {
self.rootfs_overlay = Some(RootfsOverlayConfig { upperdir, workdir });
self
}
#[must_use]
pub fn with_egress_policy(mut self, policy: EgressPolicy) -> Self {
self.egress_policy = Some(policy);
self
}
#[must_use]
pub fn with_credential_broker(mut self, broker: CredentialBrokerConfig) -> Self {
self.credential_broker = Some(broker);
self = self.with_credential_broker_identity_env();
self
}
#[must_use]
pub fn with_health_check(mut self, hc: HealthCheck) -> Self {
self.health_check = Some(hc);
self
}
#[must_use]
pub fn with_readiness_probe(mut self, probe: ReadinessProbe) -> Self {
self.readiness_probe = Some(probe);
self
}
#[must_use]
pub fn with_secret(mut self, secret: SecretMount) -> Self {
self.secrets.push(secret);
self
}
#[must_use]
pub fn with_volume(mut self, volume: VolumeMount) -> Self {
self.volumes.push(volume);
self
}
#[must_use]
pub fn with_workspace(mut self, workspace: WorkspaceConfig) -> Self {
self.workspace = workspace;
self
}
#[must_use]
pub fn with_home(mut self, home: PathBuf) -> Self {
self.home = home;
self
}
#[must_use]
pub fn with_provider_config(mut self, provider_config: ProviderConfigMount) -> Self {
self.provider_configs.push(provider_config);
self
}
#[must_use]
pub fn with_workdir(mut self, workdir: PathBuf) -> Self {
self.workdir = workdir;
self
}
#[must_use]
pub fn with_env(mut self, key: String, value: String) -> Self {
if self.credential_broker_owns_env(&key) {
return self;
}
self.environment.push((key, value));
self
}
#[must_use]
pub fn with_derived_env(mut self, key: String, value: String) -> Self {
self.derived_environment.push((key, value));
self
}
fn upsert_derived_env(&mut self, key: &str, value: String) {
self.derived_environment
.retain(|(existing_key, _)| existing_key != key);
self.derived_environment.push((key.to_string(), value));
}
#[must_use]
pub fn with_credential_broker_identity_env(mut self) -> Self {
if self.credential_broker.is_some() {
self.environment
.retain(|(key, _)| !is_credential_broker_identity_env(key));
self.upsert_derived_env(CREDENTIAL_BROKER_CONTAINER_ID_ENV, self.id.clone());
self.upsert_derived_env(
CREDENTIAL_BROKER_TOKEN_ENV,
self.credential_broker_token.clone(),
);
}
self
}
#[must_use]
pub(crate) fn credential_broker_owns_env(&self, key: &str) -> bool {
self.credential_broker.is_some() && is_credential_broker_identity_env(key)
}
#[must_use]
pub fn with_process_identity(mut self, identity: ProcessIdentity) -> Self {
self.process_identity = identity;
self
}
#[must_use]
pub fn with_config_hash(mut self, hash: u64) -> Self {
self.config_hash = Some(hash);
self
}
#[must_use]
pub fn with_sd_notify(mut self, enabled: bool) -> Self {
self.sd_notify = enabled;
self
}
#[must_use]
pub fn with_required_kernel_lockdown(mut self, mode: KernelLockdownMode) -> Self {
self.required_kernel_lockdown = Some(mode);
self
}
#[must_use]
pub fn with_verify_context_integrity(mut self, enabled: bool) -> Self {
self.verify_context_integrity = enabled;
self
}
#[must_use]
pub fn with_verify_rootfs_attestation(mut self, enabled: bool) -> Self {
self.verify_rootfs_attestation = enabled;
self
}
#[must_use]
pub fn with_seccomp_log_denied(mut self, enabled: bool) -> Self {
self.seccomp_log_denied = enabled;
self
}
#[must_use]
pub fn with_gvisor_platform(mut self, platform: GVisorPlatform) -> Self {
self.gvisor_platform = platform;
self
}
#[must_use]
pub fn with_seccomp_profile(mut self, path: PathBuf) -> Self {
self.seccomp_profile = Some(path);
self
}
#[must_use]
pub fn with_seccomp_profile_sha256(mut self, hash: String) -> Self {
self.seccomp_profile_sha256 = Some(hash);
self
}
#[must_use]
pub fn with_seccomp_mode(mut self, mode: SeccompMode) -> Self {
self.seccomp_mode = mode;
self
}
#[must_use]
pub fn with_seccomp_trace_log(mut self, path: PathBuf) -> Self {
self.seccomp_trace_log = Some(path);
self
}
#[must_use]
pub fn with_seccomp_allow_syscalls(mut self, syscalls: Vec<String>) -> Self {
self.seccomp_allow_syscalls = syscalls;
self
}
#[must_use]
pub fn with_caps_policy(mut self, path: PathBuf) -> Self {
self.caps_policy = Some(path);
self
}
#[must_use]
pub fn with_caps_policy_sha256(mut self, hash: String) -> Self {
self.caps_policy_sha256 = Some(hash);
self
}
#[must_use]
pub fn with_landlock_policy(mut self, path: PathBuf) -> Self {
self.landlock_policy = Some(path);
self
}
#[must_use]
pub fn with_landlock_policy_sha256(mut self, hash: String) -> Self {
self.landlock_policy_sha256 = Some(hash);
self
}
#[must_use]
pub fn with_pid_file(mut self, path: PathBuf) -> Self {
self.pid_file = Some(path);
self
}
#[must_use]
pub fn with_console_socket(mut self, path: PathBuf) -> Self {
self.console_socket = Some(path);
self.terminal = true;
self
}
#[must_use]
pub fn with_terminal(mut self, size: ConsoleSize) -> Self {
self.terminal = true;
self.console_size = size;
self
}
#[must_use]
pub fn with_bundle_dir(mut self, path: PathBuf) -> Self {
self.bundle_dir = Some(path);
self
}
pub fn with_state_root(mut self, root: PathBuf) -> Self {
self.state_root = Some(root);
self
}
#[must_use]
pub fn with_gpu(mut self, gpu: GpuPassthroughConfig) -> Self {
self.gpu = Some(gpu);
self
}
fn validate_credential_broker(&self) -> crate::error::Result<()> {
let Some(broker) = &self.credential_broker else {
return Ok(());
};
let crate::network::NetworkMode::Bridge(bridge_config) = &self.network else {
return Err(crate::error::NucleusError::ConfigError(
"Credential broker egress requires --network bridge so Nucleus can force the \
sandbox through the host-side broker endpoint"
.to_string(),
));
};
if bridge_config.nat_backend == crate::network::NatBackend::Userspace {
return Err(crate::error::NucleusError::ConfigError(
"Credential broker egress requires the kernel NAT backend; \
slirp4netns userspace NAT cannot route to the host-side bridge broker"
.to_string(),
));
}
broker
.validate_for_bridge(bridge_config)
.map_err(crate::error::NucleusError::ConfigError)?;
let Some(policy) = &self.egress_policy else {
return Err(crate::error::NucleusError::ConfigError(
"Credential broker egress requires a broker-only egress policy".to_string(),
));
};
if !policy.is_credential_broker_only(broker) {
return Err(crate::error::NucleusError::ConfigError(
"Credential broker egress must allow only the broker IP/port and must deny DNS"
.to_string(),
));
}
Ok(())
}
fn validate_fail_closed_isolation(&self) -> crate::error::Result<()> {
let mode = self.service_mode.label();
if self.allow_degraded_security {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} forbids --allow-degraded-security",
mode
)));
}
if self.allow_chroot_fallback {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} forbids --allow-chroot-fallback",
mode
)));
}
if matches!(self.network, crate::network::NetworkMode::Host) {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} forbids native host network mode because it collapses the \
runtime boundary; use --network gvisor-host with --runtime gvisor and \
--allow-host-network when hostinet is required",
mode
)));
}
if matches!(self.network, crate::network::NetworkMode::GVisorHost) {
if !self.use_gvisor {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} requires --runtime gvisor for --network gvisor-host",
mode
)));
}
if !self.allow_host_network {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} requires --allow-host-network for --network gvisor-host",
mode
)));
}
if self.egress_policy.is_some() {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} cannot enforce egress policy with --network gvisor-host",
mode
)));
}
} else if self.allow_host_network {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} permits --allow-host-network only with --network gvisor-host",
mode
)));
}
if self.seccomp_mode == SeccompMode::Trace {
return Err(crate::error::NucleusError::ConfigError(format!(
"{} forbids --seccomp-mode trace",
mode
)));
}
if !self.seccomp_allow_syscalls.is_empty() {
let allow_network = !matches!(self.network, crate::network::NetworkMode::None);
crate::security::SeccompManager::validate_extra_syscalls_for_production(
allow_network,
&self.seccomp_allow_syscalls,
)?;
}
Ok(())
}
pub fn validate_strict_agent_mode(&self) -> crate::error::Result<()> {
if self.service_mode != ServiceMode::StrictAgent {
return Ok(());
}
self.validate_fail_closed_isolation()?;
if !self.limits.has_cgroup_control() {
return Err(crate::error::NucleusError::ConfigError(
"Strict agent mode requires at least one cgroup control \
(default --pids limit or explicit --memory/--cpus/--pids/--io-limit)"
.to_string(),
));
}
Ok(())
}
pub fn validate_production_mode(&self) -> crate::error::Result<()> {
if self.service_mode != ServiceMode::Production {
return Ok(());
}
self.validate_fail_closed_isolation()?;
if self.gpu.is_some() {
return Err(crate::error::NucleusError::ConfigError(
"Production mode forbids --gpu host device passthrough; production services must \
declare GPU needs through an attested rootfs"
.to_string(),
));
}
if self.workspace.allow_execute && self.workspace.is_writable() {
return Err(crate::error::NucleusError::ConfigError(
"Production mode forbids writable executable workspaces; use bind-ro with \
--workspace-exec or remove --workspace-exec"
.to_string(),
));
}
let Some(rootfs_path) = self.rootfs_path.as_ref() else {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires explicit --rootfs path (no host bind mounts)".to_string(),
));
};
if self.rootfs_mode == RootfsMode::Overlay {
return Err(crate::error::NucleusError::ConfigError(
"Production mode does not yet support --rootfs-mode overlay; production mount \
auditing currently requires read-only rootfs bind mounts"
.to_string(),
));
}
if self.caps_policy.is_some() && self.caps_policy_sha256.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires --caps-policy-sha256 when using --caps-policy"
.to_string(),
));
}
if self.landlock_policy.is_some() && self.landlock_policy_sha256.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires --landlock-policy-sha256 when using --landlock-policy"
.to_string(),
));
}
if self.seccomp_profile.is_some() && self.seccomp_profile_sha256.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires --seccomp-profile-sha256 when using --seccomp-profile"
.to_string(),
));
}
if self.limits.memory_bytes.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires explicit --memory limit".to_string(),
));
}
if self.limits.cpu_quota_us.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires explicit --cpus limit".to_string(),
));
}
if !self.verify_rootfs_attestation {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires --verify-rootfs-attestation".to_string(),
));
}
validate_production_rootfs_path(rootfs_path)?;
Ok(())
}
pub fn validate_runtime_support(&self) -> crate::error::Result<()> {
self.limits.validate_runtime_sanity()?;
if let Some(user_ns_config) = &self.user_ns_config {
if !self.process_identity.additional_gids.is_empty() {
return Err(crate::error::NucleusError::ConfigError(
"Supplementary groups are currently unsupported with user namespaces"
.to_string(),
));
}
let uid_mapped = user_ns_config.uid_mappings.iter().any(|mapping| {
self.process_identity.uid >= mapping.container_id
&& self.process_identity.uid
< mapping.container_id.saturating_add(mapping.count)
});
if !uid_mapped {
return Err(crate::error::NucleusError::ConfigError(format!(
"Process uid {} is not mapped in the configured user namespace",
self.process_identity.uid
)));
}
let gid_mapped = user_ns_config.gid_mappings.iter().any(|mapping| {
self.process_identity.gid >= mapping.container_id
&& self.process_identity.gid
< mapping.container_id.saturating_add(mapping.count)
});
if !gid_mapped {
return Err(crate::error::NucleusError::ConfigError(format!(
"Process gid {} is not mapped in the configured user namespace",
self.process_identity.gid
)));
}
}
if self.seccomp_mode == SeccompMode::Trace && self.seccomp_trace_log.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"Seccomp trace mode requires --seccomp-log / seccomp_trace_log".to_string(),
));
}
normalize_container_destination(&self.workdir)?;
let workspace_path = normalize_container_destination(&self.workspace.container_path)?;
if workspace_path != std::path::Path::new("/workspace") {
return Err(crate::error::NucleusError::ConfigError(
"Workspace destination is fixed at /workspace".to_string(),
));
}
let home_path = normalize_container_destination(&self.home)?;
if home_path == workspace_path
|| home_path.starts_with(&workspace_path)
|| workspace_path.starts_with(&home_path)
{
return Err(crate::error::NucleusError::ConfigError(
"--home must not overlap /workspace".to_string(),
));
}
if let Some(host_path) = &self.workspace.host_path {
validate_workspace_host_path(host_path)?;
}
if self.workspace.mode == WorkspaceMode::CopyInOut && self.workspace.host_path.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"--workspace-mode copy-in-out requires --workspace".to_string(),
));
}
for secret in &self.secrets {
normalize_container_destination(&secret.dest)?;
}
for provider_config in &self.provider_configs {
validate_provider_config_source(&provider_config.source)?;
normalize_provider_config_destination(&home_path, &provider_config.dest)?;
}
for volume in &self.volumes {
let volume_dest = normalize_volume_destination(&volume.dest)?;
if volume_dest == workspace_path {
return Err(crate::error::NucleusError::ConfigError(
"Volume destination /workspace conflicts with --workspace".to_string(),
));
}
if volume_dest == home_path {
return Err(crate::error::NucleusError::ConfigError(
"Volume destination conflicts with --home".to_string(),
));
}
match &volume.source {
VolumeSource::Bind { source } => {
if !source.is_absolute() {
return Err(crate::error::NucleusError::ConfigError(format!(
"Volume source must be absolute: {:?}",
source
)));
}
if !source.exists() {
return Err(crate::error::NucleusError::ConfigError(format!(
"Volume source does not exist: {:?}",
source
)));
}
crate::filesystem::validate_bind_mount_source(source)?;
}
VolumeSource::Tmpfs { .. } => {}
}
}
self.validate_credential_broker()?;
if self.rootfs_mode == RootfsMode::Overlay {
if self.rootfs_path.is_none() {
return Err(crate::error::NucleusError::ConfigError(
"--rootfs-mode overlay requires --rootfs or --agent-toolchain-rootfs"
.to_string(),
));
}
if self.use_gvisor {
return Err(crate::error::NucleusError::ConfigError(
"--rootfs-mode overlay is currently supported only with --runtime native"
.to_string(),
));
}
}
if !self.use_gvisor {
return Ok(());
}
if self.seccomp_mode == SeccompMode::Trace {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support --seccomp-mode trace; use --runtime native"
.to_string(),
));
}
if self.seccomp_log_denied {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support seccomp deny logging; use --runtime native"
.to_string(),
));
}
if !self.seccomp_allow_syscalls.is_empty() {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support --seccomp-allow; use a custom --seccomp-profile or --runtime native"
.to_string(),
));
}
if self.caps_policy.is_some() {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support capability policy files; use --runtime native"
.to_string(),
));
}
if self.landlock_policy.is_some() {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support Landlock policy files; use --runtime native"
.to_string(),
));
}
if self.health_check.is_some() {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support exec health checks; use --runtime native or remove --health-cmd"
.to_string(),
));
}
if matches!(
self.readiness_probe.as_ref(),
Some(ReadinessProbe::Exec { .. }) | Some(ReadinessProbe::TcpPort(_))
) {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support exec/TCP readiness probes; use --runtime native or --readiness-sd-notify"
.to_string(),
));
}
if self.verify_context_integrity
&& self.context_dir.is_some()
&& matches!(self.context_mode, crate::filesystem::ContextMode::BindMount)
{
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime cannot verify bind-mounted context integrity; use --context-mode copy or disable --verify-context-integrity"
.to_string(),
));
}
Ok(())
}
pub fn apply_runtime_selection(
mut self,
runtime: RuntimeSelection,
oci: bool,
) -> crate::error::Result<Self> {
match runtime {
RuntimeSelection::Native => {
if oci {
return Err(crate::error::NucleusError::ConfigError(
"--bundle requires gVisor runtime; use --runtime gvisor".to_string(),
));
}
self = self.with_gvisor(false);
}
RuntimeSelection::GVisor => {
self = self.with_gvisor(true);
if !oci {
tracing::info!(
"Security hardening: enabling OCI bundle mode for gVisor runtime"
);
}
self = self.with_oci_bundle();
}
}
Ok(self)
}
}
pub fn validate_container_name(name: &str) -> crate::error::Result<()> {
if name.is_empty() || name.len() > 128 {
return Err(crate::error::NucleusError::ConfigError(
"Invalid container name: must be 1-128 characters".to_string(),
));
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
{
return Err(crate::error::NucleusError::ConfigError(
"Invalid container name: allowed characters are a-zA-Z0-9, '-', '_', '.'".to_string(),
));
}
Ok(())
}
pub fn validate_hostname(hostname: &str) -> crate::error::Result<()> {
if hostname.is_empty() || hostname.len() > 253 {
return Err(crate::error::NucleusError::ConfigError(
"Invalid hostname: must be 1-253 characters".to_string(),
));
}
for label in hostname.split('.') {
if label.is_empty() || label.len() > 63 {
return Err(crate::error::NucleusError::ConfigError(format!(
"Invalid hostname label: '{}'",
label
)));
}
if label.starts_with('-') || label.ends_with('-') {
return Err(crate::error::NucleusError::ConfigError(format!(
"Invalid hostname label '{}': cannot start or end with '-'",
label
)));
}
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(crate::error::NucleusError::ConfigError(format!(
"Invalid hostname label '{}': allowed characters are a-zA-Z0-9 and '-'",
label
)));
}
}
Ok(())
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use crate::network::NetworkMode;
#[test]
fn test_generate_container_id_is_32_hex_chars() {
let id = generate_container_id().unwrap();
assert_eq!(
id.len(),
32,
"Container ID must be full 128-bit (32 hex chars), got {}",
id.len()
);
assert!(
id.chars().all(|c| c.is_ascii_hexdigit()),
"Container ID must be hex: {}",
id
);
}
#[test]
fn test_generate_container_id_is_unique() {
let id1 = generate_container_id().unwrap();
let id2 = generate_container_id().unwrap();
assert_ne!(id1, id2, "Two consecutive IDs must differ");
}
#[test]
fn test_config_security_defaults_are_hardened() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()]).unwrap();
assert!(!cfg.allow_degraded_security);
assert!(!cfg.allow_chroot_fallback);
assert!(!cfg.allow_host_network);
assert!(cfg.proc_readonly);
assert_eq!(cfg.service_mode, ServiceMode::Agent);
assert!(cfg.rootfs_path.is_none());
assert!(cfg.egress_policy.is_none());
assert!(cfg.credential_broker.is_none());
assert!(cfg.secrets.is_empty());
assert!(cfg.volumes.is_empty());
assert_eq!(cfg.home, PathBuf::from(DEFAULT_HOME_PATH));
assert!(cfg.provider_configs.is_empty());
assert!(!cfg.sd_notify);
assert!(cfg.required_kernel_lockdown.is_none());
assert!(!cfg.verify_context_integrity);
assert!(!cfg.verify_rootfs_attestation);
assert!(!cfg.seccomp_log_denied);
assert_eq!(cfg.gvisor_platform, GVisorPlatform::Systrap);
}
#[test]
fn test_credential_broker_validates_broker_only_bridge_egress() {
let broker =
crate::network::CredentialBrokerConfig::parse_endpoint("10.0.42.1:8080").unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_network(NetworkMode::Bridge(crate::network::BridgeConfig::default()))
.with_credential_broker(broker.clone())
.with_egress_policy(broker.egress_policy());
assert!(cfg.validate_runtime_support().is_ok());
}
#[test]
fn test_credential_broker_injects_per_container_identity_env() {
let broker =
crate::network::CredentialBrokerConfig::parse_endpoint("10.0.42.1:8080").unwrap();
let cfg = ContainerConfig::try_new_with_id(
Some("0123456789abcdef0123456789abcdef".to_string()),
None,
vec!["/bin/sh".to_string()],
)
.unwrap()
.with_env(
CREDENTIAL_BROKER_TOKEN_ENV.to_string(),
"user-supplied-token".to_string(),
)
.with_env(
CREDENTIAL_BROKER_CONTAINER_ID_ENV.to_string(),
"user-supplied-container-id".to_string(),
)
.with_credential_broker(broker)
.with_env(
CREDENTIAL_BROKER_TOKEN_ENV.to_string(),
"late-user-supplied-token".to_string(),
)
.with_env(
CREDENTIAL_BROKER_CONTAINER_ID_ENV.to_string(),
"late-user-supplied-container-id".to_string(),
);
assert!(cfg.derived_environment.contains(&(
CREDENTIAL_BROKER_CONTAINER_ID_ENV.to_string(),
cfg.id.clone()
)));
assert_ne!(cfg.credential_broker_token, cfg.id);
assert!(cfg.derived_environment.contains(&(
CREDENTIAL_BROKER_TOKEN_ENV.to_string(),
cfg.credential_broker_token.clone()
)));
for key in [
CREDENTIAL_BROKER_TOKEN_ENV,
CREDENTIAL_BROKER_CONTAINER_ID_ENV,
] {
assert!(!cfg.environment.iter().any(|(env_key, _)| env_key == key));
}
}
#[test]
fn test_credential_broker_rejects_non_bridge_network() {
let broker =
crate::network::CredentialBrokerConfig::parse_endpoint("10.0.42.1:8080").unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_network(NetworkMode::None)
.with_credential_broker(broker.clone())
.with_egress_policy(broker.egress_policy());
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("requires --network bridge"));
}
#[test]
fn test_credential_broker_rejects_extra_egress_routes() {
let broker =
crate::network::CredentialBrokerConfig::parse_endpoint("10.0.42.1:8080").unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_network(NetworkMode::Bridge(crate::network::BridgeConfig::default()))
.with_credential_broker(broker)
.with_egress_policy(
crate::network::EgressPolicy::default()
.with_allowed_cidrs(vec![
"10.0.42.1/32".to_string(),
"203.0.113.0/24".to_string(),
])
.with_allowed_tcp_ports(vec![8080]),
);
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("allow only the broker"));
}
#[test]
fn test_credential_broker_rejects_userspace_nat_backend() {
let broker =
crate::network::CredentialBrokerConfig::parse_endpoint("10.0.42.1:8080").unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_network(NetworkMode::Bridge(
crate::network::BridgeConfig::default()
.with_nat_backend(crate::network::NatBackend::Userspace),
))
.with_credential_broker(broker.clone())
.with_egress_policy(broker.egress_policy());
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("kernel NAT backend"));
}
#[test]
fn test_credential_broker_rejects_ip_outside_bridge_gateway() {
let broker =
crate::network::CredentialBrokerConfig::parse_endpoint("8.8.8.8:8080").unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_network(NetworkMode::Bridge(crate::network::BridgeConfig::default()))
.with_credential_broker(broker.clone())
.with_egress_policy(broker.egress_policy());
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err
.to_string()
.contains("host-side bridge address 10.0.42.1"));
}
#[test]
fn test_credential_broker_rejects_non_gateway_bridge_ip() {
let broker =
crate::network::CredentialBrokerConfig::parse_endpoint("10.0.42.2:8080").unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_network(NetworkMode::Bridge(crate::network::BridgeConfig::default()))
.with_credential_broker(broker.clone())
.with_egress_policy(broker.egress_policy());
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err
.to_string()
.contains("host-side bridge address 10.0.42.1"));
}
#[test]
fn test_production_mode_rejects_degraded_flags() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_allow_degraded_security(true)
.with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
assert!(cfg.validate_production_mode().is_err());
}
#[test]
fn test_production_mode_rejects_chroot_fallback() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_allow_chroot_fallback(true)
.with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(
err.to_string().contains("chroot"),
"Production mode must reject chroot fallback"
);
}
#[test]
fn test_production_mode_requires_rootfs() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("--rootfs"));
}
fn test_rootfs_path() -> std::path::PathBuf {
std::path::PathBuf::from("/nix/store")
}
#[test]
fn test_production_mode_requires_memory_limit() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(rootfs);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("--memory"));
}
#[test]
fn test_production_mode_valid_config() {
let rootfs = test_rootfs_path();
if !rootfs.is_dir() {
return;
}
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(rootfs.clone())
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let result = cfg.validate_production_mode();
assert!(result.is_ok());
}
#[test]
fn test_production_mode_allows_explicit_gvisor_host_network() {
let rootfs = test_rootfs_path();
if !rootfs.is_dir() {
return;
}
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(true)
.with_service_mode(ServiceMode::Production)
.with_network(NetworkMode::GVisorHost)
.with_allow_host_network(true)
.with_rootfs_path(rootfs)
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
assert!(cfg.validate_production_mode().is_ok());
}
#[test]
fn test_production_mode_rejects_gvisor_host_network_with_egress_policy() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(true)
.with_service_mode(ServiceMode::Production)
.with_network(NetworkMode::GVisorHost)
.with_allow_host_network(true)
.with_egress_policy(crate::network::EgressPolicy::deny_all())
.with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("egress policy"));
}
#[test]
fn test_production_mode_rejects_native_host_network() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(false)
.with_service_mode(ServiceMode::Production)
.with_network(NetworkMode::Host)
.with_allow_host_network(true)
.with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("host network"));
}
#[test]
fn test_production_mode_rejects_gvisor_host_without_gvisor_runtime() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(false)
.with_service_mode(ServiceMode::Production)
.with_network(NetworkMode::GVisorHost)
.with_allow_host_network(true)
.with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("--runtime gvisor"));
}
#[test]
fn test_production_mode_rejects_gvisor_host_without_explicit_opt_in() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(true)
.with_service_mode(ServiceMode::Production)
.with_network(NetworkMode::GVisorHost)
.with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("--allow-host-network"));
}
#[test]
fn test_production_mode_rejects_rootfs_parent_traversal() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(std::path::PathBuf::from("/nix/store/../../tmp/evil-rootfs"))
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(
err.to_string().contains("parent traversal"),
"Production mode must reject raw rootfs traversal before canonicalization"
);
}
#[test]
fn test_production_mode_rejects_out_of_store_rootfs() {
let temp = tempfile::TempDir::new().unwrap();
let rootfs = temp.path().join("rootfs");
std::fs::create_dir(&rootfs).unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(rootfs)
.with_verify_rootfs_attestation(true)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(
err.to_string().contains("/nix/store"),
"Production mode must reject rootfs paths that resolve outside /nix/store"
);
}
#[test]
fn test_production_mode_requires_rootfs_attestation() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(rootfs.clone())
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("attestation"));
}
#[test]
fn test_production_mode_rejects_seccomp_trace() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(rootfs.clone())
.with_seccomp_mode(SeccompMode::Trace)
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(
err.to_string().contains("trace"),
"Production mode must reject seccomp trace mode"
);
}
#[test]
fn test_production_mode_rejects_security_critical_seccomp_allow() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(test_rootfs_path())
.with_verify_rootfs_attestation(true)
.with_seccomp_allow_syscalls(vec!["keyctl".to_string()])
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap()
.with_cpu_cores(2.0)
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("seccomp-allow"));
assert!(err.to_string().contains("keyctl"));
}
#[test]
fn test_strict_agent_mode_valid_without_production_rootfs() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::StrictAgent);
assert!(cfg.validate_strict_agent_mode().is_ok());
assert!(cfg.rootfs_path.is_none());
assert!(!cfg.verify_rootfs_attestation);
}
#[test]
fn test_strict_agent_mode_rejects_degraded_security() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::StrictAgent)
.with_allow_degraded_security(true);
let err = cfg.validate_strict_agent_mode().unwrap_err();
assert!(err.to_string().contains("degraded"));
}
#[test]
fn test_strict_agent_mode_rejects_chroot_fallback() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::StrictAgent)
.with_allow_chroot_fallback(true);
let err = cfg.validate_strict_agent_mode().unwrap_err();
assert!(err.to_string().contains("chroot"));
}
#[test]
fn test_strict_agent_mode_rejects_seccomp_trace() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::StrictAgent)
.with_seccomp_mode(SeccompMode::Trace);
let err = cfg.validate_strict_agent_mode().unwrap_err();
assert!(err.to_string().contains("trace"));
}
#[test]
fn test_strict_agent_mode_rejects_native_host_network() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(false)
.with_service_mode(ServiceMode::StrictAgent)
.with_network(NetworkMode::Host)
.with_allow_host_network(true);
let err = cfg.validate_strict_agent_mode().unwrap_err();
assert!(err.to_string().contains("host network"));
}
#[test]
fn test_strict_agent_mode_requires_some_cgroup_control() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::StrictAgent)
.with_limits(crate::resources::ResourceLimits::unlimited());
let err = cfg.validate_strict_agent_mode().unwrap_err();
assert!(err.to_string().contains("cgroup"));
}
#[test]
fn test_strict_agent_mode_allows_explicit_gvisor_host_network() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(true)
.with_service_mode(ServiceMode::StrictAgent)
.with_network(NetworkMode::GVisorHost)
.with_allow_host_network(true);
assert!(cfg.validate_strict_agent_mode().is_ok());
}
#[test]
fn test_production_mode_requires_cpu_limit() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(rootfs.clone())
.with_limits(
crate::resources::ResourceLimits::default()
.with_memory("512M")
.unwrap(),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("--cpus"));
}
#[test]
fn test_config_security_builders_override_defaults() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_allow_degraded_security(true)
.with_allow_chroot_fallback(true)
.with_allow_host_network(true)
.with_proc_readonly(false)
.with_network(NetworkMode::Host);
assert!(cfg.allow_degraded_security);
assert!(cfg.allow_chroot_fallback);
assert!(cfg.allow_host_network);
assert!(!cfg.proc_readonly);
assert!(matches!(cfg.network, NetworkMode::Host));
}
#[test]
fn test_hardening_builders_override_defaults() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_required_kernel_lockdown(KernelLockdownMode::Confidentiality)
.with_verify_context_integrity(true)
.with_verify_rootfs_attestation(true)
.with_seccomp_log_denied(true)
.with_gvisor_platform(GVisorPlatform::Kvm);
assert_eq!(
cfg.required_kernel_lockdown,
Some(KernelLockdownMode::Confidentiality)
);
assert!(cfg.verify_context_integrity);
assert!(cfg.verify_rootfs_attestation);
assert!(cfg.seccomp_log_denied);
assert_eq!(cfg.gvisor_platform, GVisorPlatform::Kvm);
}
#[test]
fn test_seccomp_trace_requires_log_path() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gvisor(false)
.with_seccomp_mode(SeccompMode::Trace);
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("seccomp-log"));
}
#[test]
fn test_gvisor_allows_custom_seccomp_profile_but_rejects_native_policy_files() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_seccomp_profile(PathBuf::from("/tmp/seccomp.json"))
.with_caps_policy(PathBuf::from("/tmp/caps.toml"));
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("capability policy"));
}
#[test]
fn test_gvisor_accepts_custom_seccomp_profile() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_seccomp_profile(PathBuf::from("/tmp/seccomp.json"));
cfg.validate_runtime_support().unwrap();
}
#[test]
fn test_gvisor_rejects_landlock_policy_file() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_landlock_policy(PathBuf::from("/tmp/landlock.toml"));
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("Landlock"));
}
#[test]
fn test_gvisor_rejects_trace_mode_even_with_log_path() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_seccomp_mode(SeccompMode::Trace)
.with_seccomp_trace_log(PathBuf::from("/tmp/trace.ndjson"));
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("gVisor runtime"));
}
#[test]
fn test_gvisor_rejects_seccomp_allow_without_custom_profile_projection() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_seccomp_allow_syscalls(vec!["io_uring_setup".to_string()]);
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("seccomp-allow"));
}
#[test]
fn test_secret_dest_must_be_absolute() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_secret(crate::container::SecretMount {
source: PathBuf::from("/run/secrets/api-key"),
dest: PathBuf::from("secrets/api-key"),
mode: 0o400,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("absolute"));
}
#[test]
fn test_secret_dest_rejects_parent_traversal() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_secret(crate::container::SecretMount {
source: PathBuf::from("/run/secrets/api-key"),
dest: PathBuf::from("/../../etc/passwd"),
mode: 0o400,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("parent traversal"));
}
#[test]
fn test_bind_volume_source_must_exist() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_volume(VolumeMount {
source: VolumeSource::Bind {
source: PathBuf::from("/tmp/definitely-missing-nucleus-volume"),
},
dest: PathBuf::from("/var/lib/app"),
read_only: false,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("Volume source does not exist"));
}
#[test]
fn test_bind_volume_source_rejects_sensitive_host_subtrees() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_volume(VolumeMount {
source: VolumeSource::Bind {
source: PathBuf::from("/proc/sys"),
},
dest: PathBuf::from("/host-proc"),
read_only: true,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("sensitive host path"));
}
#[test]
fn test_bind_volume_dest_must_be_absolute() {
let dir = tempfile::TempDir::new().unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_volume(VolumeMount {
source: VolumeSource::Bind {
source: dir.path().to_path_buf(),
},
dest: PathBuf::from("var/lib/app"),
read_only: false,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("absolute"));
}
#[test]
fn test_bind_volume_dest_rejects_reserved_container_paths() {
let dir = tempfile::TempDir::new().unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_volume(VolumeMount {
source: VolumeSource::Bind {
source: dir.path().to_path_buf(),
},
dest: PathBuf::from("/etc"),
read_only: false,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("reserved"));
}
#[test]
fn test_default_workspace_and_workdir_contract() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()]).unwrap();
assert_eq!(cfg.workspace.container_path, PathBuf::from("/workspace"));
assert_eq!(cfg.workdir, PathBuf::from("/workspace"));
assert_eq!(cfg.home, PathBuf::from(DEFAULT_HOME_PATH));
assert_eq!(cfg.workspace.mode, WorkspaceMode::BindRw);
assert!(!cfg.workspace.allow_execute);
}
#[test]
fn test_home_must_not_overlap_workspace() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_home(PathBuf::from("/workspace/.home"));
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("must not overlap /workspace"));
}
#[test]
fn test_workspace_source_validates_existing_directory() {
let dir = tempfile::TempDir::new().unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_workspace(WorkspaceConfig::new().with_host_path(dir.path().to_path_buf()));
cfg.validate_runtime_support().unwrap();
}
#[test]
fn test_copy_in_out_requires_workspace_source() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_workspace(WorkspaceConfig::new().with_mode(WorkspaceMode::CopyInOut));
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("requires --workspace"));
}
#[test]
fn test_volume_destination_cannot_replace_workspace() {
let dir = tempfile::TempDir::new().unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_volume(VolumeMount {
source: VolumeSource::Bind {
source: dir.path().to_path_buf(),
},
dest: PathBuf::from("/workspace"),
read_only: false,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("conflicts with --workspace"));
}
#[test]
fn test_volume_destination_cannot_replace_home() {
let dir = tempfile::TempDir::new().unwrap();
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_volume(VolumeMount {
source: VolumeSource::Bind {
source: dir.path().to_path_buf(),
},
dest: PathBuf::from(DEFAULT_HOME_PATH),
read_only: false,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("conflicts with --home"));
}
#[test]
fn test_production_rejects_writable_executable_workspace() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_service_mode(ServiceMode::Production)
.with_workspace(
WorkspaceConfig::new()
.with_mode(WorkspaceMode::BindRw)
.with_allow_execute(true),
);
let err = cfg.validate_production_mode().unwrap_err();
assert!(err.to_string().contains("writable executable workspaces"));
}
#[test]
fn test_tmpfs_volume_rejects_parent_traversal() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_volume(VolumeMount {
source: VolumeSource::Tmpfs {
size: Some("64M".to_string()),
},
dest: PathBuf::from("/../../var/lib/app"),
read_only: false,
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("parent traversal"));
}
#[test]
fn test_gvisor_rejects_bind_mount_context_integrity_verification() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_context(PathBuf::from("/tmp/context"))
.with_context_mode(crate::filesystem::ContextMode::BindMount)
.with_verify_context_integrity(true);
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("context integrity"));
}
#[test]
fn test_gvisor_rejects_exec_health_checks() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_health_check(HealthCheck {
command: vec!["/bin/sh".to_string(), "-c".to_string(), "true".to_string()],
interval: Duration::from_secs(30),
retries: 3,
start_period: Duration::from_secs(1),
timeout: Duration::from_secs(5),
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("health checks"));
}
#[test]
fn test_gvisor_rejects_exec_readiness_probes() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_readiness_probe(ReadinessProbe::Exec {
command: vec!["/bin/sh".to_string(), "-c".to_string(), "true".to_string()],
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("readiness"));
}
#[test]
fn test_gvisor_allows_copy_mode_context_integrity_verification() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_context(PathBuf::from("/tmp/context"))
.with_context_mode(crate::filesystem::ContextMode::Copy)
.with_verify_context_integrity(true);
assert!(cfg.validate_runtime_support().is_ok());
}
#[test]
fn test_user_namespace_rejects_unmapped_process_identity() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_rootless()
.with_process_identity(ProcessIdentity {
uid: 1000,
gid: 1000,
additional_gids: Vec::new(),
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("not mapped"));
}
#[test]
fn test_user_namespace_rejects_supplementary_groups() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_rootless()
.with_process_identity(ProcessIdentity {
uid: 0,
gid: 0,
additional_gids: vec![1],
});
let err = cfg.validate_runtime_support().unwrap_err();
assert!(err.to_string().contains("Supplementary groups"));
}
#[test]
fn test_native_runtime_disables_gvisor() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.apply_runtime_selection(RuntimeSelection::Native, false)
.unwrap();
assert!(!cfg.use_gvisor, "native runtime must disable gVisor");
assert_eq!(
cfg.trust_level,
TrustLevel::Untrusted,
"native runtime must preserve the default Untrusted trust level"
);
}
#[test]
fn test_native_runtime_preserves_explicit_trusted_policy() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_trust_level(TrustLevel::Trusted)
.apply_runtime_selection(RuntimeSelection::Native, false)
.unwrap();
assert!(!cfg.use_gvisor, "native runtime must disable gVisor");
assert_eq!(
cfg.trust_level,
TrustLevel::Trusted,
"native runtime must preserve explicit Trusted trust level"
);
}
#[test]
fn test_default_config_has_gvisor_enabled() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()]).unwrap();
assert!(cfg.use_gvisor, "default must have gVisor enabled");
assert_eq!(
cfg.trust_level,
TrustLevel::Untrusted,
"default must be Untrusted"
);
}
#[test]
fn test_console_socket_implies_terminal_mode() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_console_socket(PathBuf::from("/tmp/console.sock"));
assert!(cfg.terminal);
assert_eq!(cfg.console_socket, Some(PathBuf::from("/tmp/console.sock")));
}
#[test]
fn test_terminal_size_can_be_configured() {
let size = ConsoleSize {
width: 132,
height: 43,
};
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_terminal(size);
assert!(cfg.terminal);
assert_eq!(cfg.console_size, size);
}
#[test]
fn test_generate_container_id_returns_result() {
let id: crate::error::Result<String> = generate_container_id();
let id = id.expect("generate_container_id must return Ok, not panic");
assert_eq!(id.len(), 32, "container ID must be 32 hex chars");
assert!(
id.chars().all(|c| c.is_ascii_hexdigit()),
"container ID must be valid hex: {}",
id
);
}
#[test]
fn gpu_defaults_match_nvidia_toolkit() {
let cfg = GpuPassthroughConfig::default();
assert_eq!(cfg.vendor, GpuVendor::Auto);
assert!(cfg.devices.is_empty());
assert_eq!(cfg.driver_capabilities, DEFAULT_GPU_DRIVER_CAPABILITIES);
assert_eq!(cfg.visible_devices, DEFAULT_GPU_VISIBLE_DEVICES);
assert!(cfg.bind_driver_libraries);
}
#[test]
fn production_mode_rejects_gpu_passthrough() {
let mut cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gpu(GpuPassthroughConfig::default());
cfg.service_mode = ServiceMode::Production;
cfg.rootfs_path = Some(std::path::PathBuf::from("/nix/some/rootfs"));
let err = cfg.validate_production_mode().unwrap_err();
let msg = format!("{}", err);
assert!(
msg.contains("Production mode forbids --gpu"),
"unexpected error: {}",
msg
);
}
#[test]
fn agent_mode_allows_gpu_passthrough() {
let cfg = ContainerConfig::try_new(None, vec!["/bin/sh".to_string()])
.unwrap()
.with_gpu(GpuPassthroughConfig::default());
assert_eq!(cfg.service_mode, ServiceMode::Agent);
assert!(cfg.gpu.is_some());
cfg.validate_production_mode().expect("agent mode permits gpu");
}
#[test]
fn gpu_environment_includes_nvidia_vars() {
let gpu = GpuPassthroughConfig {
vendor: GpuVendor::Nvidia,
..GpuPassthroughConfig::default()
};
let env = gpu_environment(&gpu);
let keys: Vec<&str> = env.iter().map(|(k, _)| *k).collect();
assert!(keys.contains(&"NVIDIA_VISIBLE_DEVICES"));
assert!(keys.contains(&"NVIDIA_DRIVER_CAPABILITIES"));
assert!(keys.contains(&"__EGL_VENDOR_LIBRARY_FILENAMES"));
}
#[test]
fn gpu_environment_omits_egl_when_not_nvidia() {
let gpu = GpuPassthroughConfig {
vendor: GpuVendor::Amd,
..GpuPassthroughConfig::default()
};
let env = gpu_environment(&gpu);
assert!(!env.iter().any(|(k, _)| *k == "__EGL_VENDOR_LIBRARY_FILENAMES"));
}
}