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:
308
deploy/OPERATIONS.md
Normal file
308
deploy/OPERATIONS.md
Normal 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
62
deploy/nginx/default.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user