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
|
||||
"#;
|
||||
|
||||
/// 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;
|
||||
|
||||
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<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> {
|
||||
let out = SafeCommand::new("sudo")?
|
||||
.arg("fail2ban-client")?
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue