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
9pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
33pub enum TrustLevel {
34 Trusted,
36 #[default]
38 Untrusted,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
46pub enum ServiceMode {
47 #[default]
49 Agent,
50 Production,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
60pub enum KernelLockdownMode {
61 Integrity,
63 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#[derive(Debug, Clone)]
85pub struct HealthCheck {
86 pub command: Vec<String>,
88 pub interval: Duration,
90 pub retries: u32,
92 pub start_period: Duration,
94 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#[derive(Debug, Clone)]
112pub struct SecretMount {
113 pub source: PathBuf,
115 pub dest: PathBuf,
117 pub mode: u32,
119}
120
121#[derive(Debug, Clone)]
123pub enum ReadinessProbe {
124 Exec { command: Vec<String> },
126 TcpPort(u16),
128 SdNotify,
130}
131
132#[derive(Debug, Clone)]
134pub struct ContainerConfig {
135 pub id: String,
137
138 pub name: String,
140
141 pub command: Vec<String>,
143
144 pub context_dir: Option<PathBuf>,
146
147 pub limits: ResourceLimits,
149
150 pub namespaces: NamespaceConfig,
152
153 pub user_ns_config: Option<UserNamespaceConfig>,
155
156 pub hostname: Option<String>,
158
159 pub use_gvisor: bool,
161
162 pub trust_level: TrustLevel,
164
165 pub network: crate::network::NetworkMode,
167
168 pub context_mode: crate::filesystem::ContextMode,
170
171 pub allow_degraded_security: bool,
173
174 pub allow_chroot_fallback: bool,
176
177 pub allow_host_network: bool,
179
180 pub proc_readonly: bool,
182
183 pub service_mode: ServiceMode,
185
186 pub rootfs_path: Option<PathBuf>,
189
190 pub egress_policy: Option<EgressPolicy>,
192
193 pub health_check: Option<HealthCheck>,
195
196 pub readiness_probe: Option<ReadinessProbe>,
198
199 pub secrets: Vec<SecretMount>,
201
202 pub environment: Vec<(String, String)>,
204
205 pub config_hash: Option<u64>,
207
208 pub sd_notify: bool,
210
211 pub required_kernel_lockdown: Option<KernelLockdownMode>,
213
214 pub verify_context_integrity: bool,
216
217 pub verify_rootfs_attestation: bool,
219
220 pub seccomp_log_denied: bool,
222
223 pub gvisor_platform: GVisorPlatform,
225
226 pub seccomp_profile: Option<PathBuf>,
229
230 pub seccomp_profile_sha256: Option<String>,
232
233 pub seccomp_mode: SeccompMode,
235
236 pub seccomp_trace_log: Option<PathBuf>,
238
239 pub caps_policy: Option<PathBuf>,
241
242 pub caps_policy_sha256: Option<String>,
244
245 pub landlock_policy: Option<PathBuf>,
247
248 pub landlock_policy_sha256: Option<String>,
250
251 pub hooks: Option<crate::security::OciHooks>,
253
254 pub pid_file: Option<PathBuf>,
256
257 pub console_socket: Option<PathBuf>,
259
260 pub bundle_dir: Option<PathBuf>,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
266pub enum SeccompMode {
267 #[default]
269 Enforce,
270 Trace,
273}
274
275impl ContainerConfig {
276 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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
776pub 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
794pub 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 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 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}