use crate::filesystem::normalize_container_destination;
use crate::isolation::{NamespaceConfig, UserNamespaceConfig};
use crate::network::EgressPolicy;
use crate::resources::ResourceLimits;
use crate::security::GVisorPlatform;
use std::path::PathBuf;
use std::time::Duration;
pub fn generate_container_id() -> crate::error::Result<String> {
use std::io::Read;
let mut buf = [0u8; 16];
let mut file = std::fs::File::open("/dev/urandom").map_err(|e| {
crate::error::NucleusError::ConfigError(format!(
"Failed to open /dev/urandom for container ID generation: {}",
e
))
})?;
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)]
pub enum TrustLevel {
Trusted,
#[default]
Untrusted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub enum ServiceMode {
#[default]
Agent,
Production,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
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 enum ReadinessProbe {
Exec { command: Vec<String> },
TcpPort(u16),
SdNotify,
}
#[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 egress_policy: Option<EgressPolicy>,
pub health_check: Option<HealthCheck>,
pub readiness_probe: Option<ReadinessProbe>,
pub secrets: Vec<SecretMount>,
pub environment: Vec<(String, String)>,
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 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 bundle_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub enum SeccompMode {
#[default]
Enforce,
Trace,
}
impl ContainerConfig {
#[deprecated(
since = "0.2.1",
note = "Use try_new() instead to handle errors gracefully"
)]
pub fn new(name: Option<String>, command: Vec<String>) -> Self {
Self::try_new(name, command).expect("secure container ID generation failed")
}
pub fn try_new(name: Option<String>, command: Vec<String>) -> crate::error::Result<Self> {
let id = 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,
egress_policy: None,
health_check: None,
readiness_probe: None,
secrets: Vec::new(),
environment: Vec::new(),
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,
caps_policy: None,
caps_policy_sha256: None,
landlock_policy: None,
landlock_policy_sha256: None,
hooks: None,
pid_file: None,
console_socket: None,
bundle_dir: 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_egress_policy(mut self, policy: EgressPolicy) -> Self {
self.egress_policy = Some(policy);
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_env(mut self, key: String, value: String) -> Self {
self.environment.push((key, value));
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_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
}
#[must_use]
pub fn with_bundle_dir(mut self, path: PathBuf) -> Self {
self.bundle_dir = Some(path);
self
}
pub fn validate_production_mode(&self) -> crate::error::Result<()> {
if self.service_mode != ServiceMode::Production {
return Ok(());
}
if self.allow_degraded_security {
return Err(crate::error::NucleusError::ConfigError(
"Production mode forbids --allow-degraded-security".to_string(),
));
}
if self.allow_chroot_fallback {
return Err(crate::error::NucleusError::ConfigError(
"Production mode forbids --allow-chroot-fallback".to_string(),
));
}
if self.allow_host_network {
return Err(crate::error::NucleusError::ConfigError(
"Production mode forbids --allow-host-network".to_string(),
));
}
if matches!(self.network, crate::network::NetworkMode::Host) {
return Err(crate::error::NucleusError::ConfigError(
"Production mode forbids host network mode".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(),
));
};
let is_test_rootfs = rootfs_path
.to_string_lossy()
.contains("nucleus-test-nix-store");
if !rootfs_path.starts_with("/nix/store") && !is_test_rootfs {
return Err(crate::error::NucleusError::ConfigError(
"Production mode requires a /nix/store rootfs path".to_string(),
));
}
if self.seccomp_mode == SeccompMode::Trace {
return Err(crate::error::NucleusError::ConfigError(
"Production mode forbids --seccomp-mode trace".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(),
));
}
if !rootfs_path.exists() {
return Err(crate::error::NucleusError::ConfigError(format!(
"Production mode rootfs path does not exist: {:?}",
rootfs_path
)));
}
Ok(())
}
pub fn validate_runtime_support(&self) -> crate::error::Result<()> {
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(),
));
}
for secret in &self.secrets {
normalize_container_destination(&secret.dest)?;
}
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_profile.is_some() || self.seccomp_log_denied {
return Err(crate::error::NucleusError::ConfigError(
"gVisor runtime does not support custom seccomp profiles or seccomp deny logging; use --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: &str,
oci: bool,
) -> crate::error::Result<Self> {
match runtime {
"native" => {
if oci {
return Err(crate::error::NucleusError::ConfigError(
"--bundle requires gVisor runtime; use --runtime gvisor".to_string(),
));
}
self = self.with_gvisor(false).with_trust_level(TrustLevel::Trusted);
}
"gvisor" => {
self = self.with_gvisor(true);
if !oci {
tracing::info!(
"Security hardening: enabling OCI bundle mode for gVisor runtime"
);
}
self = self.with_oci_bundle();
}
other => {
return Err(crate::error::NucleusError::ConfigError(format!(
"Unknown runtime '{}'; supported values are 'native' and 'gvisor'",
other
)));
}
}
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::new(None, vec!["/bin/sh".to_string()]);
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.secrets.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_production_mode_rejects_degraded_flags() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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::new(None, vec!["/bin/sh".to_string()])
.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::new(None, vec!["/bin/sh".to_string()])
.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 {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::SeqCst);
let real_dir = std::env::temp_dir().join(format!(
"nucleus-test-real-rootfs-{}-{}",
std::process::id(),
id
));
std::fs::create_dir_all(&real_dir).unwrap();
let fake_nix_store = std::env::temp_dir().join(format!(
"nucleus-test-nix-store-{}-{}",
std::process::id(),
id
));
let link = fake_nix_store.join("nucleus-test-rootfs");
std::fs::create_dir_all(&fake_nix_store).unwrap();
std::os::unix::fs::symlink(&real_dir, &link).unwrap();
link
}
#[test]
fn test_production_mode_requires_memory_limit() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.with_service_mode(ServiceMode::Production)
.with_rootfs_path(rootfs);
let err = cfg.validate_production_mode().unwrap_err();
let _ = std::fs::remove_dir_all(&cfg.rootfs_path.as_ref().unwrap());
assert!(err.to_string().contains("--memory"));
}
#[test]
fn test_production_mode_valid_config() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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();
let _ = std::fs::remove_dir_all(&rootfs);
assert!(result.is_ok());
}
#[test]
fn test_production_mode_requires_rootfs_attestation() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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();
let _ = std::fs::remove_dir_all(&rootfs);
assert!(err.to_string().contains("attestation"));
}
#[test]
fn test_production_mode_rejects_seccomp_trace() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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();
let _ = std::fs::remove_dir_all(&rootfs);
assert!(
err.to_string().contains("trace"),
"Production mode must reject seccomp trace mode"
);
}
#[test]
fn test_production_mode_requires_cpu_limit() {
let rootfs = test_rootfs_path();
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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();
let _ = std::fs::remove_dir_all(&rootfs);
assert!(err.to_string().contains("--cpus"));
}
#[test]
fn test_config_security_builders_override_defaults() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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::new(None, vec!["/bin/sh".to_string()])
.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::new(None, vec!["/bin/sh".to_string()])
.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_rejects_native_security_policy_files() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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("gVisor runtime"));
}
#[test]
fn test_gvisor_rejects_landlock_policy_file() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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::new(None, vec!["/bin/sh".to_string()])
.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_secret_dest_must_be_absolute() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]).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::new(None, vec!["/bin/sh".to_string()]).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_gvisor_rejects_bind_mount_context_integrity_verification() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.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::new(None, vec!["/bin/sh".to_string()]).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::new(None, vec!["/bin/sh".to_string()]).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::new(None, vec!["/bin/sh".to_string()])
.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_native_runtime_disables_gvisor() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
.with_gvisor(false)
.with_trust_level(TrustLevel::Trusted);
assert!(!cfg.use_gvisor, "native runtime must disable gVisor");
assert_eq!(
cfg.trust_level,
TrustLevel::Trusted,
"native runtime must set Trusted trust level"
);
}
#[test]
fn test_default_config_has_gvisor_enabled() {
let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]);
assert!(cfg.use_gvisor, "default must have gVisor enabled");
assert_eq!(
cfg.trust_level,
TrustLevel::Untrusted,
"default must be Untrusted"
);
}
#[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
);
}
}