feat: MeshSafe 文件摆渡安全卫士 v1.0 初始提交

核心功能:
- 6 层检测管线: L0 结构分析 → L1 ClamAV → L2 文档深度检测 → L3 关键词匹配 → L4 YARA → L5 PE/二进制
- FastAPI + PostgreSQL + Redis + Docker Compose 架构
- 客户端 HMAC-SHA256 签名鉴权 + 限流 + 幂等性
- 控制台 UI (登录/Dashboard/诊断中心/策略管理/任务列表/测试中心/产品介绍页)
- 三层诊断定时器 (error_scan 30s / db_aggregate 30s / container_check 5min)

并发与稳定性修复:
- 诊断定时器 Redis 时间槽位锁 (多 worker 同一槽位只有一个执行)
- JSONL 存储 fcntl 跨进程排他锁 + 容错解码 + 原子重写
- 移除 SQLite fallback, 统一 PostgreSQL
- API_CLIENTS 脱敏为环境变量, .env 已 .gitignore
This commit is contained in:
2026-10-28 19:07:11 +08:00
commit 16f134e328
113 changed files with 24604 additions and 0 deletions

308
deploy/OPERATIONS.md Normal file
View File

@@ -0,0 +1,308 @@
# MeshSafe 部署操作手册
> 本文档是**给运维/实施工程师直接照着干的**。不讲原理,只讲"改什么、改成什么、敲什么命令"。仓库根目录在服务器上假设为 `/opt/meshsafe`(Windows 上就是 `d:\project\MeshSafe`)。下面统一用 `/opt/meshsafe` 指代。
***
## 第零步:先填这 5 个决策项
**动手改配置之前先把这 5 个值定下来**,不确定的问甲方。 
| # | 决策项 | 填什么 | 怎么查 |
| - | -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------ |
| ① | 服务器 CPU **物理核数** | 一个整数,比如 8 | Linux: `lscpu | grep "^CPU(s)"` 的值 / `lscpu | grep "Socket(s)"` 再除一下;Windows: 任务管理器 → 性能 → CPU |
| ② | 有几个**外部系统**要对接扫描 API | 比如 3 个:采购系统、档案系统、摆渡客户端 | 问甲方"哪些系统会调你家扫描接口" |
| ③ | 预计**峰值并发**(同时在扫的文件数) | 比如 50 | 问甲方"忙的时候一般多少个文件同时扫" |
| ④ | Postgres 密码定什么 | 自己定一个 ≥ 16 位的随机串 | 自己想,不要用仓库默认的 `meshsafe_db_pw_2026` |
| ⑤ | 服务器**内网 IP** | 比如 `192.168.50.200` | `ip addr` / `ipconfig` 查 |
> **例**:某客户服务器 8 核,3 个对接系统,峰值并发 50,Postgres 密码 abc123xyz789qwe,内网 IP 192.168.50.200
***
## 第一步:生成密钥(3 条命令,复制就能跑)
在服务器上(或本地)跑:
```bash
# 1. MASTER_KEY(主密钥,所有 API 客户端密钥从它派生)
python -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())"
# 输出示例:F0CcBAXSk050fCbtf4jYwyaPW8dCkiUnsJZ7H2WPgFk=
# 2. ADMIN_PASSWORD(控制台默认管理员密码,16 位随机)
python -c "import base64, os; print(base64.b64encode(os.urandom(12)).decode())"
# 输出示例:SGVsbG9Xb3JsZDEyMzQ1Njc4
# 3. Postgres 密码(如果不想自己想,也可以生成)
python -c "import base64, os; print(base64.b64encode(os.urandom(16)).decode())"
# 输出示例:YWJjZGVmZ2hpamtsbW5vcHFyc3Q=
```
**生成好后把这 3 个值记下来**。MASTER_KEY 要**离线备份**(让两位管理员各自存一份,密码管理器 / 纸质都行)。
***
## 第二步:逐文件改
### 文件 1:`.env`(项目根目录下)
打开 `/opt/meshsafe/.env`,**改下面几行**:
| 行 | 当前内容 | 改成 |
| -- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| 11 | `MESHSAFE_MASTER_KEY=F0CcBAXSk050fCbtf4jYwyaPW8dCkiUnsJZ7H2WPgFk=` | 第一步生成的 MASTER_KEY |
| 14 | `MESHSAFE_DB_PASSWORD=meshsafe_db_pw_2026` | 第四步的 Postgres 密码 |
| 19 | `MESHSAFE_DB_URL=postgresql+psycopg2://meshsafe:meshsafe_db_pw_2026@postgres:5432/meshsafe` | ⚠️ **密码要和第 14 行一致**。把 URL 里的密码部分(`meshsafe_db_pw_2026`)替换成你实际的 Postgres 密码。完整形式:`postgresql+psycopg2://meshsafe:你的Postgres密码@postgres:5432/meshsafe` |
| 23 | `MESHSAFE_ADMIN_PASSWORD=admin123` | 第一步生成的 ADMIN_PASSWORD |
| 26 | `UVICORN_WORKERS=15` | 第一步的 CPU **物理核数** |
| 27 | `DB_POOL_SIZE=30` | **计算**:物理核数 × 3。8 核填 24,16 核填 45 |
| 28 | `DB_MAX_OVERFLOW=50` | **计算**:物理核数 × 5。8 核填 40,16 核填 80 |
> 上面示例(8 核服务器、密码 abc123xyz789qwe)的 .env 相关行:```
> MESHSAFE_MASTER_KEY=你生成的key
> MESHSAFE_DB_PASSWORD=abc123xyz789qwe
> MESHSAFE_DB_URL=postgresql+psycopg2://meshsafe:abc123xyz789qwe@postgres:5432/meshsafe
> MESHSAFE_ADMIN_PASSWORD=你生成的admin密码
> UVICORN_WORKERS=8
> DB_POOL_SIZE=24
> DB_MAX_OVERFLOW=40
> ```
***
### 文件 2:`docker-compose.yml`(项目根目录下)
打开 `/opt/meshsafe/docker-compose.yml`,**改 1 行**(api 服务的 environment 里,大概第 106 行):
```
当前: - MESHSAFE_API_CLIENTS=test-client:test-secret,duandao-system-a:real-secret
```
**改成**:把"②有几个外部系统"里的每个系统,分配一对 `client_id:client_secret`,逗号隔开。
**给每个对接系统生成 client_secret**:
```bash
python -c "import base64, os; print(base64.b64encode(os.urandom(24)).decode())"
```
**示例**(3 个对接系统):
```yaml
- MESHSAFE_API_CLIENTS=system-caigou:aBc123xyz789...,system-dangoaN:DeF456ghi012...,system-bodao:GhI789jkl345...
```
> **格式说明**:`client_id` 用英文代号(不要中文、不要空格),`client_secret` 是上条命令生成的随机串,一对一对用英文逗号隔开。
> **其他都不要改!** compose 里的 `UVICORN_WORKERS`、`DB_POOL_SIZE`、`DB_MAX_OVERFLOW` 这三行是从 .env 读取的,改 .env 就自动生效了。
***
### 文件 3:`deploy/nginx/default.conf`
打开 `/opt/meshsafe/deploy/nginx/default.conf`,**可选改 IP 白名单**。
**不改也行**(内网光闸场景)。改的话,在每个 `location` 块里加 `allow` 和 `deny` 行,格式:
```nginx
location /console/ {
allow 运维网段/掩码; # 例如 allow 192.168.1.0/24;
deny all; # 其他都拒绝
proxy_pass http://api:8443;
... 其余不动 ...
}
location /api/ {
allow 摆渡客户端固定IP; # 例如 allow 192.168.50.100;
deny all;
proxy_pass http://api:8443;
... 其余不动 ...
}
```
**如果不知道有哪些 IP 要放行,先跳过这步**——等测试通了再回来加。
***
## 第三步:启动
按顺序执行,**每一步看状态对了再走下一步**:
```bash
cd /opt/meshsafe
# 1. 建磁盘目录
mkdir -p data/postgres data/redis data/clamav data/logs data/temp data/diagnostics
# 2. 起基础设施(Postgres + Redis)
docker compose up -d postgres redis
# 3. 等它们 healthy
docker compose ps postgres redis
# 看到 "healthy" 再往下,否则等 10 秒再看
# 4. 起 ClamAV(病毒引擎)
# 注意:如果是光闸内网,ClamAV 签名库要先摆渡到 data/clamav/ 目录
# 首次启动如果 data/clamav 是空的会自动 freshclam(需要外网)
docker compose up -d clamav
# 等 2~5 分钟,因为 ClamAV 启动慢
# 5. 构建并起 API
docker compose up -d --build api
# 6. 初始化数据库(建表 + admin 账号 + 初始策略)
docker compose exec api python scripts/init_db.py
# 应该看到 "=== 初始化完成 ==="
# 7. 起 Nginx(最后)
docker compose up -d nginx
```
**全绿检查**:
```bash
docker compose ps
# 所有 service 状态应该是 Up (healthy) 或 Up
```
***
## 第四步:验证
```bash
# 1. 健康检查
curl http://服务器IP:44380/health
# 应该返回 {"status":"ok", ...}
# 2. 控制台(浏览器打开)
# http://服务器IP:44380/console/login
# 用 .env 里的 MESHSAFE_ADMIN_PASSWORD 登录(首次登录会强制改密码)
```
***
## 第五步:给对接方交付的东西
对 `MESHSAFE_API_CLIENTS` 里配的**每一个** client_id:
| 交付物 | 内容 | 传递方式 |
| ------------------ | ------------------------- | -------------------------------- |
| **client_id** | 例如 `system-caigou` | 邮件/IM 明文 |
| **client_secret** | 对应的那个 secret | **加密传**(企业邮箱加密 / 内网加密聊天,不要明文发微信) |
| **MASTER_KEY** | **不要给!** 只在 MeshSafe 服务端用 | — |
| **API 地址** | `http://服务器IP:44380` | 明文 |
| **Python 签名示例** | 见下面"附件 A" | 明文 |
> **给密钥时要提醒对方**:secret 泄露或人员离职,立即通知你吊销(你从 `MESHSAFE_API_CLIENTS` 里删掉那一对,重启 api 即生效)。
***
## 第六步:日常操作命令速查
```bash
# 日志
docker compose logs -f api --tail 200 # 实时看 api 日志
docker compose logs -f nginx --tail 200 # 看 nginx 访问日志
# 重启
docker compose restart api # 改 .env 后必须重启
docker compose restart nginx # 改 default.conf 后必须重启
# 更新 API 客户端密钥(改完 .env 或 compose 里的 MESHSAFE_API_CLIENTS)
docker compose restart api
# 诊断打包(生成 tar.gz 给研发看)
docker compose exec api python -m app.diagnostic.dumper
# 强制重建 admin 账号(忘了密码时用)
docker compose exec api python -c "
from app.db import SessionLocal
from app.models import ConsoleUser
db = SessionLocal()
db.query(ConsoleUser).filter_by(username='admin').delete()
db.commit()
db.close()
"
docker compose exec api python scripts/init_db.py
# 全停
docker compose down
# 全停并删数据(⚠️ 生产不要乱执行!会清掉所有扫描记录和策略历史)
docker compose down -v
```
***
## 附件 A:给对接方的 Python 签名示例
对方拿到 client_id + client_secret 后,用下面这个代码发扫描请求:
```python
import os, time, hashlib, hmac, base64
import requests
# ====== 对接方填写这三个值 ======
CLIENT_ID = "你拿到的client_id"
CLIENT_SECRET = "你拿到的client_secret"
BASE_URL = "http://MeshSafe服务器IP:44380"
# ===============================
def hkdf_sha256(secret: bytes, salt: str, info: str, length: int = 32) -> bytes:
"""从 client_secret 派生请求签名密钥。不要改这行。"""
prk = hmac.new(salt.encode(), secret, hashlib.sha256).digest()
okm, block, counter = b"", b"", 1
while len(okm) < length:
block = hmac.new(prk, block + info.encode() + bytes([counter]), hashlib.sha256).digest()
okm += block
counter += 1
return okm[:length]
def sign_request(method, path, query=None, body=b""):
ts = str(int(time.time()))
nonce = os.urandom(16).hex()
body_hash = hashlib.sha256(body).hexdigest()
qs = "&".join(f"{k}={v}" for k, v in sorted((query or {}).items()))
signing_string = "\n".join([method.upper(), path, qs, body_hash, ts, nonce, CLIENT_ID])
signing_key = hkdf_sha256(
CLIENT_SECRET.encode(),
CLIENT_ID,
"meshsafe/api/v1/request-signing",
)
sig = hmac.new(signing_key, signing_string.encode(), hashlib.sha256).hexdigest()
return {
"X-MS-Client-Id": CLIENT_ID,
"X-MS-Timestamp": ts,
"X-MS-Nonce": nonce,
"X-MS-Body-SHA256": body_hash,
"X-MS-Signature": sig,
}
# ====== 实际使用 ======
# 场景 1:扫一个文件
with open("待扫描的文件.exe", "rb") as f:
body = f.read()
headers = sign_request(
"POST",
"/api/v1/scans/submit",
query={"channel": "caiGou系统"}, # 可选,标识来源
body=body,
)
r = requests.post(f"{BASE_URL}/api/v1/scans/submit?channel=caiGou系统",
headers=headers, data=body)
print(r.status_code, r.json())
# 期望:200 + {"verdict": "malicious"/"pass", "score": ..., "action": ...}
```
***
## 附件 B:验签失败对照(对接方报错时查这里)
| HTTP 状态 | detail 内容 | 原因 |
| ------- | ----------- | ---------------------------------------------- |
| 401 | 缺少必要的认证请求头 | 5 个 `X-MS-*` 头少了哪个 |
| 401 | 认证失败 | client_id 写错了?secret 错了?我方 MASTER_KEY 换了没通知? |
| 401 | 请求时间戳超出允许偏差 | 你方服务器和 MeshSafe 服务器时间差超过 5 分钟了(NTP 对时) |
| 401 | 重复请求,已拒绝 | 同一个 nonce 用了两次(每次必须生成新 nonce) |
| 429 | 请求过于频繁 | 你方调用超过 120 次/分钟/客户端。降速或让我方调大限流 |

62
deploy/nginx/default.conf Normal file
View File

@@ -0,0 +1,62 @@
# =============================================================================
# MeshSafe Nginx 反向代理(内网 HTTP 明文)
# =============================================================================
#
# 外部端口:44380(compose 映射 44380:80)
# 上游:api:8443(docker 网络内服务名)
# 无 TLS —— 光闸内网场景,外层防护靠光闸本身
#
# 访问路径:
# http://<内网服务器IP>:44380/console/login → 控制台
# http://<内网服务器IP>:44380/api/v1/scans → 扫描 API
# http://<内网服务器IP>:44380/api/v1/health → 健康检查
# =============================================================================
server {
listen 80;
server_name _;
# ---- 根路径 → 自动跳转到控制台登录页(保留原始 host + port)----
location = / {
return 302 $scheme://$http_host/console/login;
}
# 光闸可能传大文件(ISO 镜像 / 大压缩包)
client_max_body_size 500M;
client_body_timeout 600s;
client_header_timeout 60s;
send_timeout 600s;
# ---- 控制台 ----
location /console/ {
proxy_pass http://api:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
}
# ---- 扫描 API ----
location /api/ {
proxy_pass http://api:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_request_buffering off; # 大文件流式转发,不在 Nginx 缓冲
}
# ---- 健康检查(关 access_log 减少日志噪音)----
location = /health {
proxy_pass http://api:8443/api/v1/health;
access_log off;
}
# ---- 静态资源(logo、CSS 等)----
location /static/ {
proxy_pass http://api:8443/static/;
expires 7d;
access_log off;
}
}