Skip to main content

nucleus/container/
config.rs

1use crate::filesystem::normalize_container_destination;
2use crate::isolation::{NamespaceConfig, UserNamespaceConfig};
3use crate::network::EgressPolicy;
4use crate::resources::ResourceLimits;
5use crate::security::GVisorPlatform;
6use std::path::PathBuf;
7use std::time::Duration;
8
9/// Generate a unique 32-hex-char container ID (128-bit) using /dev/urandom.
10pub fn generate_container_id() -> crate::error::Result<String> {
11    use std::io::Read;
12
13    let mut buf = [0u8; 16];
14    let mut file = std::fs::File::open("/dev/urandom").map_err(|e| {
15        crate::error::NucleusError::ConfigError(format!(
16            "Failed to open /dev/urandom for container ID generation: {}",
17            e
18        ))
19    })?;
20    file.read_exact(&mut buf).map_err(|e| {
21        crate::error::NucleusError::ConfigError(format!(
22            "Failed to read secure random bytes for container ID generation: {}",
23            e
24        ))
25    })?;
26    Ok(hex::encode(buf))
27}
28
29/// Trust level for a container workload.
30///
31/// Determines the minimum isolation guarantees the runtime must enforce.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
33pub enum TrustLevel {
34    /// Native kernel isolation (namespaces + seccomp + Landlock) is acceptable.
35    Trusted,
36    /// Requires gVisor; refuses to start without it unless degraded mode is allowed.
37    #[default]
38    Untrusted,
39}
40
41/// Service mode for the container.
42///
43/// Determines whether the container runs as an ephemeral agent sandbox
44/// or a long-running production service with stricter requirements.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
46pub enum ServiceMode {
47    /// Ephemeral agent workload (default). Allows degraded fallbacks.
48    #[default]
49    Agent,
50    /// Long-running production service. Enforces strict security invariants:
51    /// - Forbids degraded security, chroot fallback, and host networking
52    /// - Requires cgroup resource limits
53    /// - Requires pivot_root (no chroot fallback)
54    /// - Requires explicit rootfs path (no host bind mounts)
55    Production,
56}
57
58/// Required host kernel lockdown mode, when asserted by the runtime.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
60pub enum KernelLockdownMode {
61    /// Integrity mode blocks kernel writes from privileged userspace.
62    Integrity,
63    /// Confidentiality mode additionally blocks kernel data disclosure paths.
64    Confidentiality,
65}
66
67impl KernelLockdownMode {
68    pub fn as_str(self) -> &'static str {
69        match self {
70            Self::Integrity => "integrity",
71            Self::Confidentiality => "confidentiality",
72        }
73    }
74
75    pub fn accepts(self, active: Self) -> bool {
76        match self {
77            Self::Integrity => matches!(active, Self::Integrity | Self::Confidentiality),
78            Self::Confidentiality => matches!(active, Self::Confidentiality),
79        }
80    }
81}
82
83/// Health check configuration for long-running services.
84#[derive(Debug, Clone)]
85pub struct HealthCheck {
86    /// Command to run inside the container to check health.
87    pub command: Vec<String>,
88    /// Interval between health checks.
89    pub interval: Duration,
90    /// Number of consecutive failures before marking unhealthy.
91    pub retries: u32,
92    /// Grace period after start before health checks begin.
93    pub start_period: Duration,
94    /// Timeout for each health check execution.
95    pub timeout: Duration,
96}
97
98impl Default for HealthCheck {
99    fn default() -> Self {
100        Self {
101            command: Vec::new(),
102            interval: Duration::from_secs(30),
103            retries: 3,
104            start_period: Duration::from_secs(5),
105            timeout: Duration::from_secs(5),
106        }
107    }
108}
109
110/// Secrets configuration for mounting secret files into the container.
111#[derive(Debug, Clone)]
112pub struct SecretMount {
113    /// Source path on the host (or Nix store path).
114    pub source: PathBuf,
115    /// Destination path inside the container.
116    pub dest: PathBuf,
117    /// File mode (default: 0o400, read-only by owner).
118    pub mode: u32,
119}
120
121/// Readiness probe configuration.
122#[derive(Debug, Clone)]
123pub enum ReadinessProbe {
124    /// Run a command; ready when it exits 0.
125    Exec { command: Vec<String> },
126    /// Check TCP port connectivity.
127    TcpPort(u16),
128    /// Use sd_notify protocol (service sends READY=1).
129    SdNotify,
130}
131
132/// Container configuration
133#[derive(Debug, Clone)]
134pub struct ContainerConfig {
135    /// Unique container ID (auto-generated 32 hex chars, 128-bit)
136    pub id: String,
137
138    /// User-supplied container name (optional, defaults to ID)
139    pub name: String,
140
141    /// Command to execute in the container
142    pub command: Vec<String>,
143
144    /// Context directory to pre-populate (optional)
145    pub context_dir: Option<PathBuf>,
146
147    /// Resource limits
148    pub limits: ResourceLimits,
149
150    /// Namespace configuration
151    pub namespaces: NamespaceConfig,
152
153    /// User namespace configuration (for rootless mode)
154    pub user_ns_config: Option<UserNamespaceConfig>,
155
156    /// Hostname to set in UTS namespace (optional)
157    pub hostname: Option<String>,
158
159    /// Whether to use gVisor runtime
160    pub use_gvisor: bool,
161
162    /// Trust level for this workload
163    pub trust_level: TrustLevel,
164
165    /// Network mode
166    pub network: crate::network::NetworkMode,
167
168    /// Context mode (copy or bind mount)
169    pub context_mode: crate::filesystem::ContextMode,
170
171    /// Allow degraded security behavior if a hardening layer cannot be applied
172    pub allow_degraded_security: bool,
173
174    /// Allow chroot fallback when pivot_root fails (weaker isolation)
175    pub allow_chroot_fallback: bool,
176
177    /// Require explicit opt-in for host networking
178    pub allow_host_network: bool,
179
180    /// Mount /proc read-only inside the container
181    pub proc_readonly: bool,
182
183    /// Service mode (agent vs production)
184    pub service_mode: ServiceMode,
185
186    /// Pre-built rootfs path (Nix store path). When set, this is bind-mounted
187    /// as the container root instead of bind-mounting host /bin, /usr, /lib, etc.
188    pub rootfs_path: Option<PathBuf>,
189
190    /// Egress policy for audited outbound network access.
191    pub egress_policy: Option<EgressPolicy>,
192
193    /// Health check configuration for long-running services.
194    pub health_check: Option<HealthCheck>,
195
196    /// Readiness probe for service startup detection.
197    pub readiness_probe: Option<ReadinessProbe>,
198
199    /// Secret files to mount into the container.
200    pub secrets: Vec<SecretMount>,
201
202    /// Environment variables to pass to the container process.
203    pub environment: Vec<(String, String)>,
204
205    /// Desired topology config hash for reconciliation change detection.
206    pub config_hash: Option<u64>,
207
208    /// Enable sd_notify integration (pass NOTIFY_SOCKET into container).
209    pub sd_notify: bool,
210
211    /// Require the host kernel to be in at least this lockdown mode.
212    pub required_kernel_lockdown: Option<KernelLockdownMode>,
213
214    /// Verify context contents before executing the workload.
215    pub verify_context_integrity: bool,
216
217    /// Verify rootfs attestation manifest before mounting it.
218    pub verify_rootfs_attestation: bool,
219
220    /// Request kernel logging for denied seccomp decisions when supported.
221    pub seccomp_log_denied: bool,
222
223    /// Select the gVisor platform backend.
224    pub gvisor_platform: GVisorPlatform,
225
226    /// Path to a per-service seccomp profile (JSON, OCI subset format).
227    /// When set, this profile is used instead of the built-in allowlist.
228    pub seccomp_profile: Option<PathBuf>,
229
230    /// Expected SHA-256 hash of the seccomp profile file for integrity verification.
231    pub seccomp_profile_sha256: Option<String>,
232
233    /// Seccomp operating mode.
234    pub seccomp_mode: SeccompMode,
235
236    /// Path to write seccomp trace log (NDJSON) when seccomp_mode == Trace.
237    pub seccomp_trace_log: Option<PathBuf>,
238
239    /// Path to capability policy file (TOML).
240    pub caps_policy: Option<PathBuf>,
241
242    /// Expected SHA-256 hash of the capability policy file.
243    pub caps_policy_sha256: Option<String>,
244
245    /// Path to Landlock policy file (TOML).
246    pub landlock_policy: Option<PathBuf>,
247
248    /// Expected SHA-256 hash of the Landlock policy file.
249    pub landlock_policy_sha256: Option<String>,
250
251    /// OCI lifecycle hooks to execute at various container lifecycle points.
252    pub hooks: Option<crate::security::OciHooks>,
253
254    /// Path to write the container PID (OCI --pid-file).
255    pub pid_file: Option<PathBuf>,
256
257    /// Path to AF_UNIX socket for console pseudo-terminal master (OCI --console-socket).
258    pub console_socket: Option<PathBuf>,
259
260    /// Override OCI bundle directory path (OCI --bundle).
261    pub bundle_dir: Option<PathBuf>,
262}
263
264/// Seccomp operating mode.
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
266pub enum SeccompMode {
267    /// Normal enforcement — deny unlisted syscalls.
268    #[default]
269    Enforce,
270    /// Trace mode — allow all syscalls but log them for profile generation.
271    /// Development only; rejected in production mode.
272    Trace,
273}
274
275impl ContainerConfig {
276    /// Create a new container config with a random ID.
277    ///
278    /// # Panics
279    /// Panics if secure random bytes cannot be read from `/dev/urandom`.
280    /// Prefer [`Self::try_new`] for production code.
281    #[deprecated(
282        since = "0.2.1",
283        note = "Use try_new() instead to handle errors gracefully"
284    )]
285    pub fn new(name: Option<String>, command: Vec<String>) -> Self {
286        Self::try_new(name, command).expect("secure container ID generation failed")
287    }
288
289    pub fn try_new(name: Option<String>, command: Vec<String>) -> crate::error::Result<Self> {
290        let id = generate_container_id()?;
291        let name = name.unwrap_or_else(|| id.clone());
292        Ok(Self {
293            id,
294            name: name.clone(),
295            command,
296            context_dir: None,
297            limits: ResourceLimits::default(),
298            namespaces: NamespaceConfig::default(),
299            user_ns_config: None,
300            hostname: Some(name),
301            use_gvisor: true,
302            trust_level: TrustLevel::default(),
303            network: crate::network::NetworkMode::None,
304            context_mode: crate::filesystem::ContextMode::Copy,
305            allow_degraded_security: false,
306            allow_chroot_fallback: false,
307            allow_host_network: false,
308            proc_readonly: true,
309            service_mode: ServiceMode::default(),
310            rootfs_path: None,
311            egress_policy: None,
312            health_check: None,
313            readiness_probe: None,
314            secrets: Vec::new(),
315            environment: Vec::new(),
316            config_hash: None,
317            sd_notify: false,
318            required_kernel_lockdown: None,
319            verify_context_integrity: false,
320            verify_rootfs_attestation: false,
321            seccomp_log_denied: false,
322            gvisor_platform: GVisorPlatform::default(),
323            seccomp_profile: None,
324            seccomp_profile_sha256: None,
325            seccomp_mode: SeccompMode::default(),
326            seccomp_trace_log: None,
327            caps_policy: None,
328            caps_policy_sha256: None,
329            landlock_policy: None,
330            landlock_policy_sha256: None,
331            hooks: None,
332            pid_file: None,
333            console_socket: None,
334            bundle_dir: None,
335        })
336    }
337
338    /// Enable rootless mode with user namespace mapping
339    #[must_use]
340    pub fn with_rootless(mut self) -> Self {
341        self.namespaces.user = true;
342        self.user_ns_config = Some(UserNamespaceConfig::rootless());
343        self
344    }
345
346    /// Configure custom user namespace mapping
347    #[must_use]
348    pub fn with_user_namespace(mut self, config: UserNamespaceConfig) -> Self {
349        self.namespaces.user = true;
350        self.user_ns_config = Some(config);
351        self
352    }
353
354    #[must_use]
355    pub fn with_context(mut self, dir: PathBuf) -> Self {
356        self.context_dir = Some(dir);
357        self
358    }
359
360    #[must_use]
361    pub fn with_limits(mut self, limits: ResourceLimits) -> Self {
362        self.limits = limits;
363        self
364    }
365
366    #[must_use]
367    pub fn with_namespaces(mut self, namespaces: NamespaceConfig) -> Self {
368        self.namespaces = namespaces;
369        self
370    }
371
372    #[must_use]
373    pub fn with_hostname(mut self, hostname: Option<String>) -> Self {
374        self.hostname = hostname;
375        self
376    }
377
378    #[must_use]
379    pub fn with_gvisor(mut self, enabled: bool) -> Self {
380        self.use_gvisor = enabled;
381        self
382    }
383
384    #[must_use]
385    pub fn with_trust_level(mut self, level: TrustLevel) -> Self {
386        self.trust_level = level;
387        self
388    }
389
390    /// Enable OCI bundle runtime path (always OCI for gVisor).
391    #[must_use]
392    pub fn with_oci_bundle(mut self) -> Self {
393        self.use_gvisor = true;
394        self
395    }
396
397    #[must_use]
398    pub fn with_network(mut self, mode: crate::network::NetworkMode) -> Self {
399        self.network = mode;
400        self
401    }
402
403    #[must_use]
404    pub fn with_context_mode(mut self, mode: crate::filesystem::ContextMode) -> Self {
405        self.context_mode = mode;
406        self
407    }
408
409    #[must_use]
410    pub fn with_allow_degraded_security(mut self, allow: bool) -> Self {
411        self.allow_degraded_security = allow;
412        self
413    }
414
415    #[must_use]
416    pub fn with_allow_chroot_fallback(mut self, allow: bool) -> Self {
417        self.allow_chroot_fallback = allow;
418        self
419    }
420
421    #[must_use]
422    pub fn with_allow_host_network(mut self, allow: bool) -> Self {
423        self.allow_host_network = allow;
424        self
425    }
426
427    #[must_use]
428    pub fn with_proc_readonly(mut self, proc_readonly: bool) -> Self {
429        self.proc_readonly = proc_readonly;
430        self
431    }
432
433    #[must_use]
434    pub fn with_service_mode(mut self, mode: ServiceMode) -> Self {
435        self.service_mode = mode;
436        self
437    }
438
439    #[must_use]
440    pub fn with_rootfs_path(mut self, path: PathBuf) -> Self {
441        self.rootfs_path = Some(path);
442        self
443    }
444
445    #[must_use]
446    pub fn with_egress_policy(mut self, policy: EgressPolicy) -> Self {
447        self.egress_policy = Some(policy);
448        self
449    }
450
451    #[must_use]
452    pub fn with_health_check(mut self, hc: HealthCheck) -> Self {
453        self.health_check = Some(hc);
454        self
455    }
456
457    #[must_use]
458    pub fn with_readiness_probe(mut self, probe: ReadinessProbe) -> Self {
459        self.readiness_probe = Some(probe);
460        self
461    }
462
463    #[must_use]
464    pub fn with_secret(mut self, secret: SecretMount) -> Self {
465        self.secrets.push(secret);
466        self
467    }
468
469    #[must_use]
470    pub fn with_env(mut self, key: String, value: String) -> Self {
471        self.environment.push((key, value));
472        self
473    }
474
475    #[must_use]
476    pub fn with_config_hash(mut self, hash: u64) -> Self {
477        self.config_hash = Some(hash);
478        self
479    }
480
481    #[must_use]
482    pub fn with_sd_notify(mut self, enabled: bool) -> Self {
483        self.sd_notify = enabled;
484        self
485    }
486
487    #[must_use]
488    pub fn with_required_kernel_lockdown(mut self, mode: KernelLockdownMode) -> Self {
489        self.required_kernel_lockdown = Some(mode);
490        self
491    }
492
493    #[must_use]
494    pub fn with_verify_context_integrity(mut self, enabled: bool) -> Self {
495        self.verify_context_integrity = enabled;
496        self
497    }
498
499    #[must_use]
500    pub fn with_verify_rootfs_attestation(mut self, enabled: bool) -> Self {
501        self.verify_rootfs_attestation = enabled;
502        self
503    }
504
505    #[must_use]
506    pub fn with_seccomp_log_denied(mut self, enabled: bool) -> Self {
507        self.seccomp_log_denied = enabled;
508        self
509    }
510
511    #[must_use]
512    pub fn with_gvisor_platform(mut self, platform: GVisorPlatform) -> Self {
513        self.gvisor_platform = platform;
514        self
515    }
516
517    #[must_use]
518    pub fn with_seccomp_profile(mut self, path: PathBuf) -> Self {
519        self.seccomp_profile = Some(path);
520        self
521    }
522
523    #[must_use]
524    pub fn with_seccomp_profile_sha256(mut self, hash: String) -> Self {
525        self.seccomp_profile_sha256 = Some(hash);
526        self
527    }
528
529    #[must_use]
530    pub fn with_seccomp_mode(mut self, mode: SeccompMode) -> Self {
531        self.seccomp_mode = mode;
532        self
533    }
534
535    #[must_use]
536    pub fn with_seccomp_trace_log(mut self, path: PathBuf) -> Self {
537        self.seccomp_trace_log = Some(path);
538        self
539    }
540
541    #[must_use]
542    pub fn with_caps_policy(mut self, path: PathBuf) -> Self {
543        self.caps_policy = Some(path);
544        self
545    }
546
547    #[must_use]
548    pub fn with_caps_policy_sha256(mut self, hash: String) -> Self {
549        self.caps_policy_sha256 = Some(hash);
550        self
551    }
552
553    #[must_use]
554    pub fn with_landlock_policy(mut self, path: PathBuf) -> Self {
555        self.landlock_policy = Some(path);
556        self
557    }
558
559    #[must_use]
560    pub fn with_landlock_policy_sha256(mut self, hash: String) -> Self {
561        self.landlock_policy_sha256 = Some(hash);
562        self
563    }
564
565    #[must_use]
566    pub fn with_pid_file(mut self, path: PathBuf) -> Self {
567        self.pid_file = Some(path);
568        self
569    }
570
571    #[must_use]
572    pub fn with_console_socket(mut self, path: PathBuf) -> Self {
573        self.console_socket = Some(path);
574        self
575    }
576
577    #[must_use]
578    pub fn with_bundle_dir(mut self, path: PathBuf) -> Self {
579        self.bundle_dir = Some(path);
580        self
581    }
582
583    /// Validate that production mode invariants are satisfied.
584    /// Called before container startup when service_mode == Production.
585    pub fn validate_production_mode(&self) -> crate::error::Result<()> {
586        if self.service_mode != ServiceMode::Production {
587            return Ok(());
588        }
589
590        if self.allow_degraded_security {
591            return Err(crate::error::NucleusError::ConfigError(
592                "Production mode forbids --allow-degraded-security".to_string(),
593            ));
594        }
595
596        if self.allow_chroot_fallback {
597            return Err(crate::error::NucleusError::ConfigError(
598                "Production mode forbids --allow-chroot-fallback".to_string(),
599            ));
600        }
601
602        if self.allow_host_network {
603            return Err(crate::error::NucleusError::ConfigError(
604                "Production mode forbids --allow-host-network".to_string(),
605            ));
606        }
607
608        if matches!(self.network, crate::network::NetworkMode::Host) {
609            return Err(crate::error::NucleusError::ConfigError(
610                "Production mode forbids host network mode".to_string(),
611            ));
612        }
613
614        // Production mode requires explicit rootfs (no host bind mount fallback)
615        let Some(rootfs_path) = self.rootfs_path.as_ref() else {
616            return Err(crate::error::NucleusError::ConfigError(
617                "Production mode requires explicit --rootfs path (no host bind mounts)".to_string(),
618            ));
619        };
620
621        // Allow test rootfs paths under /tmp that simulate /nix/store structure
622        let is_test_rootfs = rootfs_path
623            .to_string_lossy()
624            .contains("nucleus-test-nix-store");
625        if !rootfs_path.starts_with("/nix/store") && !is_test_rootfs {
626            return Err(crate::error::NucleusError::ConfigError(
627                "Production mode requires a /nix/store rootfs path".to_string(),
628            ));
629        }
630
631        if self.seccomp_mode == SeccompMode::Trace {
632            return Err(crate::error::NucleusError::ConfigError(
633                "Production mode forbids --seccomp-mode trace".to_string(),
634            ));
635        }
636
637        // Production mode requires explicit resource limits
638        if self.limits.memory_bytes.is_none() {
639            return Err(crate::error::NucleusError::ConfigError(
640                "Production mode requires explicit --memory limit".to_string(),
641            ));
642        }
643
644        if self.limits.cpu_quota_us.is_none() {
645            return Err(crate::error::NucleusError::ConfigError(
646                "Production mode requires explicit --cpus limit".to_string(),
647            ));
648        }
649
650        if !self.verify_rootfs_attestation {
651            return Err(crate::error::NucleusError::ConfigError(
652                "Production mode requires --verify-rootfs-attestation".to_string(),
653            ));
654        }
655
656        // Verify rootfs exists (checked last, after config invariants)
657        if !rootfs_path.exists() {
658            return Err(crate::error::NucleusError::ConfigError(format!(
659                "Production mode rootfs path does not exist: {:?}",
660                rootfs_path
661            )));
662        }
663
664        Ok(())
665    }
666
667    /// Validate runtime-specific feature support.
668    pub fn validate_runtime_support(&self) -> crate::error::Result<()> {
669        if self.seccomp_mode == SeccompMode::Trace && self.seccomp_trace_log.is_none() {
670            return Err(crate::error::NucleusError::ConfigError(
671                "Seccomp trace mode requires --seccomp-log / seccomp_trace_log".to_string(),
672            ));
673        }
674
675        for secret in &self.secrets {
676            normalize_container_destination(&secret.dest)?;
677        }
678
679        if !self.use_gvisor {
680            return Ok(());
681        }
682
683        if self.seccomp_mode == SeccompMode::Trace {
684            return Err(crate::error::NucleusError::ConfigError(
685                "gVisor runtime does not support --seccomp-mode trace; use --runtime native"
686                    .to_string(),
687            ));
688        }
689
690        if self.seccomp_profile.is_some() || self.seccomp_log_denied {
691            return Err(crate::error::NucleusError::ConfigError(
692                "gVisor runtime does not support custom seccomp profiles or seccomp deny logging; use --runtime native"
693                    .to_string(),
694            ));
695        }
696
697        if self.caps_policy.is_some() {
698            return Err(crate::error::NucleusError::ConfigError(
699                "gVisor runtime does not support capability policy files; use --runtime native"
700                    .to_string(),
701            ));
702        }
703
704        if self.landlock_policy.is_some() {
705            return Err(crate::error::NucleusError::ConfigError(
706                "gVisor runtime does not support Landlock policy files; use --runtime native"
707                    .to_string(),
708            ));
709        }
710
711        if self.health_check.is_some() {
712            return Err(crate::error::NucleusError::ConfigError(
713                "gVisor runtime does not support exec health checks; use --runtime native or remove --health-cmd"
714                    .to_string(),
715            ));
716        }
717
718        if matches!(
719            self.readiness_probe.as_ref(),
720            Some(ReadinessProbe::Exec { .. }) | Some(ReadinessProbe::TcpPort(_))
721        ) {
722            return Err(crate::error::NucleusError::ConfigError(
723                "gVisor runtime does not support exec/TCP readiness probes; use --runtime native or --readiness-sd-notify"
724                    .to_string(),
725            ));
726        }
727
728        if self.verify_context_integrity
729            && self.context_dir.is_some()
730            && matches!(self.context_mode, crate::filesystem::ContextMode::BindMount)
731        {
732            return Err(crate::error::NucleusError::ConfigError(
733                "gVisor runtime cannot verify bind-mounted context integrity; use --context-mode copy or disable --verify-context-integrity"
734                    .to_string(),
735            ));
736        }
737
738        Ok(())
739    }
740
741    /// Apply runtime selection (native vs gVisor) and OCI bundle mode.
742    pub fn apply_runtime_selection(
743        mut self,
744        runtime: &str,
745        oci: bool,
746    ) -> crate::error::Result<Self> {
747        match runtime {
748            "native" => {
749                if oci {
750                    return Err(crate::error::NucleusError::ConfigError(
751                        "--bundle requires gVisor runtime; use --runtime gvisor".to_string(),
752                    ));
753                }
754                self = self.with_gvisor(false).with_trust_level(TrustLevel::Trusted);
755            }
756            "gvisor" => {
757                self = self.with_gvisor(true);
758                if !oci {
759                    tracing::info!(
760                        "Security hardening: enabling OCI bundle mode for gVisor runtime"
761                    );
762                }
763                self = self.with_oci_bundle();
764            }
765            other => {
766                return Err(crate::error::NucleusError::ConfigError(format!(
767                    "Unknown runtime '{}'; supported values are 'native' and 'gvisor'",
768                    other
769                )));
770            }
771        }
772        Ok(self)
773    }
774}
775
776/// Validate a container name for safe use.
777pub fn validate_container_name(name: &str) -> crate::error::Result<()> {
778    if name.is_empty() || name.len() > 128 {
779        return Err(crate::error::NucleusError::ConfigError(
780            "Invalid container name: must be 1-128 characters".to_string(),
781        ));
782    }
783    if !name
784        .chars()
785        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
786    {
787        return Err(crate::error::NucleusError::ConfigError(
788            "Invalid container name: allowed characters are a-zA-Z0-9, '-', '_', '.'".to_string(),
789        ));
790    }
791    Ok(())
792}
793
794/// Validate a hostname according to RFC 1123.
795pub fn validate_hostname(hostname: &str) -> crate::error::Result<()> {
796    if hostname.is_empty() || hostname.len() > 253 {
797        return Err(crate::error::NucleusError::ConfigError(
798            "Invalid hostname: must be 1-253 characters".to_string(),
799        ));
800    }
801
802    for label in hostname.split('.') {
803        if label.is_empty() || label.len() > 63 {
804            return Err(crate::error::NucleusError::ConfigError(format!(
805                "Invalid hostname label: '{}'",
806                label
807            )));
808        }
809        if label.starts_with('-') || label.ends_with('-') {
810            return Err(crate::error::NucleusError::ConfigError(format!(
811                "Invalid hostname label '{}': cannot start or end with '-'",
812                label
813            )));
814        }
815        if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
816            return Err(crate::error::NucleusError::ConfigError(format!(
817                "Invalid hostname label '{}': allowed characters are a-zA-Z0-9 and '-'",
818                label
819            )));
820        }
821    }
822
823    Ok(())
824}
825
826#[cfg(test)]
827#[allow(deprecated)]
828mod tests {
829    use super::*;
830    use crate::network::NetworkMode;
831
832    #[test]
833    fn test_generate_container_id_is_32_hex_chars() {
834        let id = generate_container_id().unwrap();
835        assert_eq!(
836            id.len(),
837            32,
838            "Container ID must be full 128-bit (32 hex chars), got {}",
839            id.len()
840        );
841        assert!(
842            id.chars().all(|c| c.is_ascii_hexdigit()),
843            "Container ID must be hex: {}",
844            id
845        );
846    }
847
848    #[test]
849    fn test_generate_container_id_is_unique() {
850        let id1 = generate_container_id().unwrap();
851        let id2 = generate_container_id().unwrap();
852        assert_ne!(id1, id2, "Two consecutive IDs must differ");
853    }
854
855    #[test]
856    fn test_config_security_defaults_are_hardened() {
857        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]);
858        assert!(!cfg.allow_degraded_security);
859        assert!(!cfg.allow_chroot_fallback);
860        assert!(!cfg.allow_host_network);
861        assert!(cfg.proc_readonly);
862        assert_eq!(cfg.service_mode, ServiceMode::Agent);
863        assert!(cfg.rootfs_path.is_none());
864        assert!(cfg.egress_policy.is_none());
865        assert!(cfg.secrets.is_empty());
866        assert!(!cfg.sd_notify);
867        assert!(cfg.required_kernel_lockdown.is_none());
868        assert!(!cfg.verify_context_integrity);
869        assert!(!cfg.verify_rootfs_attestation);
870        assert!(!cfg.seccomp_log_denied);
871        assert_eq!(cfg.gvisor_platform, GVisorPlatform::Systrap);
872    }
873
874    #[test]
875    fn test_production_mode_rejects_degraded_flags() {
876        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
877            .with_service_mode(ServiceMode::Production)
878            .with_allow_degraded_security(true)
879            .with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
880            .with_limits(
881                crate::resources::ResourceLimits::default()
882                    .with_memory("512M")
883                    .unwrap()
884                    .with_cpu_cores(2.0)
885                    .unwrap(),
886            );
887        assert!(cfg.validate_production_mode().is_err());
888    }
889
890    #[test]
891    fn test_production_mode_rejects_chroot_fallback() {
892        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
893            .with_service_mode(ServiceMode::Production)
894            .with_allow_chroot_fallback(true)
895            .with_rootfs_path(std::path::PathBuf::from("/nix/store/fake-rootfs"))
896            .with_limits(
897                crate::resources::ResourceLimits::default()
898                    .with_memory("512M")
899                    .unwrap()
900                    .with_cpu_cores(2.0)
901                    .unwrap(),
902            );
903        let err = cfg.validate_production_mode().unwrap_err();
904        assert!(
905            err.to_string().contains("chroot"),
906            "Production mode must reject chroot fallback"
907        );
908    }
909
910    #[test]
911    fn test_production_mode_requires_rootfs() {
912        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
913            .with_service_mode(ServiceMode::Production)
914            .with_limits(
915                crate::resources::ResourceLimits::default()
916                    .with_memory("512M")
917                    .unwrap(),
918            );
919        let err = cfg.validate_production_mode().unwrap_err();
920        assert!(err.to_string().contains("--rootfs"));
921    }
922
923    fn test_rootfs_path() -> std::path::PathBuf {
924        use std::sync::atomic::{AtomicU64, Ordering};
925        static COUNTER: AtomicU64 = AtomicU64::new(0);
926        let id = COUNTER.fetch_add(1, Ordering::SeqCst);
927
928        let real_dir = std::env::temp_dir().join(format!(
929            "nucleus-test-real-rootfs-{}-{}",
930            std::process::id(),
931            id
932        ));
933        std::fs::create_dir_all(&real_dir).unwrap();
934
935        let fake_nix_store = std::env::temp_dir().join(format!(
936            "nucleus-test-nix-store-{}-{}",
937            std::process::id(),
938            id
939        ));
940        let link = fake_nix_store.join("nucleus-test-rootfs");
941        std::fs::create_dir_all(&fake_nix_store).unwrap();
942        std::os::unix::fs::symlink(&real_dir, &link).unwrap();
943
944        link
945    }
946
947    #[test]
948    fn test_production_mode_requires_memory_limit() {
949        let rootfs = test_rootfs_path();
950        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
951            .with_service_mode(ServiceMode::Production)
952            .with_rootfs_path(rootfs);
953        let err = cfg.validate_production_mode().unwrap_err();
954        let _ = std::fs::remove_dir_all(&cfg.rootfs_path.as_ref().unwrap());
955        assert!(err.to_string().contains("--memory"));
956    }
957
958    #[test]
959    fn test_production_mode_valid_config() {
960        let rootfs = test_rootfs_path();
961        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
962            .with_service_mode(ServiceMode::Production)
963            .with_rootfs_path(rootfs.clone())
964            .with_verify_rootfs_attestation(true)
965            .with_limits(
966                crate::resources::ResourceLimits::default()
967                    .with_memory("512M")
968                    .unwrap()
969                    .with_cpu_cores(2.0)
970                    .unwrap(),
971            );
972        let result = cfg.validate_production_mode();
973        let _ = std::fs::remove_dir_all(&rootfs);
974        assert!(result.is_ok());
975    }
976
977    #[test]
978    fn test_production_mode_requires_rootfs_attestation() {
979        let rootfs = test_rootfs_path();
980        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
981            .with_service_mode(ServiceMode::Production)
982            .with_rootfs_path(rootfs.clone())
983            .with_limits(
984                crate::resources::ResourceLimits::default()
985                    .with_memory("512M")
986                    .unwrap()
987                    .with_cpu_cores(2.0)
988                    .unwrap(),
989            );
990        let err = cfg.validate_production_mode().unwrap_err();
991        let _ = std::fs::remove_dir_all(&rootfs);
992        assert!(err.to_string().contains("attestation"));
993    }
994
995    #[test]
996    fn test_production_mode_rejects_seccomp_trace() {
997        let rootfs = test_rootfs_path();
998        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
999            .with_service_mode(ServiceMode::Production)
1000            .with_rootfs_path(rootfs.clone())
1001            .with_seccomp_mode(SeccompMode::Trace)
1002            .with_limits(
1003                crate::resources::ResourceLimits::default()
1004                    .with_memory("512M")
1005                    .unwrap()
1006                    .with_cpu_cores(2.0)
1007                    .unwrap(),
1008            );
1009        let err = cfg.validate_production_mode().unwrap_err();
1010        let _ = std::fs::remove_dir_all(&rootfs);
1011        assert!(
1012            err.to_string().contains("trace"),
1013            "Production mode must reject seccomp trace mode"
1014        );
1015    }
1016
1017    #[test]
1018    fn test_production_mode_requires_cpu_limit() {
1019        let rootfs = test_rootfs_path();
1020        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1021            .with_service_mode(ServiceMode::Production)
1022            .with_rootfs_path(rootfs.clone())
1023            .with_limits(
1024                crate::resources::ResourceLimits::default()
1025                    .with_memory("512M")
1026                    .unwrap(),
1027            );
1028        let err = cfg.validate_production_mode().unwrap_err();
1029        let _ = std::fs::remove_dir_all(&rootfs);
1030        assert!(err.to_string().contains("--cpus"));
1031    }
1032
1033    #[test]
1034    fn test_config_security_builders_override_defaults() {
1035        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1036            .with_allow_degraded_security(true)
1037            .with_allow_chroot_fallback(true)
1038            .with_allow_host_network(true)
1039            .with_proc_readonly(false)
1040            .with_network(NetworkMode::Host);
1041
1042        assert!(cfg.allow_degraded_security);
1043        assert!(cfg.allow_chroot_fallback);
1044        assert!(cfg.allow_host_network);
1045        assert!(!cfg.proc_readonly);
1046        assert!(matches!(cfg.network, NetworkMode::Host));
1047    }
1048
1049    #[test]
1050    fn test_hardening_builders_override_defaults() {
1051        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1052            .with_required_kernel_lockdown(KernelLockdownMode::Confidentiality)
1053            .with_verify_context_integrity(true)
1054            .with_verify_rootfs_attestation(true)
1055            .with_seccomp_log_denied(true)
1056            .with_gvisor_platform(GVisorPlatform::Kvm);
1057
1058        assert_eq!(
1059            cfg.required_kernel_lockdown,
1060            Some(KernelLockdownMode::Confidentiality)
1061        );
1062        assert!(cfg.verify_context_integrity);
1063        assert!(cfg.verify_rootfs_attestation);
1064        assert!(cfg.seccomp_log_denied);
1065        assert_eq!(cfg.gvisor_platform, GVisorPlatform::Kvm);
1066    }
1067
1068    #[test]
1069    fn test_seccomp_trace_requires_log_path() {
1070        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1071            .with_gvisor(false)
1072            .with_seccomp_mode(SeccompMode::Trace);
1073
1074        let err = cfg.validate_runtime_support().unwrap_err();
1075        assert!(err.to_string().contains("seccomp-log"));
1076    }
1077
1078    #[test]
1079    fn test_gvisor_rejects_native_security_policy_files() {
1080        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1081            .with_seccomp_profile(PathBuf::from("/tmp/seccomp.json"))
1082            .with_caps_policy(PathBuf::from("/tmp/caps.toml"));
1083
1084        let err = cfg.validate_runtime_support().unwrap_err();
1085        assert!(err.to_string().contains("gVisor runtime"));
1086    }
1087
1088    #[test]
1089    fn test_gvisor_rejects_landlock_policy_file() {
1090        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1091            .with_landlock_policy(PathBuf::from("/tmp/landlock.toml"));
1092
1093        let err = cfg.validate_runtime_support().unwrap_err();
1094        assert!(err.to_string().contains("Landlock"));
1095    }
1096
1097    #[test]
1098    fn test_gvisor_rejects_trace_mode_even_with_log_path() {
1099        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1100            .with_seccomp_mode(SeccompMode::Trace)
1101            .with_seccomp_trace_log(PathBuf::from("/tmp/trace.ndjson"));
1102
1103        let err = cfg.validate_runtime_support().unwrap_err();
1104        assert!(err.to_string().contains("gVisor runtime"));
1105    }
1106
1107    #[test]
1108    fn test_secret_dest_must_be_absolute() {
1109        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]).with_secret(
1110            crate::container::SecretMount {
1111                source: PathBuf::from("/run/secrets/api-key"),
1112                dest: PathBuf::from("secrets/api-key"),
1113                mode: 0o400,
1114            },
1115        );
1116
1117        let err = cfg.validate_runtime_support().unwrap_err();
1118        assert!(err.to_string().contains("absolute"));
1119    }
1120
1121    #[test]
1122    fn test_secret_dest_rejects_parent_traversal() {
1123        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]).with_secret(
1124            crate::container::SecretMount {
1125                source: PathBuf::from("/run/secrets/api-key"),
1126                dest: PathBuf::from("/../../etc/passwd"),
1127                mode: 0o400,
1128            },
1129        );
1130
1131        let err = cfg.validate_runtime_support().unwrap_err();
1132        assert!(err.to_string().contains("parent traversal"));
1133    }
1134
1135    #[test]
1136    fn test_gvisor_rejects_bind_mount_context_integrity_verification() {
1137        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1138            .with_context(PathBuf::from("/tmp/context"))
1139            .with_context_mode(crate::filesystem::ContextMode::BindMount)
1140            .with_verify_context_integrity(true);
1141
1142        let err = cfg.validate_runtime_support().unwrap_err();
1143        assert!(err.to_string().contains("context integrity"));
1144    }
1145
1146    #[test]
1147    fn test_gvisor_rejects_exec_health_checks() {
1148        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]).with_health_check(
1149            HealthCheck {
1150                command: vec!["/bin/sh".to_string(), "-c".to_string(), "true".to_string()],
1151                interval: Duration::from_secs(30),
1152                retries: 3,
1153                start_period: Duration::from_secs(1),
1154                timeout: Duration::from_secs(5),
1155            },
1156        );
1157
1158        let err = cfg.validate_runtime_support().unwrap_err();
1159        assert!(err.to_string().contains("health checks"));
1160    }
1161
1162    #[test]
1163    fn test_gvisor_rejects_exec_readiness_probes() {
1164        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]).with_readiness_probe(
1165            ReadinessProbe::Exec {
1166                command: vec!["/bin/sh".to_string(), "-c".to_string(), "true".to_string()],
1167            },
1168        );
1169
1170        let err = cfg.validate_runtime_support().unwrap_err();
1171        assert!(err.to_string().contains("readiness"));
1172    }
1173
1174    #[test]
1175    fn test_gvisor_allows_copy_mode_context_integrity_verification() {
1176        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1177            .with_context(PathBuf::from("/tmp/context"))
1178            .with_context_mode(crate::filesystem::ContextMode::Copy)
1179            .with_verify_context_integrity(true);
1180
1181        assert!(cfg.validate_runtime_support().is_ok());
1182    }
1183
1184    #[test]
1185    fn test_native_runtime_disables_gvisor() {
1186        // --runtime native must explicitly disable gVisor and set Trusted trust level
1187        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()])
1188            .with_gvisor(false)
1189            .with_trust_level(TrustLevel::Trusted);
1190        assert!(!cfg.use_gvisor, "native runtime must disable gVisor");
1191        assert_eq!(
1192            cfg.trust_level,
1193            TrustLevel::Trusted,
1194            "native runtime must set Trusted trust level"
1195        );
1196    }
1197
1198    #[test]
1199    fn test_default_config_has_gvisor_enabled() {
1200        let cfg = ContainerConfig::new(None, vec!["/bin/sh".to_string()]);
1201        assert!(cfg.use_gvisor, "default must have gVisor enabled");
1202        assert_eq!(
1203            cfg.trust_level,
1204            TrustLevel::Untrusted,
1205            "default must be Untrusted"
1206        );
1207    }
1208
1209    #[test]
1210    fn test_generate_container_id_returns_result() {
1211        // BUG-07: generate_container_id must return Result, not panic.
1212        // Verify by calling it and checking the Ok value is valid hex.
1213        let id: crate::error::Result<String> = generate_container_id();
1214        let id = id.expect("generate_container_id must return Ok, not panic");
1215        assert_eq!(id.len(), 32, "container ID must be 32 hex chars");
1216        assert!(
1217            id.chars().all(|c| c.is_ascii_hexdigit()),
1218            "container ID must be valid hex: {}",
1219            id
1220        );
1221    }
1222}