diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index eeb0c97..522dc01 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,6 +17,6 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build - run: cargo build --verbose + run: cd server_manager && cargo build --verbose - name: Run tests - run: cargo test --verbose + run: cd server_manager && cargo test --verbose diff --git a/server_manager/Cargo.lock b/server_manager/Cargo.lock index 0b2321c..2019f0a 100644 --- a/server_manager/Cargo.lock +++ b/server_manager/Cargo.lock @@ -525,7 +525,7 @@ dependencies = [ [[package]] name = "server_manager" -version = "0.1.0" +version = "1.0.0" dependencies = [ "anyhow", "async-trait", diff --git a/server_manager/src/core/system.rs b/server_manager/src/core/system.rs index 7fedf20..86fe01f 100644 --- a/server_manager/src/core/system.rs +++ b/server_manager/src/core/system.rs @@ -71,6 +71,9 @@ net.core.default_qdisc=fq net.ipv4.tcp_congestion_control=bbr net.core.somaxconn=4096 net.ipv4.tcp_fastopen=3 +vm.max_map_count=262144 +net.core.rmem_max=4194304 +net.core.wmem_max=1048576 "#, swappiness); let path = Path::new("/etc/sysctl.d/99-server-manager-optimization.conf"); diff --git a/server_manager/src/services/apps.rs b/server_manager/src/services/apps.rs index f2a158f..2ae4f1f 100644 --- a/server_manager/src/services/apps.rs +++ b/server_manager/src/services/apps.rs @@ -10,7 +10,7 @@ pub struct VaultwardenService; impl Service for VaultwardenService { fn name(&self) -> &'static str { "vaultwarden" } fn image(&self) -> &'static str { "vaultwarden/server:latest" } - fn ports(&self) -> Vec { vec!["8001:80".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:8001:80".to_string()] } fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); vars.insert("ADMIN_TOKEN".to_string(), secrets.vaultwarden_admin_token.clone().unwrap_or_default()); @@ -25,7 +25,7 @@ pub struct FilebrowserService; impl Service for FilebrowserService { fn name(&self) -> &'static str { "filebrowser" } fn image(&self) -> &'static str { "filebrowser/filebrowser:latest" } - fn ports(&self) -> Vec { vec!["8002:80".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:8002:80".to_string()] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec![ "./config/filebrowser:/config".to_string(), @@ -39,7 +39,7 @@ pub struct YourlsService; impl Service for YourlsService { fn name(&self) -> &'static str { "yourls" } fn image(&self) -> &'static str { "yourls/yourls:latest" } - fn ports(&self) -> Vec { vec!["8003:80".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:8003:80".to_string()] } fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); vars.insert("YOURLS_DB_HOST".to_string(), "mariadb".to_string()); @@ -57,7 +57,7 @@ pub struct GLPIService; impl Service for GLPIService { fn name(&self) -> &'static str { "glpi" } fn image(&self) -> &'static str { "diouxx/glpi:latest" } // Common community image, official docker-library is scarce - fn ports(&self) -> Vec { vec!["8088:80".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:8088:80".to_string()] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["./config/glpi:/var/www/html/glpi".to_string()] } @@ -68,7 +68,7 @@ pub struct GiteaService; impl Service for GiteaService { fn name(&self) -> &'static str { "gitea" } fn image(&self) -> &'static str { "gitea/gitea:latest" } - fn ports(&self) -> Vec { vec!["3000:3000".to_string(), "2222:22".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:3000:3000".to_string(), "2222:22".to_string()] } fn env_vars(&self, _hw: &HardwareInfo, secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); vars.insert("GITEA__database__DB_TYPE".to_string(), "mysql".to_string()); @@ -92,7 +92,7 @@ pub struct RoundcubeService; impl Service for RoundcubeService { fn name(&self) -> &'static str { "roundcube" } fn image(&self) -> &'static str { "roundcube/roundcubemail:latest" } - fn ports(&self) -> Vec { vec!["8090:80".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:8090:80".to_string()] } fn env_vars(&self, _hw: &HardwareInfo, _secrets: &Secrets) -> HashMap { let mut vars = HashMap::new(); vars.insert("ROUNDCUBEMAIL_DB_TYPE".to_string(), "sqlite".to_string()); // Defaulting to sqlite as per memory hints or keeping simple. @@ -114,7 +114,7 @@ pub struct NextcloudService; impl Service for NextcloudService { fn name(&self) -> &'static str { "nextcloud" } fn image(&self) -> &'static str { "lscr.io/linuxserver/nextcloud:latest" } - fn ports(&self) -> Vec { vec!["4443:443".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:4443:443".to_string()] } fn configure(&self, _hw: &HardwareInfo, secrets: &Secrets) -> Result<()> { let config_dir = Path::new("./config/nextcloud"); fs::create_dir_all(config_dir).context("Failed to create nextcloud config dir")?; diff --git a/server_manager/src/services/arr.rs b/server_manager/src/services/arr.rs index 6f8b1fe..4906270 100644 --- a/server_manager/src/services/arr.rs +++ b/server_manager/src/services/arr.rs @@ -9,7 +9,7 @@ macro_rules! define_arr_service { impl Service for $struct_name { fn name(&self) -> &'static str { $name } fn image(&self) -> &'static str { $image } - fn ports(&self) -> Vec { vec![format!("{}:{}", $port, $port)] } + fn ports(&self) -> Vec { vec![format!("127.0.0.1:{}:{}", $port, $port)] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec![ format!("./config/{}:/config", $name), diff --git a/server_manager/src/services/download.rs b/server_manager/src/services/download.rs index 17530dc..36b1970 100644 --- a/server_manager/src/services/download.rs +++ b/server_manager/src/services/download.rs @@ -10,7 +10,7 @@ impl Service for QBittorrentService { fn image(&self) -> &'static str { "lscr.io/linuxserver/qbittorrent:latest" } fn ports(&self) -> Vec { - vec!["8080:8080".to_string(), "6881:6881".to_string(), "6881:6881/udp".to_string()] + vec!["127.0.0.1:8080:8080".to_string(), "6881:6881".to_string(), "6881:6881/udp".to_string()] } fn env_vars(&self, hw: &HardwareInfo, _secrets: &Secrets) -> HashMap { diff --git a/server_manager/src/services/infra.rs b/server_manager/src/services/infra.rs index ced9bd7..cd37587 100644 --- a/server_manager/src/services/infra.rs +++ b/server_manager/src/services/infra.rs @@ -11,9 +11,9 @@ pub struct MariaDBService; impl Service for MariaDBService { fn name(&self) -> &'static str { "mariadb" } fn image(&self) -> &'static str { "lscr.io/linuxserver/mariadb:latest" } - fn ports(&self) -> Vec { vec!["3306:3306".to_string()] } + fn ports(&self) -> Vec { vec![] } // Internal only - fn configure(&self, _hw: &HardwareInfo, secrets: &Secrets) -> Result<()> { + fn configure(&self, hw: &HardwareInfo, secrets: &Secrets) -> Result<()> { let init_dir = Path::new("./config/mariadb/initdb.d"); fs::create_dir_all(init_dir).context("Failed to create mariadb initdb.d")?; @@ -46,6 +46,22 @@ impl Service for MariaDBService { fs::write(init_dir.join("init.sql"), sql).context("Failed to write init.sql")?; + // Optimization: Generate custom.cnf + let (buffer_pool, log_file_size, max_connections) = match hw.profile { + HardwareProfile::High => ("4G", "1G", "500"), + HardwareProfile::Standard => ("1G", "256M", "100"), + HardwareProfile::Low => ("256M", "64M", "50"), + }; + + let custom_cnf = format!(r#"[mysqld] +innodb_buffer_pool_size={} +innodb_log_file_size={} +max_connections={} +"#, buffer_pool, log_file_size, max_connections); + + // Parent dir is ./config/mariadb/ + fs::write(init_dir.parent().unwrap().join("custom.cnf"), custom_cnf).context("Failed to write custom.cnf")?; + Ok(()) } @@ -83,7 +99,7 @@ pub struct RedisService; impl Service for RedisService { fn name(&self) -> &'static str { "redis" } fn image(&self) -> &'static str { "redis:alpine" } - fn ports(&self) -> Vec { vec!["6379:6379".to_string()] } + fn ports(&self) -> Vec { vec![] } // Internal only fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["./config/redis:/data".to_string()] } @@ -149,7 +165,7 @@ pub struct PortainerService; impl Service for PortainerService { fn name(&self) -> &'static str { "portainer" } fn image(&self) -> &'static str { "portainer/portainer-ce:latest" } - fn ports(&self) -> Vec { vec!["9000:9000".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:9000:9000".to_string()] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["/var/run/docker.sock:/var/run/docker.sock".to_string(), "./config/portainer:/data".to_string()] } @@ -160,7 +176,7 @@ pub struct NetdataService; impl Service for NetdataService { fn name(&self) -> &'static str { "netdata" } fn image(&self) -> &'static str { "netdata/netdata" } - fn ports(&self) -> Vec { vec!["19999:19999".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:19999:19999".to_string()] } fn cap_add(&self) -> Vec { vec!["SYS_PTRACE".to_string()] } fn security_opts(&self) -> Vec { vec!["apparmor:unconfined".to_string()] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { @@ -176,7 +192,7 @@ pub struct UptimeKumaService; impl Service for UptimeKumaService { fn name(&self) -> &'static str { "uptime-kuma" } fn image(&self) -> &'static str { "louislam/uptime-kuma:1" } - fn ports(&self) -> Vec { vec!["3001:3001".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:3001:3001".to_string()] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["./config/uptime-kuma:/app/data".to_string()] } diff --git a/server_manager/src/services/media.rs b/server_manager/src/services/media.rs index be31c23..e1f4486 100644 --- a/server_manager/src/services/media.rs +++ b/server_manager/src/services/media.rs @@ -76,7 +76,7 @@ pub struct TautulliService; impl Service for TautulliService { fn name(&self) -> &'static str { "tautulli" } fn image(&self) -> &'static str { "lscr.io/linuxserver/tautulli:latest" } - fn ports(&self) -> Vec { vec!["8181:8181".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:8181:8181".to_string()] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["./config/tautulli:/config".to_string()] } @@ -95,7 +95,7 @@ pub struct OverseerrService; impl Service for OverseerrService { fn name(&self) -> &'static str { "overseerr" } fn image(&self) -> &'static str { "lscr.io/linuxserver/overseerr:latest" } - fn ports(&self) -> Vec { vec!["5055:5055".to_string()] } + fn ports(&self) -> Vec { vec!["127.0.0.1:5055:5055".to_string()] } fn volumes(&self, _hw: &HardwareInfo) -> Vec { vec!["./config/overseerr:/config".to_string()] } diff --git a/server_manager/tests/integration_tests.rs b/server_manager/tests/integration_tests.rs index 596fcb7..baa0a6f 100644 --- a/server_manager/tests/integration_tests.rs +++ b/server_manager/tests/integration_tests.rs @@ -56,6 +56,42 @@ fn test_generate_compose_structure() { assert!(plex_nets.contains(&serde_yaml::Value::from("server_manager_net"))); } +#[test] +fn test_security_bindings() { + // Test that sensitive services are bound to localhost and internal DBs have no ports + let hw = HardwareInfo { + profile: HardwareProfile::Standard, + ram_gb: 8, + cpu_cores: 4, + has_nvidia: false, + has_intel_quicksync: false, + disk_gb: 512, + swap_gb: 2, + user_id: "1000".to_string(), + group_id: "1000".to_string(), + }; + let secrets = Secrets::default(); + + let result = build_compose_structure(&hw, &secrets).unwrap(); + let services = result.get(&serde_yaml::Value::from("services")).unwrap().as_mapping().unwrap(); + + // 1. MariaDB should have NO ports + let mariadb = services.get(&serde_yaml::Value::from("mariadb")).unwrap().as_mapping().unwrap(); + assert!(!mariadb.contains_key(&serde_yaml::Value::from("ports")), "MariaDB should not expose ports"); + + // 2. Sonarr should be bound to 127.0.0.1 + let sonarr = services.get(&serde_yaml::Value::from("sonarr")).unwrap().as_mapping().unwrap(); + let ports = sonarr.get(&serde_yaml::Value::from("ports")).unwrap().as_sequence().unwrap(); + let port_str = ports[0].as_str().unwrap(); + assert!(port_str.starts_with("127.0.0.1:"), "Sonarr port should be bound to localhost: {}", port_str); + + // 3. Plex should still be exposed (host mapping implied or explicit 0.0.0.0) + let plex = services.get(&serde_yaml::Value::from("plex")).unwrap().as_mapping().unwrap(); + let ports = plex.get(&serde_yaml::Value::from("ports")).unwrap().as_sequence().unwrap(); + let port_str = ports[0].as_str().unwrap(); + assert!(!port_str.starts_with("127.0.0.1:"), "Plex port should be exposed: {}", port_str); +} + #[test] fn test_profile_logic_low() { // Test that Low profile disables SpamAssassin in MailService @@ -121,6 +157,8 @@ fn test_resource_generation() { has_intel_quicksync: false, disk_gb: 1000, swap_gb: 4, + user_id: "1000".to_string(), + group_id: "1000".to_string(), }; let secrets = Secrets::default();