Files
MeshSate/deploy/OPERATIONS.md
zhaolei 16f134e328 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
2026-10-28 19:07:11 +08:00

308 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 次/分钟/客户端。降速或让我方调大限流 |