核心功能: - 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
14 KiB
MeshSafe 部署操作手册
本文档是给运维/实施工程师直接照着干的。不讲原理,只讲"改什么、改成什么、敲什么命令"。仓库根目录在服务器上假设为
/opt/meshsafe(Windows 上就是d:\project\MeshSafe)。下面统一用/opt/meshsafe指代。
第零步:先填这 5 个决策项
动手改配置之前先把这 5 个值定下来,不确定的问甲方。
| # | 决策项 | 填什么 | 怎么查 |
|---|---|---|---|
| ① | 服务器 CPU 物理核数 | 一个整数,比如 8 | Linux: `lscpu |
| ② | 有几个外部系统要对接扫描 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 条命令,复制就能跑)
在服务器上(或本地)跑:
# 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:
python -c "import base64, os; print(base64.b64encode(os.urandom(24)).decode())"
示例(3 个对接系统):
- 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 行,格式:
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 要放行,先跳过这步——等测试通了再回来加。
第三步:启动
按顺序执行,每一步看状态对了再走下一步:
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
全绿检查:
docker compose ps
# 所有 service 状态应该是 Up (healthy) 或 Up
第四步:验证
# 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 即生效)。
第六步:日常操作命令速查
# 日志
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 后,用下面这个代码发扫描请求:
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 次/分钟/客户端。降速或让我方调大限流 |