security: add CoreDNS ACL hardening and fail2ban proxy jail
Some checks failed
BotServer CI / build (push) Failing after 4m55s
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:
parent
c340f95da1
commit
7906a9bf32
4 changed files with 241 additions and 4 deletions
122
src/security/protection/dns_hardener.rs
Normal file
122
src/security/protection/dns_hardener.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")?
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue