diff --git a/src/security/protection/dns_hardener.rs b/src/security/protection/dns_hardener.rs new file mode 100644 index 00000000..925009c0 --- /dev/null +++ b/src/security/protection/dns_hardener.rs @@ -0,0 +1,122 @@ +use anyhow::{Context, Result}; +use tracing::info; + +use crate::security::command_guard::SafeCommand; + +const DNS_CONTAINER: &str = "pragmatismo-dns"; +const COREFILE_PATH: &str = "/opt/gbo/conf/Corefile"; + +/// Corefile template with ACL (anti-amplification) + errors plugin. +/// Zones are passed in at runtime; the forward/catch-all block is always added. +const COREFILE_ZONE_TEMPLATE: &str = r#"{zone}:53 {{ + file /opt/gbo/data/{zone}.zone + bind 0.0.0.0 + acl {{ + allow type ANY net 10.0.0.0/8 127.0.0.0/8 + allow type ANY net {server_ip}/32 + allow type A net 0.0.0.0/0 + allow type AAAA net 0.0.0.0/0 + allow type MX net 0.0.0.0/0 + allow type TXT net 0.0.0.0/0 + allow type NS net 0.0.0.0/0 + allow type SOA net 0.0.0.0/0 + allow type SRV net 0.0.0.0/0 + allow type CNAME net 0.0.0.0/0 + allow type HTTPS net 0.0.0.0/0 + block + }} + cache + errors +}} + +"#; + +const COREFILE_FORWARD: &str = r#". { + forward . 8.8.8.8 1.1.1.1 + cache + errors + log +} +"#; + +pub struct DnsHardener; + +impl DnsHardener { + /// Patch the Corefile inside the DNS container: + /// 1. Add ACL (anti-amplification) to each zone block if missing + /// 2. Add errors plugin to all blocks + /// 3. Reload CoreDNS (SIGHUP) + pub async fn apply(zones: &[&str], server_ip: &str) -> Result { + let mut log = String::new(); + + let original = Self::read_config().await?; + + // If already hardened, skip + if original.contains("acl {") { + log.push_str("Corefile already hardened\n"); + return Ok(log); + } + + let mut patched = String::new(); + for zone in zones { + patched.push_str( + &COREFILE_ZONE_TEMPLATE + .replace("{zone}", zone) + .replace("{server_ip}", server_ip), + ); + } + patched.push_str(COREFILE_FORWARD); + + Self::write_config(&patched).await?; + Self::reload_coredns(&mut log).await?; + + log.push_str("Corefile hardened (ACL + errors) and CoreDNS reloaded\n"); + info!("CoreDNS hardening applied"); + Ok(log) + } + + pub async fn status() -> Result { + let out = Self::lxc_exec(&["coredns", "--version"]).await?; + Ok(out) + } + + async fn read_config() -> Result { + let out = Self::lxc_exec(&["cat", COREFILE_PATH]).await?; + Ok(out) + } + + async fn write_config(content: &str) -> Result<()> { + let host_tmp = "/tmp/gb-corefile"; + std::fs::write(host_tmp, content).context("failed to write Corefile to /tmp")?; + + SafeCommand::new("lxc")? + .arg("file")? + .arg("push")? + .arg(host_tmp)? + .arg(&format!("{DNS_CONTAINER}{COREFILE_PATH}"))? + .execute() + .context("lxc file push Corefile failed")?; + + Ok(()) + } + + async fn reload_coredns(log: &mut String) -> Result<()> { + Self::lxc_exec(&["pkill", "-HUP", "coredns"]) + .await + .context("CoreDNS SIGHUP failed")?; + log.push_str("CoreDNS reloaded\n"); + Ok(()) + } + + async fn lxc_exec(cmd: &[&str]) -> Result { + let mut c = SafeCommand::new("lxc")? + .arg("exec")? + .arg(DNS_CONTAINER)? + .arg("--")?; + for arg in cmd { + c = c.arg(arg)?; + } + let out = c.execute().context("lxc exec failed")?; + Ok(String::from_utf8_lossy(&out.stdout).into_owned()) + } +} diff --git a/src/security/protection/fail2ban.rs b/src/security/protection/fail2ban.rs index 08f5e576..62ead18b 100644 --- a/src/security/protection/fail2ban.rs +++ b/src/security/protection/fail2ban.rs @@ -26,6 +26,37 @@ port = pop3,pop3s,imap,imaps,submission,465,sieve logpath = %(dovecot_log)s "#; +/// jail.local for the proxy container (Caddy) — no sshd, only HTTP flood +const PROXY_JAIL_LOCAL: &str = r#"[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 + +[caddy-http-flood] +enabled = true +filter = caddy +logpath = /opt/gbo/logs/access.log +maxretry = 100 +findtime = 60s +bantime = 1h +action = iptables-multiport[name=caddy, port="80,443", protocol=tcp] +"#; + +/// Disable the debian default sshd jail (proxy has no sshd) +const PROXY_DEFAULTS_DEBIAN: &str = r#"[sshd] +enabled = false +"#; + +/// fail2ban filter for Caddy JSON access log +const CADDY_FILTER: &str = r#"[Definition] +failregex = ^.*"remote_ip":"".*"status":4[0-9][0-9].*$ + ^.*"client_ip":"".*"status":4[0-9][0-9].*$ +ignoreregex = +datepattern = {"ts":\s*%%s +"#; + +const PROXY_CONTAINER: &str = "pragmatismo-proxy"; + pub struct Fail2banManager; impl Fail2banManager { @@ -40,6 +71,68 @@ impl Fail2banManager { Ok(log) } + /// Install and configure fail2ban in the proxy (Caddy) LXC container. + pub async fn apply_proxy() -> Result { + let mut log = String::new(); + + // Install fail2ban inside the proxy container + Self::lxc_exec(PROXY_CONTAINER, &["apt-get", "install", "-y", "--fix-missing", "fail2ban"]) + .await + .context("failed to install fail2ban in proxy container")?; + + // Write caddy filter + std::fs::write("/tmp/gb-caddy-filter.conf", CADDY_FILTER) + .context("failed to write caddy filter to /tmp")?; + SafeCommand::new("lxc")? + .arg("file")? + .arg("push")? + .arg("/tmp/gb-caddy-filter.conf")? + .arg(&format!("{PROXY_CONTAINER}/etc/fail2ban/filter.d/caddy.conf"))? + .execute() + .context("lxc file push caddy filter failed")?; + + // Disable default sshd jail (no sshd in proxy) + std::fs::write("/tmp/gb-proxy-defaults.conf", PROXY_DEFAULTS_DEBIAN) + .context("failed to write proxy defaults to /tmp")?; + SafeCommand::new("lxc")? + .arg("file")? + .arg("push")? + .arg("/tmp/gb-proxy-defaults.conf")? + .arg(&format!("{PROXY_CONTAINER}/etc/fail2ban/jail.d/defaults-debian.conf"))? + .execute() + .context("lxc file push proxy defaults failed")?; + + // Write proxy jail.local + std::fs::write("/tmp/gb-proxy-jail.local", PROXY_JAIL_LOCAL) + .context("failed to write proxy jail.local to /tmp")?; + SafeCommand::new("lxc")? + .arg("file")? + .arg("push")? + .arg("/tmp/gb-proxy-jail.local")? + .arg(&format!("{PROXY_CONTAINER}/etc/fail2ban/jail.local"))? + .execute() + .context("lxc file push proxy jail.local failed")?; + + // Enable and restart + Self::lxc_exec(PROXY_CONTAINER, &["systemctl", "enable", "--now", "fail2ban"]).await?; + Self::lxc_exec(PROXY_CONTAINER, &["systemctl", "restart", "fail2ban"]).await?; + + log.push_str("fail2ban configured in proxy container (caddy-http-flood jail)\n"); + info!("fail2ban proxy jail applied"); + Ok(log) + } + + async fn lxc_exec(container: &str, cmd: &[&str]) -> Result { + let mut c = SafeCommand::new("lxc")? + .arg("exec")? + .arg(container)? + .arg("--")?; + for arg in cmd { + c = c.arg(arg)?; + } + c.execute().context("lxc exec failed") + } + pub async fn status() -> Result { let out = SafeCommand::new("sudo")? .arg("fail2ban-client")? diff --git a/src/security/protection/mod.rs b/src/security/protection/mod.rs index 201b1c30..d58d9f79 100644 --- a/src/security/protection/mod.rs +++ b/src/security/protection/mod.rs @@ -1,6 +1,7 @@ pub mod api; pub mod caddy_hardener; pub mod chkrootkit; +pub mod dns_hardener; pub mod fail2ban; pub mod firewall; pub mod installer; diff --git a/src/security/protection/security_fix.rs b/src/security/protection/security_fix.rs index 59c86153..6c9a1caf 100644 --- a/src/security/protection/security_fix.rs +++ b/src/security/protection/security_fix.rs @@ -2,13 +2,20 @@ use anyhow::Result; use serde::Serialize; use tracing::info; -use super::{caddy_hardener::CaddyHardener, fail2ban::Fail2banManager, firewall::FirewallManager}; +use super::{ + caddy_hardener::CaddyHardener, + dns_hardener::DnsHardener, + fail2ban::Fail2banManager, + firewall::FirewallManager, +}; #[derive(Debug, Serialize)] pub struct SecurityFixReport { pub firewall: StepResult, pub fail2ban: StepResult, + pub fail2ban_proxy: StepResult, pub caddy: StepResult, + pub dns: StepResult, pub success: bool, } @@ -36,18 +43,28 @@ pub async fn run_security_fix() -> SecurityFixReport { info!("Starting security fix: firewall"); let firewall = StepResult::from(FirewallManager::apply().await); - info!("Starting security fix: fail2ban"); + info!("Starting security fix: fail2ban (host/email)"); let fail2ban = StepResult::from(Fail2banManager::apply().await); + info!("Starting security fix: fail2ban proxy (caddy-http-flood)"); + let fail2ban_proxy = StepResult::from(Fail2banManager::apply_proxy().await); + info!("Starting security fix: caddy hardening"); let caddy = StepResult::from(CaddyHardener::apply().await); - let success = firewall.ok && fail2ban.ok && caddy.ok; + info!("Starting security fix: CoreDNS hardening (ACL + errors)"); + let dns = StepResult::from( + DnsHardener::apply(&["pragmatismo.com.br", "ddsites.com.br"], "82.29.59.188").await, + ); + + let success = firewall.ok && fail2ban.ok && fail2ban_proxy.ok && caddy.ok && dns.ok; SecurityFixReport { firewall, fail2ban, + fail2ban_proxy, caddy, + dns, success, } } @@ -56,13 +73,17 @@ pub async fn run_security_fix() -> SecurityFixReport { pub async fn run_security_status() -> SecurityFixReport { let firewall = StepResult::from(FirewallManager::status().await); let fail2ban = StepResult::from(Fail2banManager::status().await); + let fail2ban_proxy = StepResult::from(Fail2banManager::status().await); let caddy = StepResult::from(CaddyHardener::status().await); - let success = firewall.ok && fail2ban.ok && caddy.ok; + let dns = StepResult::from(DnsHardener::status().await); + let success = firewall.ok && fail2ban.ok && caddy.ok && dns.ok; SecurityFixReport { firewall, fail2ban, + fail2ban_proxy, caddy, + dns, success, } }