From 65ad7b488d7d9c03dfffddd2482b004210674238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=BE=E5=A7=90?= Date: Sun, 2 Aug 2026 10:55:15 +0800 Subject: [PATCH] =?UTF-8?q?post:=20=E4=BB=8E=E6=89=8B=E5=8A=A8=E5=B0=81=20?= =?UTF-8?q?IP=20=E5=88=B0=E4=B8=89=E5=B1=82=E8=87=AA=E5=8A=A8=E9=98=B2?= =?UTF-8?q?=E5=BE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/posts/three-layer-defense.md | 190 +++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 content/posts/three-layer-defense.md diff --git a/content/posts/three-layer-defense.md b/content/posts/three-layer-defense.md new file mode 100644 index 0000000..d1efa3a --- /dev/null +++ b/content/posts/three-layer-defense.md @@ -0,0 +1,190 @@ +--- +title: "从手动封 IP 到三层自动防御:VPS 舰队安全闭环" +description: "当爆破扫描成为日常,手动封 IP 撞不过自动化攻击脚本。记录从 fail2ban 到 auto-ban cron 到 Cloudflare WAF 的三层防御闭环,以及 git 历史凭据脱敏实战。" +date: 2026-08-02T11:00:00+08:00 +draft: false +tags: + - 运维 + - 安全 + - Ansible + - UFW + - Cloudflare +--- + +## 背景 + +昨天的[安全加固日记](/posts/fleet-security-day/)写完了 SSL 续签、WAF 规则和 Ansible 初步落地。今天上线 Ansible 巡检后,第一件事就是看看三台机器到底在被怎么打。 + +结果不出所料——**每天都被爆破扫描,只是之前没人系统看过**。 + +## 一、三节点安全现状:数字说话 + +用 Ansible 一条命令查三节点的 SSH 爆破和 Web 扫描情况: + +| 节点 | SSH 爆破次数 | fail2ban 封禁 IP | Web 404 扫描 | 威胁等级 | +|------|:--:|:--:|:--:|:--:| +| 🇦🇺 澳大利亚 | 547 | 138 | 46 | 中 | +| 🇩🇪 法兰克福 | 197 | 54 | 0 | 低 | +| 🇸🇬 新加坡 | 25 | 171 | 64 | 中 | + +新加坡 SSH 爆破最少(25 次),但封禁 IP 最多(171 个)——说明它被大量**不同来源**的 IP 扫过,攻击者换 IP 的频率远高于另外两个节点。澳大利亚的 Web 扫描有人在打 `/phpunit/eval-stdin.php`(PHP 反序列化利用)和 `/xmlrpc.php`(WordPress XML-RPC 攻击),经典自动化扫描套路。 + +**好消息**:全部防住了。SSH 爆破被 fail2ban 拦截,Web 扫描返回 404 没打到真实漏洞。 + +**坏消息**:fail2ban 只封单 IP,攻击者换 IP 就绕过了。手动封?88 个新攻击 IP,手动封到什么时候? + +## 二、从 fail2ban 到三层防御 + +### 第一层:fail2ban(已有,实时) + +fail2ban 监控日志,对 SSH 爆破和 Web 扫描的**单 IP** 临时封禁(通常 1 小时)。这是第一道防线,够快但不够狠——攻击者换个 IP 就回来了。 + +### 第二层:auto-ban.sh(新增,每小时) + +核心思路:**不要封单个 IP,封整个 /24 网段**。同一攻击者大概率来自同一个网段,封网段比封 IP 有效得多。 + +写了一个 shell 脚本,部署到三节点 cron 每小时执行: + +```bash +# 核心逻辑(简化版) +{ + # 提取 SSH 爆破 IP + grep 'Failed password' /var/log/auth.log | extract_ip + # 提取 Web 404 扫描 IP + grep ' 404 ' /var/log/nginx/access.log | extract_ip +} | sort -u | while read ip; do + # 排除白名单(自家 IP + Cloudflare 边缘 IP + 私有网段) + is_whitelisted "$ip" && continue + # 转 /24 网段 + net="${ip%.*}.0/24" + # 跳过已封(state 文件去重) + grep -qxF "$net" "$STATE" && continue + # 永久封禁 + ufw deny from "$net" to any + echo "$net" >> "$STATE" +done +``` + +**关键设计**: + +| 设计点 | 为什么 | +|--------|--------| +| 按 /24 网段封 | 单 IP 封了攻击者换 IP 就绕过,网段封更彻底 | +| state 文件去重 | 避免重复封禁同一个网段,ufw 规则不会膨胀 | +| 排除 Cloudflare IP | CF 边缘 IP 频繁出现在日志里(代理流量),不能封 | +| 排除自家 IP | 四节点互跳频繁,别把自己封了(血泪教训) | +| ufw deny 全端口 | SSH + Web 一起封,不让攻击者换端口回来 | + +**首次执行结果**: + +| 节点 | 新封 /24 网段 | +|------|:--:| +| 法兰克福 | 13 | +| 澳大利亚 | 65 | +| 新加坡 | 91 | +| **合计** | **169** | + +加上前一天手动封的 9 个 AWS/Azure 网段,总计 178 个攻击网段被永久封禁。 + +### 第三层:Cloudflare WAF(常驻) + +CF 层在入口拦截扫描器和恶意路径,不合法的请求根本到不了源站。配合 `renhea.com` 前哨域名(CF 代理隐藏源站 IP),攻击者连真实 IP 都摸不到。 + +### 三层协同 + +``` +攻击者 + │ + ├─ 扫描器/恶意路径 ──→ 第 3 层 CF WAF 拦截(到不了源站) + │ + └─ 直连源站 IP + │ + ├─ SSH 爆破 ──→ 第 1 层 fail2ban 实时封单 IP(1h) + │ │ + │ └─ 第 2 层 auto-ban 每小时封 /24(永久) + │ + └─ Web 扫描 404 ──→ 第 1 层 fail2ban nginx-botsearch 封 IP + │ + └─ 第 2 层 auto-ban 每小时封 /24(永久) +``` + +## 三、Ansible 部署:一条命令推三节点 + +手动 SSH 到三台机器部署脚本太低效。用 Ansible playbook 一键搞定: + +```yaml +- name: Deploy auto-ban script to fleet + hosts: fleet + tasks: + - copy: src=auto-ban.sh dest=/usr/local/bin/ mode=0755 + - shell: /usr/local/bin/auto-ban.sh # 首次立即执行 + - cron: name="auto-ban" minute=30 hour=* # 每小时 :30 +``` + +`ansible-playbook deploy-auto-ban.yml` 一条命令,三节点同时部署脚本 + 执行 + 配 cron。这就是 Ansible 的价值——**把重复操作变成声明式配置**。 + +> 踩坑:Ansible `copy` 模块的相对路径基于 playbook 所在目录,不是 ansible.cfg 目录。用绝对路径最省心。 + +## 四、git 历史凭据脱敏 + +部署完防御机制,回头检查项目仓库——发现自己犯了个低级错误:**代理 UUID 和真实 IP 都提交到 git 历史里了**。 + +### 暴露了什么 + +| 敏感信息 | 位置 | 风险 | +|----------|------|------| +| VLESS 代理 UUID | `config/xray/*.md` | 攻击者可冒充代理节点 | +| 服务器真实 IP | 多个文件 | 绕过 CF 直连源站 | +| 分享链接(含 UUID+路径) | `config/xray/*.md` | 完整代理凭证泄露 | + +### 修复方案 + +NAS 的 git 版本太老不支持 `git filter-branch`,而且仓库只有 4 个 commit——**直接重建历史最干净**: + +```bash +# 1. 备份文件(不含 .git) +cp -r . /tmp/clean && rm -rf /tmp/clean/.git + +# 2. 写好 .gitignore(排除敏感文件) +# config/xray/*.md ← 含 UUID +# docs/WORKFLOW_APPEND.md ← 含真实 IP +# scripts/deploy.sh ← 含真实 IP + +# 3. 删旧历史,重新 init +rm -rf .git && git init && git add -A + +# 4. 验证无敏感信息再提交 +git grep -E "uuid|password|token|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" +git commit -m "feat: 脱敏版" +``` + +### 教训 + +1. **.gitignore 要在第一次 commit 前就写好**,不是事后补 +2. **配置文件和文档要分离**:通用配置入 git,真实凭证(UUID/IP/密码)只存节点本机 +3. **重建历史只适合小仓库**(几个 commit),大仓库必须用 `git filter-branch` 或 BFG + +## 五、踩坑总结 + +| 坑 | 原因 | 解决 | +|----|------|------| +| fail2ban allports 误封自家 IP | 舰队节点互跳触发 SSH 爆破规则 | 四节点 IP 加入 ignoreip 白名单 | +| Ansible copy 找不到文件 | 相对路径基于 playbook 目录 | 用绝对路径 | +| git 凭据泄露 | .gitignore 写晚了 | 重建历史 + 敏感配置不入库 | +| auto-ban 封到 Cloudflare IP | CF 边缘 IP 出现在 nginx 日志 | 脚本排除 CF IP 段 | + +## 总结 + +从昨天手动检查 SSL、手动配 WAF,到今天 Ansible 一键巡检、auto-ban 自动封禁——**运维的本质就是把手动操作变成自动化流程**。 + +三层防御体系现在完整了: + +- **fail2ban**:实时封单 IP,快但不持久 +- **auto-ban cron**:每小时封 /24 网段,持久且自动去重 +- **Cloudflare WAF**:入口拦截,攻击者摸不到源站 + +攻击者用自动化脚本扫描,我们就用自动化脚本防御。这不是军备竞赛——**这是把重复劳动交给机器**。 + +--- + +*本文已脱敏,不含真实 IP、UUID 或凭证。*