适用场景:家庭/小型办公/边缘集群,想把「被动挨打」变「主动防御」
三层体系:Fail2Ban(本地日志封禁) → CrowdSec(众包威胁情报+决策引擎) → Wazuh(HIDS/日志关联/合规审计)
部署形态:Docker Compose 编排、统一仪表盘、告警路由、自动响应、误封白名单
—
0. 为什么要三层?(单层都有盲区)
| 层级 | 角色 | 擅长 | 盲区 | 互补方式 |
| Fail2Ban | 本地规则引擎 | 解析任意日志格式、正则匹配、立即 iptables/nftables 封禁 | 无威胁情报、无跨主机关联、规则手写维护 | 兜底:应用层异常(如 404 扫描、API 暴力破解)第一时间封 |
| CrowdSec | 众包决策引擎 | 社区共享恶意 IP、现成场景、多 Bouncer(防火墙/HTTP/CDN)、自动拉取决策 | 依赖社区规则覆盖面、本地日志解析需配置 | 增强:把全网已知恶意 IP 同步到本地防火墙 |
| Wazuh | HIDS + SIEM | 文件完整性监控 (FIM)、Rootkit 检测、日志关联、MITRE ATT&CK 映射、合规报告 | 实时封禁能力弱、部署相对重 | 深度:事后溯源、横向移动检测、合规审计、统一仪表盘 |
一句话:Fail2Ban 守门、CrowdSec 看黑名单、Wazuh 装监控摄像头+审计日志。
—
1. 架构总览(Docker Compose 编排)
+------------------+ +------------------+ +------------------+
| Fail2Ban | | CrowdSec | | Wazuh |
| (容器) | | (容器) | | (Manager+Indexer|
| | | | | +Dashboard) |
| - sshd filter | | - acquis.yaml | | |
| - nginx filter | | - parsers/ | | - FIM |
| - custom filter | | - scenarios/ | | - Rootcheck |
| - banaction | | - bouncers/ | | - Log Analysis |
| → nftables | | → nftables | | - Compliance |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
| 共享网络 / 日志卷 | |
+-----------+-----------+-----------+-----------+
| |
+------v------+ +------v------+
| nftables | | Elasticsearch|
| (宿主机) | | (Indexer) |
+-------------+ +--------------+
—
2. 宿主机准备(所有节点跑一次)
# 1. 内核模块
modprobe nf_tables nf_conntrack nf_defrag_ipv4 nf_defrag_ipv6
echo -e "nf_tables\nnf_conntrack" > /etc/modules-load.d/firewall.conf
# 2. 目录
mkdir -p /opt/security/{fail2ban,crowdsec,wazuh}/{config,logs,data}
# 3. 宿主机 nftables 基础链(供 Bouncer 写入)
nft add table inet crowdsec
nft add chain inet crowdsec crowdsec-chain { type filter hook input priority 0 \; policy accept \; }
nft add chain inet crowdsec crowdsec6-chain { type filter hook input priority 0 \; policy accept \; }
—
3. Docker Compose 完整编排(/opt/security/docker-compose.yml)
version: "3.8"
networks:
security:
driver: bridge
volumes:
crowdsec-config:
crowdsec-data:
wazuh-indexer-data:
wazuh-manager-data:
wazuh-dashboard-data:
services:
# ==================== Fail2Ban ====================
fail2ban:
image: crazymax/fail2ban:latest
container_name: fail2ban
restart: unless-stopped
network_mode: host # 必须 host 网络才能操作宿主机 nftables
cap_add:
- NET_ADMIN
- NET_RAW
environment:
- TZ=Asia/Shanghai
- F2B_LOG_LEVEL=INFO
- F2B_DB_PURGE_AGE=30d
volumes:
- /opt/security/fail2ban/config:/etc/fail2ban
- /opt/security/fail2ban/logs:/var/log/fail2ban
- /var/log:/host/var/log:ro # 读取宿主机日志
- /run/systemd/journal:/run/systemd/journal:ro # systemd 日志
# 配置文件见下文 3.1
# ==================== CrowdSec ====================
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
- SYS_PTRACE
environment:
- TZ=Asia/Shanghai
- COLLECTIONS=crowdsecurity/sshd crowdsecurity/nginx crowdsecurity/iptables crowdsecurity/base-http-scenarios crowdsecurity/http-cve
- PARSERS=crowdsecurity/sshd-logs crowdsecurity/nginx-logs crowdsecurity/syslog-logs
- POSTOVERFLOWS=crowdsecurity/http-backdoors-attempts crowdsecurity/http-sensitive-files crowdsecurity/http-probing
volumes:
- /opt/security/crowdsec/config:/etc/crowdsec
- /opt/security/crowdsec/data:/var/lib/crowdsec/data
- /var/log:/host/var/log:ro
- /run/systemd/journal:/run/systemd/journal:ro
depends_on:
- fail2ban
# CrowdSec Bouncer:nftables(落地封禁)
crowdsec-bouncer-nftables:
image: crowdsecurity/crowdsec-bouncer-nftables:latest
container_name: crowdsec-bouncer-nftables
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
environment:
- CROWDSEC_AGENT_URL=http://localhost:8080
- CROWDSEC_BOUNCER_API_KEY=${BOUNCER_API_KEY} # 见下文生成
- CROWDSEC_UPDATE_FREQUENCY=10s
- CROWDSEC_NFTABLES_IPV4_TABLE=inet
- CROWDSEC_NFTABLES_IPV4_CHAIN=crowdsec-chain
- CROWDSEC_NFTABLES_IPV6_TABLE=inet
- CROWDSEC_NFTABLES_IPV6_CHAIN=crowdsec6-chain
depends_on:
- crowdsec
# ==================== Wazuh Manager ====================
wazuh-manager:
image: wazuh/wazuh-manager:4.9.0
container_name: wazuh-manager
restart: unless-stopped
networks:
- security
ports:
- "1514:1514/udp" # Agent 通信
- "1515:1515" # Enrollment
- "55000:55000" # API
environment:
- TZ=Asia/Shanghai
- WAZUH_MANAGER_NODE_NAME=manager-1
- WAZUH_MANAGER_CLUSTER_NODES=manager-1
- WAZUH_MANAGER_CLUSTER_KEY=ChangeMeClusterKey
- WAZUH_REGISTRATION_PASSWORD=ChangeMeRegistrationPwd
- WAZUH_API_USER=wazuh
- WAZUH_API_PASSWORD=ChangeMeApiPwd
volumes:
- /opt/security/wazuh/config:/wazuh-config-mount
- /opt/security/wazuh/data:/var/ossec/data
- /opt/security/wazuh/logs:/var/ossec/logs
- /var/log:/host/var/log:ro
configs:
- source: wazuh-manager-config
target: /wazuh-config-mount/ossec.conf
# ==================== Wazuh Indexer (Elasticsearch) ====================
wazuh-indexer:
image: wazuh/wazuh-indexer:4.9.0
container_name: wazuh-indexer
restart: unless-stopped
networks:
- security
environment:
- TZ=Asia/Shanghai
- OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g
- WAZUH_INDEXER_NODE_NAME=indexer-1
- WAZUH_INDEXER_CLUSTER_NODES=indexer-1
- WAZUH_INDEXER_CLUSTER_KEY=ChangeMeClusterKey
- WAZUH_INDEXER_PASSWORD=ChangeMeIndexerPwd
volumes:
- /opt/security/wazuh/indexer-data:/var/lib/wazuh-indexer
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
# ==================== Wazuh Dashboard ====================
wazuh-dashboard:
image: wazuh/wazuh-dashboard:4.9.0
container_name: wazuh-dashboard
restart: unless-stopped
networks:
- security
ports:
- "5601:5601"
environment:
- TZ=Asia/Shanghai
- WAZUH_DASHBOARD_INDEXER_URL=https://wazuh-indexer:9200
- WAZUH_DASHBOARD_MANAGER_URL=https://wazuh-manager:55000
- WAZUH_DASHBOARD_PASSWORD=ChangeMeDashboardPwd
volumes:
- /opt/security/wazuh/dashboard-data:/usr/share/wazuh-dashboard/data
depends_on:
- wazuh-manager
- wazuh-indexer
configs:
wazuh-manager-config:
file: ./wazuh-manager/ossec.conf
—
4. 关键配置文件(放在对应卷目录)
4.1 Fail2Ban:/opt/security/fail2ban/config/jail.d/custom.conf
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports
backend = systemd
findtime = 600
maxretry = 3
bantime = 86400
ignoreip = 127.0.0.1/8 ::1 192.168.10.0/24 10.10.10.0/24 # 管理网段白名单
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /host/var/log/auth.log
maxretry = 3
bantime = 86400
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /host/var/log/nginx/error.log
maxretry = 3
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
logpath = /host/var/log/nginx/access.log
maxretry = 2
bantime = 604800
[nginx-badbots]
enabled = true
filter = nginx-badbots
logpath = /host/var/log/nginx/access.log
maxretry = 2
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
banaction = nftables-allports
bantime = 604800
findtime = 86400
maxretry = 5
自定义 filter 放 /opt/security/fail2ban/config/filter.d/,如 nginx-waf.conf、api-abuse.conf。
4.2 CrowdSec:/opt/security/crowdsec/config/acquis.yaml(关键:采集宿主机日志)
filenames:
- /host/var/log/auth.log
- /host/var/log/nginx/access.log
- /host/var/log/nginx/error.log
- /host/var/log/syslog
- /host/var/log/kern.log
journalctl:
- _SYSTEMD_UNIT=ssh.service
- _SYSTEMD_UNIT=nginx.service
- _SYSTEMD_UNIT=docker.service
安装完后进容器跑:cscli collections install crowdsecurity/sshd crowdsecurity/nginx crowdsecurity/base-http-scenarios
cscli parsers install crowdsecurity/sshd-logs crowdsecurity/nginx-logs
4.3 CrowdSec Bouncer API Key 生成
# 在 crowdsec 容器内
docker exec -it crowdsec cscli bouncers add nftables-bouncer
# 输出 API Key,写入 .env 或直接填入 docker-compose.yml 的 BOUNCER_API_KEY
4.4 Wazuh Manager:/opt/security/wazuh/manager/ossec.conf(核心片段)
<ossec_config>
<global>
<json_output>yes</json_output>
<alerts_log>yes</alerts_log>
<logall>no</logall>
<email_notification>no</email_notification>
</global>
<rules>
<include>ruleset/rules/*.xml</include>
<decoder_dir>ruleset/decoders</decoder_dir>
<rule_dir>ruleset/rules</rule_dir>
<list>etc/lists/audit-keys</list>
<list>etc/lists/amazon/aws-eventnames</list>
</rules>
<syscheck>
<disabled>no</disabled>
<frequency>43200</frequency>
<scan_on_start>yes</scan_on_start>
<directories check_all="yes">/etc,/usr/bin,/usr/sbin,/bin,/sbin,/opt</directories>
<ignore>/etc/mtab</ignore>
<ignore>/etc/hosts.deny</ignore>
<ignore>/etc/mail/statistics</ignore>
<ignore>/etc/random-seed</ignore>
<ignore>/etc/adjtime</ignore>
<ignore>/etc/ld.so.cache</ignore>
<nodiff>/etc/ssl/private</nodiff>
<windows_registry>HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run</windows_registry>
<windows_registry>HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce</windows_registry>
<registry_ignore>HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run\WindowsDefender</registry_ignore>
<registry_ignore>HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce\WindowsDefender</registry_ignore>
<sync>yes</sync>
<realtime>yes</realtime>
<report_changes>yes</report_changes>
<windows_audit_interval>3600</windows_audit_interval>
<process_check>yes</process_check>
<directories realtime="yes" check_all="yes">/etc,/usr/bin,/usr/sbin</directories>
</syscheck>
<rootcheck>
<disabled>no</disabled>
<check_dev>yes</check_dev>
<check_files>yes</check_files>
<check_if>yes</check_if>
<check_pids>yes</check_pids>
<check_ports>yes</check_ports>
<check_unixaudit>yes</check_unixaudit>
<readall>yes</readall>
</rootcheck>
<localfile>
<log_format>syslog</log_format>
<location>/host/var/log/auth.log</location>
</localfile>
<localfile>
<log_format>syslog</log_format>
<location>/host/var/log/nginx/access.log</location>
</localfile>
<localfile>
<log_format>syslog</log_format>
<location>/host/var/log/nginx/error.log</location>
</localfile>
<localfile>
<log_format>syslog</log_format>
<location>/host/var/log/syslog</location>
</localfile>
<localfile>
<log_format>json</log_format>
<location>/var/ossec/logs/active-responses.log</location>
</localfile>
<wodle name="command">
<disabled>no</disabled>
<tag>Wazuh-Command</tag>
<command>/bin/bash /var/ossec/active-response/bin/firewall-drop.sh</command>
<interval>0</interval>
<ignore_output>no</ignore_output>
<run_on_start>no</run_on_start>
</wodle>
<active_response>
<disabled>no</disabled>
<command>firewall-drop</command>
<location>local</location>
<rules_id>100200, 100201, 5710, 5712, 5714, 5716, 5720, 5722, 5724, 5726, 5728, 5730</rules_id>
<timeout>600</timeout>
<repeated_offenders>30,60,120,240,480,960,1920,3840,7680</repeated_offenders>
</active_response>
</ossec_config>
—
5. 部署与验证
cd /opt/security
docker compose up -d
# 1. Fail2Ban
docker logs -f fail2ban
fail2ban-client status sshd
# 2. CrowdSec
docker exec -it crowdsec cscli decisions list
docker exec -it crowdsec cscli hub list
docker exec -it crowdsec cscli metrics
# 3. Bouncer 同步
nft list table inet crowdsec
# 4. Wazuh
# 浏览器打开 https://<IP>:5601
# 用户: wazuh / 密码: ChangeMeDashboardPwd
# 首次登录 → 添加 Agent → 复制注册命令 → 在被监控主机跑
—
6. Wazuh Agent 部署(被监控的每台主机)
# Ubuntu/Debian
curl -sO https://packages.wazuh.com/4.9/wazuh-agent_4.9.0-1_amd64.deb
sudo WAZUH_MANAGER=<MANAGER_IP> WAZUH_REGISTRATION_PASSWORD=ChangeMeRegistrationPwd dpkg -i wazuh-agent_4.9.0-1_amd64.deb
sudo systemctl enable --now wazuh-agent
# 验证
sudo /var/ossec/bin/agent_control -l
自动注册:Manager 配置 WAZUH_REGISTRATION_PASSWORD,Agent 安装时传同一密码即可自动入库。
—
7. 告警路由(统一推送到 Telegram/钉钉/企微/邮件)
7.1 CrowdSec → Webhook → Telegram
# /opt/security/crowdsec/config/notifications/telegram.yaml
type: http
name: telegram
format: |
{
"chat_id": "<CHAT_ID>",
"text": "🚨 CrowdSec 告警\n决策: {{.Decision.Type}} {{.Decision.Value}}\n场景: {{.Decision.Scenario}}\n来源: {{.Decision.Source}}\n时间: {{.Decision.CreatedAt}}",
"parse_mode": "HTML"
}
url: "https://api.telegram.org/bot<BOT_TOKEN>/sendMessage"
method: POST
headers:
Content-Type: "application/json"
docker exec -it crowdsec cscli notifications install /etc/crowdsec/notifications/telegram.yaml
7.2 Wazuh → 集成模块(原生支持)
<!-- ossec.conf 中添加 -->
<integration>
<name>telegram</name>
<hook_url>https://api.telegram.org/bot<BOT_TOKEN>/sendMessage</hook_url>
<chat_id><CHAT_ID></chat_id>
<level>7</level>
<group>syscheck,rootcheck,authentication_failed</group>
<alert_format>json</alert_format>
</integration>
也可走 Elasticsearch Watcher / Grafana Alerting 统一做二次加工。
—
8. 误封白名单 & 自动解封策略
| 场景 | 处理方式 |
| 管理员 IP 被误封 | CrowdSec:cscli decisions delete -i <IP>;Fail2Ban:fail2ban-client set sshd unbanip <IP>;Wazuh:Dashboard → Active Response → Delete |
| 固定白名单 | 三层都加 ignoreip / whitelist / agentless_excluded_ips |
| 自动解封 | Fail2Ban bantime / CrowdSec remediation 过期自动删 / Wazuh timeout 到期自动解除 |
| 持久化决策 | CrowdSec cscli decisions add --ip <IP> --type ban --duration -1(永久) |
—
9. 统一仪表盘(Grafana 聚合)
# 数据源
# 1. CrowdSec:Prometheus metrics (crowdsec:6060/metrics) → Grafana Dashboard ID: 15753
# 2. Fail2Ban:node_exporter + fail2ban_exporter (https://github.com/fail2ban/fail2ban-exporter) → Dashboard ID: 12034
# 3. Wazuh:原生 Dashboard (5601) 或导出 Elasticsearch 到 Grafana
—
10. 维护清单
| 频率 | 动作 |
| 每天 | cscli decisions list、fail2ban-client status、Wazuh Dashboard 告警巡检 |
| 每周 | cscli hub update && cscli hub upgrade、检查 nft list table inet crowdsec 规则数、Wazuh Agent 在线状态 |
| 每月 | Fail2Ban 规则审查(新增日志源)、CrowdSec 场景/解析器更新、Wazuh 规则集更新 (/var/ossec/bin/wazuh-update-rules) |
| 每季度 | 白名单复核、告警阈值调优、规则误报率统计、备份 Wazuh Indexer 快照 |
—
11. 常见坑 & 避坑指南
| 现象 | 原因 | 修正 |
| CrowdSec 不封禁 | Bouncer 未运行 / API Key 错误 / acquis.yaml 路径不对 | docker logs crowdsec-bouncer-nftables、检查 cscli bouncers list |
| Fail2Ban 封不住 IPv6 | banaction 只配了 nftables-multiport(仅 v4) | 加 nftables-allports、确认 nft list table inet crowdsec 有 v6 链 |
| Wazuh Agent 连不上 Manager | 1514/1515 端口未通 / 注册密码错 / 证书不信任 | tcpdump -i any port 1514、重新注册 agent-auth |
| 规则过多导致 nftables 性能下降 | 决策数 > 10 万 | CrowdSec 设置 max_decisions_per_bouncer、定期 cscli decisions prune |
| 磁盘被 Wazuh Indexer 撑爆 | 无 ILM 策略、副本数过多 | 配置 Index Lifecycle Management:热/温/冷/删除、副本 1 |
| 误封导致服务不可用 | 白名单未生效 / 规则过激 | 分级告警:L1 只记录、L2 短时封、L3 永久封;核心服务 IP 强制白名单 |
—
12. 资源 & 参考
—
(完)