N100 安全事件响应:OpenClaw SSH 暴力破解与 fail2ban 加固
#安全 #fail2ban #frp #N100 #运维
背景
排查 N100(Ubuntu 22.04)开机卡死问题时,发现系统存在持续 11 天的 SSH 暴力破解,来源为内部 Docker 容器(openclaw),同时 frp 内网穿透服务将 SSH 端口暴露在公网,导致来自外部的 SSH 探测。本文记录整个事件的发现、处置和加固过程。
事件时间线
| 时间 | 事件 |
|---|---|
| 7月4日 | N100 正常运行 |
| 7月5日 | openclaw 容器开始向本机 SSH 发起字典攻击 |
| 7月16日 | 攻击最后记录(7月17日重启后 openclaw 未随 boot 自启) |
| 7月17日 | N100 多次开机卡死;排查发现 ext-mount.service 因外接 btrfs 盘状态异常导致内核挂起 |
| 7月17日 | 停止 openclaw 容器;分析 SSH 日志;配置 fail2ban |
| 7月17日 | 实施 openclaw 容器安全加固(非 root 用户、bridge 网络、SSH wrapper、AGENT.md 信任等级) |
| 7月17日 | 卸载 Neo4j(端口 7687 公网暴露);v2rayA 管理界面限制到本地(端口 2017) |
| 7月17日 | 部署宿主机安全巡检脚本,建立端口基线,接入 openclaw heartbeat |
开机卡死根因
根因:外接 btrfs 硬盘在 mount -a 时导致内核挂起
1 | ext-mount.service → mount-retry.sh → /bin/mount -a → btrfs mount → kernel freeze |
-
/etc/fstab挂载了两个外接卷:/nfs(btrfs)和/smb(exfat) -
多次强制断电后 btrfs journal 进入不一致状态,内核 replay 时死锁
-
所有 freeze 均在 Docker “Loading containers: done.” 之后 ~0s 内发生(因 ext-mount.service 与 Docker 并发启动)
临时处置:
1 | # 断开外接硬盘后启动正常 |
长期修复:
-
重新接入前执行
btrfs check --repair(需卸载) -
在
ext-mount.service的ExecStart超时前加保护,避免 kernel 长等待
磁盘依赖关系与服务关机/启动顺序详见 《MyUbuntu 服务关机与启动流程》。
SSH 暴力破解事件
发现过程
排查 N100 启动日志时发现大量来自 127.0.0.1 的 SSH 登录失败,进一步统计:
1 | 总失败次数:1,280,825 次 |
根因:openclaw 容器
-
openclaw 以
host网络模式运行(所有流量显示为127.0.0.1) -
容器拥有本机 SSH 授权密钥(
openclaw@MiLin,ED25519),可无密码登录milin用户 -
推测:openclaw 的某个 agent 任务(可能通过微信或 cron 触发)执行了 SSH 扫描脚本,持续运行未被发现
处置
1 | docker stop openclaw |
从 authorized_keys 清理 openclaw 密钥:
1 | sed -i '/openclaw@/d' ~/.ssh/authorized_keys |
frp 端口安全分析
架构
1 | N100(内网) ──frpc──→ VPS:6000(frps) |
入方向端口评估
| 端口 | 状态 | 用途 | 建议 |
|---|---|---|---|
| 20 | 未监听 | FTP data | ✅ 关闭(云安全组) |
| 21 | 未监听 | FTP control | ✅ 关闭(云安全组) |
| 6000 | 监听 | frps 控制端口 | ⚠️ 保留,token 验证,可考虑 IP 白名单 |
| 6001 | 监听 | frps Dashboard | 🔴 限制访问,含管理凭据,不应公网暴露 |
| 6002 | 监听 | SSH via frp | ⚠️ 保留(远程访问需要),配合 fail2ban 使用 |
| 7000 | 未监听 | frp 默认端口 | ✅ 关闭(云安全组) |
| 4455 | 监听 | WebDAV(nginx) | ⚠️ 确认 nginx 配置了 Basic Auth |
| 25432 | 未监听 | PostgreSQL 变体端口 | ✅ 关闭(云安全组) |
额外发现的公网暴露端口(未在问题范围内但需关注):
-
:2017— v2rayA 管理界面,不应对公网开放 -
:6001— frps Dashboard,有 Web UI 和凭据
立即操作:关闭 frps Dashboard 公网暴露
编辑 /etc/frp/frps.ini,将 dashboard 绑定到 loopback:
1 | dashboard_addr = 127.0.0.1 |
重启 frps 后通过 SSH 本地转发访问:
1 | ssh -L 6001:127.0.0.1:6001 [VPS_IP] |
fail2ban 配置
适用范围
-
N100(MyUbuntuLocal):防护本机 SSH(port 22)
-
VPS(MyServer):防护 VPS 自身 SSH
安装
1 | sudo apt update && sudo apt install -y fail2ban |
配置文件 /etc/fail2ban/jail.local
1 | [DEFAULT] |
应用并验证
1 | sudo cp /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak # 备份原配置 |
查看封禁列表
1 | sudo fail2ban-client status sshd |
手动解封
1 | sudo fail2ban-client set sshd unbanip <IP> |
fail2ban 原理与用法
是什么
本质是一个日志监控 + 自动封 IP 的守护进程。持续扫描系统日志,发现某个 IP 在短时间内触发过多失败登录,就调用 iptables/nftables 把该 IP 封掉。
1 | 攻击者 IP → 尝试 SSH 登录失败 |
核心概念
jail(监狱) — 一个监控规则单元,对应一个服务(SSH、nginx 等)。每个 jail 独立配置:
| 参数 | 含义 | 推荐配置 |
|---|---|---|
filter |
用哪个正则匹配日志 | sshd(内置) |
logpath |
监控哪个日志文件 | /var/log/auth.log |
maxretry |
失败几次触发封禁 | 5 次 |
findtime |
计数的时间窗口 | 600 秒(10 分钟) |
bantime |
封禁持续时长 | 86400 秒(24 小时) |
ignoreip |
永不封禁的白名单 | 127.0.0.1 192.168.0.0/24 |
常用命令
1 | # 查看所有 jail 运行状态 |
局限性与适用边界
| 场景 | fail2ban 是否有效 |
|---|---|
| 外部 IP 暴力破解 SSH 密码 | ✅ 有效,触发阈值后自动封禁 |
| frp 穿透后的外部 SSH 攻击 | ✅ 有效(攻击在目标机上仍显示为外部 IP) |
| 本机进程攻击(如 host 网络容器) | ❌ 无效,来源是 127.0.0.1 被 ignoreip 豁免 |
| 公钥认证暴力尝试 | ❌ 无效,公钥失败通常无日志或日志格式不同 |
| 攻击者不断换 IP | ⚠️ 效果有限,只增加成本 |
最根本的 SSH 防护是在 /etc/ssh/sshd_config 中关掉密码登录:
1 | PasswordAuthentication no |
配合密钥登录 + fail2ban,SSH 暴力破解基本可以忽略。
本次事件中 openclaw 容器以 host 网络模式运行,来源显示为
127.0.0.1,fail2ban 对此完全无效。容器类攻击需从权限隔离层面解决(非 root 用户、非 host 网络、命令审批)。
openclaw 容器安全加固
攻击向量分析
本次事件最可能的触发路径:
1 | 外部来源(微信消息 / 邮件正文) |
这是典型的提示注入攻击(Prompt Injection):攻击者将指令嵌入 AI 会处理的内容(邮件/消息),使 AI 执行未经授权的操作。
修复一:去掉 root 用户(Fix 1)
修改 docker-compose.yml:
1 | user: "1000:1000" # 改为容器内的 node 用户(非 root) |
同时将所有路径从 /root/.openclaw/ 改为 /home/node/.openclaw/:
1 | # 迁移数据目录所有权(须在容器停止后执行) |
效果:容器内进程不再拥有 root 权限,无法直接写入宿主机 root-owned 文件。
修复二:bridge 网络(Fix 2)
修改 docker-compose.yml:
1 | # 移除 network_mode: host |
效果:容器与宿主机网络隔离,来自容器的 SSH 连接在宿主机日志中不再显示为 127.0.0.1,而是容器网关 IP,fail2ban 可以正常统计封禁。
注意:bridge 模式下容器访问宿主机需通过宿主机的 LAN IP(192.168.[HOST].155),SSH config 中 HostName 使用固定局域网 IP 不受影响。
修复四:SSH 命令白名单(Fix 4,方案 A)
在宿主机创建 /etc/openclaw/ssh-wrapper.sh(root 拥有,不可被 openclaw 用户修改):
1 |
|
在 ~/.ssh/authorized_keys 中,将 openclaw 密钥条目替换为:
1 | command="/etc/openclaw/ssh-wrapper.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 [OPENCLAW_PUBKEY] openclaw@[HOST] |
效果:
-
所有通过 openclaw SSH 密钥执行的命令都写入
/var/log/openclaw-ssh.log -
禁止交互式 shell(无法直接
ssh MyUbuntu进入 shell) -
封锁已知扫描工具(ssh-keyscan、nmap、hydra 等)
局限:blocklist 无法穷举所有攻击变体,是深度防御的一层,不是完整防线。
修复附加:工具目录只读挂载
1 | volumes: |
在宿主机 /home/milin/tools/security_check.sh 中提供只读的安全快照工具,openclaw 通过 SSH 调用:
1 | ssh MyUbuntu "bash /home/milin/tools/security_check.sh 2>&1" |
2>&1 确保 stderr 一并返回给 openclaw,避免静默丢失错误信息。
方案 B:Agent 系统提示信任等级防御
在 data/agents/main/AGENT.md 和 data/agents/heartbeat/agent/AGENT.md 中注入安全策略:
1 | ## 信任等级模型 |
局限:模型行为受提示注入影响,无法提供硬性保证。是防御纵深的软层,需配合硬层(wrapper)使用。
加固后的优点与风险
| 项目 | 说明 |
|---|---|
| ✅ 容器非 root | 减少容器逃逸的影响半径 |
| ✅ bridge 网络 | 容器 SSH 连接可被 fail2ban 正常封禁 |
| ✅ SSH 命令日志 | 所有 openclaw SSH 命令全部可审计 |
| ✅ 已知工具封锁 | ssh-keyscan 等工具无法执行 |
| ✅ 工具只读挂载 | 宿主机工具脚本不可被容器篡改 |
| ✅ Agent 信任分级 | 邮件/微信内容被标记为不可信来源 |
| ⚠️ blocklist 不完整 | 攻击者可绕过(如重命名工具、Python 实现) |
| ⚠️ 提示注入仍存在 | 系统提示可被精心构造的输入绕过 |
| ✅ wrapper 在 /etc/openclaw/ | root 拥有,milin 用户无法修改 |
最根本的防护是限制 openclaw 的执行权限:要么使用 exec-approvals.json 要求每次 shell 命令人工审批,要么在 openclaw 加入命令沙箱功能之前保持手动审核高风险操作。
其他端口安全修复
v2rayA 管理界面(:2017)限制到本地
v2rayA 的 Web UI 默认监听 0.0.0.0:2017,对外暴露管理界面。修改配置文件:
1 | # /etc/default/v2raya |
1 | sudo systemctl restart v2raya |
远程访问时通过 SSH 本地转发:
1 | ssh -L 2017:127.0.0.1:2017 [VPS_或_N100] |
Neo4j 卸载(:7687)
Neo4j 占用 862MB 内存,Bolt 端口 7687 监听 0.0.0.0,不应对外暴露且实际未被使用,直接卸载:
1 | sudo systemctl stop neo4j && sudo systemctl disable neo4j |
宿主机安全巡检脚本
设计原则
脚本位于 /home/milin/tools/security_check.sh,只读、无副作用(端口基线文件除外),供 openclaw 通过 SSH 定期调用并分析输出。
调用方式
1 | ssh MyUbuntu "bash /home/milin/tools/security_check.sh 2>&1" |
-
使用
2>&1确保 stderr 也被 openclaw 捕获 -
脚本运行约 1-2 秒,输出约 80-120 行纯文本
-
openclaw 读取完整输出后分析汇总节,有
⚠ ALERT则报告给用户
检查项覆盖
| 检查项 | 内容 |
|---|---|
| SSH 暴力破解 | 全量日志失败统计、Top 10 攻击 IP、内部来源异常识别 |
| fail2ban | 进程状态、历史封禁次数、最近封禁记录 |
| 公网端口扫描 | 宿主机 + 所有 Docker 容器端口,与基线对比检测新增端口 |
| 对外建立连接 | 过滤已知白名单(v2ray、frpc、Ollama),报告未知外部连接 |
| Docker 安全属性 | 容器用户、网络模式(host 告警)、privileged 告警 |
| SSH 授权密钥 | openclaw 密钥是否有 command= 限制 |
| 可疑进程 | 长时间运行的 shell、SSH 客户端进程 |
| 系统资源 | 磁盘超 85% 告警、内存、负载 |
增量端口检测机制
首次运行后将当前所有公网端口保存为基线(~/.security_ports_baseline)。后续运行时:
-
新增端口 →
⚠ ALERT: 新增公网端口 :XXXX(无论端口号是什么) -
已知高危端口(数据库、管理面板)→ 始终
⚠ ALERT,不受基线豁免 -
基线内已知端口 →
✓ 已知公网端口
这样无需维护静态黑白名单,后续新增任何服务都会被自动捕获。
输出结构
1 | ╔══════════════════════════════════════╗ |
openclaw 优先读取末尾"汇总"节,有告警则逐项分析并报告用户,不自行执行修复操作。
后续建议
N100 已完成:
-
command=限制条目(wrapper 位于/etc/openclaw/ssh-wrapper.sh) -
1000:1000)、Fix2(bridge 网络)、Fix4(SSH wrapper) -
AGENT.md 信任等级策略写入
待处理:
-
VPS 安全组:在云控制台关闭 20、21、7000、25432 端口入方向规则
-
frps Dashboard:将
dashboard_addr改为127.0.0.1,不对公网暴露(当前 :6001 仍公网可达) -
VPS PasswordAuthentication:在
/etc/ssh/sshd_config中设置PasswordAuthentication no -
N100 PasswordAuthentication:同上(需 sudo 确认当前值)
-
btrfs 硬盘:
btrfs check --readonly结果无错误,已恢复自动挂载(2026-07-18) -
openclaw 容器恢复自启:已改回
restart: unless-stopped(2026-07-18) -
openclaw exec-approvals:已配置 socket 路径与审批策略(2026-07-18)
-
废弃 webdav 目录清理:
/home/milin/dockerfile/webdav/已删除(2026-07-18)
