# 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 次/分钟/客户端。降速或让我方调大限流 |