security: add CoreDNS ACL hardening and fail2ban proxy jail
Some checks failed
BotServer CI / build (push) Failing after 4m55s

- dns_hardener.rs: apply ACL (anti-amplification) + errors plugin to Corefile via lxc
- fail2ban.rs: add apply_proxy() for caddy-http-flood jail in pragmatismo-proxy container
- security_fix.rs: integrate dns and fail2ban_proxy steps into run_security_fix/status
- mod.rs: export dns_hardener module
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-17 11:18:19 -03:00
parent c340f95da1
commit 7906a9bf32
4 changed files with 241 additions and 4 deletions

View file

@ -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<String> {
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<String> {
let out = Self::lxc_exec(&["coredns", "--version"]).await?;
Ok(out)
}
async fn read_config() -> Result<String> {
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<String> {
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())
}
}

View file

@ -26,6 +26,37 @@ port = pop3,pop3s,imap,imaps,submission,465,sieve
logpath = %(dovecot_log)s 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":"<HOST>".*"status":4[0-9][0-9].*$
^.*"client_ip":"<HOST>".*"status":4[0-9][0-9].*$
ignoreregex =
datepattern = {"ts":\s*%%s
"#;
const PROXY_CONTAINER: &str = "pragmatismo-proxy";
pub struct Fail2banManager; pub struct Fail2banManager;
impl Fail2banManager { impl Fail2banManager {
@ -40,6 +71,68 @@ impl Fail2banManager {
Ok(log) Ok(log)
} }
/// Install and configure fail2ban in the proxy (Caddy) LXC container.
pub async fn apply_proxy() -> Result<String> {
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<std::process::Output> {
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<String> { pub async fn status() -> Result<String> {
let out = SafeCommand::new("sudo")? let out = SafeCommand::new("sudo")?
.arg("fail2ban-client")? .arg("fail2ban-client")?

View file

@ -1,6 +1,7 @@
pub mod api; pub mod api;
pub mod caddy_hardener; pub mod caddy_hardener;
pub mod chkrootkit; pub mod chkrootkit;
pub mod dns_hardener;
pub mod fail2ban; pub mod fail2ban;
pub mod firewall; pub mod firewall;
pub mod installer; pub mod installer;

View file

@ -2,13 +2,20 @@ use anyhow::Result;
use serde::Serialize; use serde::Serialize;
use tracing::info; 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)] #[derive(Debug, Serialize)]
pub struct SecurityFixReport { pub struct SecurityFixReport {
pub firewall: StepResult, pub firewall: StepResult,
pub fail2ban: StepResult, pub fail2ban: StepResult,
pub fail2ban_proxy: StepResult,
pub caddy: StepResult, pub caddy: StepResult,
pub dns: StepResult,
pub success: bool, pub success: bool,
} }
@ -36,18 +43,28 @@ pub async fn run_security_fix() -> SecurityFixReport {
info!("Starting security fix: firewall"); info!("Starting security fix: firewall");
let firewall = StepResult::from(FirewallManager::apply().await); 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); 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"); info!("Starting security fix: caddy hardening");
let caddy = StepResult::from(CaddyHardener::apply().await); 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 { SecurityFixReport {
firewall, firewall,
fail2ban, fail2ban,
fail2ban_proxy,
caddy, caddy,
dns,
success, success,
} }
} }
@ -56,13 +73,17 @@ pub async fn run_security_fix() -> SecurityFixReport {
pub async fn run_security_status() -> SecurityFixReport { pub async fn run_security_status() -> SecurityFixReport {
let firewall = StepResult::from(FirewallManager::status().await); let firewall = StepResult::from(FirewallManager::status().await);
let fail2ban = StepResult::from(Fail2banManager::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 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 { SecurityFixReport {
firewall, firewall,
fail2ban, fail2ban,
fail2ban_proxy,
caddy, caddy,
dns,
success, success,
} }
} }