Files
RAG/docs/02-deployment-and-security.md
YoVinchen 63279cf213
Some checks failed
verify / verify (push) Has been cancelled
Make deployment truth and remaining graduation gates explicit
Document the runnable Docker workflow, expected one-shot container exits, server-controlled Bailian namespace switch, current earned progress, and the security and evaluation work that still blocks private-data or graduation release claims.

Constraint: Current local Bailian credentials are rejected by the configured workspace

Rejected: Mark the project complete from synthetic metrics | would overstate provider, OCR, authorization, and blind-evaluation readiness

Confidence: high

Scope-risk: narrow

Directive: Keep progress evidence task-based and distinguish synthetic engineering validation from real geological efficacy

Tested: make verify-design; Markdown links; secret scan; diff checks

Not-tested: Final thesis review and live provider authorization
2026-07-13 05:58:39 +08:00

615 lines
25 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.
# Docker 部署、配置与安全设计
| 项目 | 设计值 |
|---|---|
| 部署方式 | Docker Compose 单机 |
| 长期服务 | `web``gateway``api``model-gateway``worker-local``worker-model``db` |
| 一次性服务 | `migrate``upload-init`;成功后正常 `Exited (0)` |
| 对外端口 | 仅 Nginx/Web |
| 密钥 | Docker Secret / Secret Manager |
| 数据库 | PostgreSQL 17 + pgvector 0.8.x实现时固定 digest |
## 1. 部署目标
第一版的“最简单启动”定义为:
```bash
docker compose up -d --build
```
这条命令当前完成数据库健康等待、一次性迁移、上传卷初始化、两个最小权限 Worker、
`model-gateway`、API、入口 Gateway 和 Web 启动。`migrate``upload-init` 成功后显示
`Exited (0)` 是预期终态,不是服务暂停。它不等于“所有数据和模型凭证自动生成”,首次部署
仍必须创建本地 Secret真实模式还必须完成密钥轮换和三模型能力探测。
架构定位是适合毕设、演示和单机内部使用的生产式单节点,不宣称跨主机高可用。互联网生产环境应替换托管数据库、对象存储和集中密钥管理。
## 2. Compose 服务设计
### 2.1 服务列表
| 服务 | 镜像/构建 | 职责 | 网络暴露 |
|---|---|---|---|
| `db` | 固定版本 pgvector 镜像 | 元数据、向量、任务、会话、评测 | 仅内部网络 |
| `migrate` | 与后端同镜像 | 运行 Alembic一次成功退出 | 无 |
| `upload-init` | 与后端同镜像 | 无网络初始化上传卷属主,一次成功退出 | 无网络 |
| `api` | `backend/Dockerfile` | FastAPI、检索、问答、管理 API | 仅 internal data/model 网络 |
| `gateway` | 与后端同镜像 | 固定上游、请求边界、脱敏错误和流式转发 | 仅 internal ingress/data 网络 |
| `model-gateway` | 与后端同镜像 | 唯一读取百炼 Key代理 Embedding/Rerank/Chat | 仅 internal model + egress 网络,无宿主机端口 |
| `worker-local` | 同一后端镜像 | 本地文件验证、解析、切分;有上传卷,无模型网络/Token | 仅 internal data 网络 |
| `worker-model` | 同一后端镜像 | 只处理已审批文本的向量化;无上传卷 | 仅 internal data/model 网络 |
| `web` | `frontend/Dockerfile` | React 静态资源、Nginx 同源入口和 SSE 代理 | `127.0.0.1:8000`;生产部署 HTTPS |
| `ocr-worker` | 可选 profile | PaddleOCR 重型任务 | 无 |
| `prometheus/grafana` | 可选 profile | 本地观测 | 默认不对公网 |
### 2.2 Compose 骨架(设计示意)
```yaml
name: geological-rag
x-runtime-config: &runtime-config
APP_ENV: ${APP_ENV:-production}
APP_NAME: ${APP_NAME:-geological-rag}
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
POSTGRES_DB: ${POSTGRES_DB:-geological_rag}
POSTGRES_USER: ${POSTGRES_APP_USER:-geological_rag_app}
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_app_password
UPLOAD_ROOT: ${UPLOAD_ROOT:-/data/uploads}
MAX_UPLOAD_MB: "${MAX_UPLOAD_MB:-100}"
x-rag-config: &rag-config
DASHSCOPE_API_KEY_FILE: /run/secrets/bailian_api_key
BAILIAN_OPENAI_BASE_URL: "${BAILIAN_OPENAI_BASE_URL:?set BAILIAN_OPENAI_BASE_URL}"
BAILIAN_NATIVE_BASE_URL: "${BAILIAN_NATIVE_BASE_URL:?set BAILIAN_NATIVE_BASE_URL}"
BAILIAN_RERANK_BASE_URL: "${BAILIAN_RERANK_BASE_URL:?set BAILIAN_RERANK_BASE_URL}"
EMBEDDING_MODEL: ${EMBEDDING_MODEL:-text-embedding-v4}
EMBEDDING_DIMENSION: "${EMBEDDING_DIMENSION:-1024}"
RERANK_MODEL: ${RERANK_MODEL:-qwen3-rerank}
LLM_MODEL: ${LLM_MODEL:-deepseek-v4-flash}
CHUNK_TARGET_TOKENS: "${CHUNK_TARGET_TOKENS:-512}"
CHUNK_MAX_TOKENS: "${CHUNK_MAX_TOKENS:-800}"
CHUNK_OVERLAP_TOKENS: "${CHUNK_OVERLAP_TOKENS:-64}"
VECTOR_TOP_K: "${VECTOR_TOP_K:-50}"
RERANK_TOP_N: "${RERANK_TOP_N:-10}"
CONTEXT_TOP_N: "${CONTEXT_TOP_N:-8}"
MAX_CONTEXT_TOKENS: "${MAX_CONTEXT_TOKENS:-10000}"
MODEL_TIMEOUT_SECONDS: "${MODEL_TIMEOUT_SECONDS:-90}"
MODEL_MAX_RETRIES: "${MODEL_MAX_RETRIES:-3}"
MODEL_MAX_CONCURRENCY: "${MODEL_MAX_CONCURRENCY:-4}"
x-model-client-config: &model-client-config
MODEL_GATEWAY_BASE_URL: http://model-gateway:8000
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_api_token
MODEL_GATEWAY_CALLER: api
services:
db:
image: pgvector/pgvector:0.8.2-pg17
environment:
POSTGRES_DB: ${POSTGRES_DB:-geological_rag}
POSTGRES_USER: ${POSTGRES_BOOTSTRAP_USER:-postgres}
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_bootstrap_password
POSTGRES_MIGRATOR_USER: ${POSTGRES_MIGRATOR_USER:-geological_rag_migrator}
POSTGRES_MIGRATOR_PASSWORD_FILE: /run/secrets/postgres_migrator_password
POSTGRES_APP_USER: ${POSTGRES_APP_USER:-geological_rag_app}
POSTGRES_APP_PASSWORD_FILE: /run/secrets/postgres_app_password
secrets:
- postgres_bootstrap_password
- postgres_migrator_password
- postgres_app_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./ops/postgres/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB && test -f $$PGDATA/.rag-bootstrap-complete"]
interval: 5s
timeout: 3s
retries: 20
networks: [data]
restart: unless-stopped
migrate:
build: ./backend
command: ["alembic", "upgrade", "head"]
depends_on:
db:
condition: service_healthy
environment:
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
POSTGRES_DB: ${POSTGRES_DB:-geological_rag}
POSTGRES_USER: ${POSTGRES_MIGRATOR_USER:-geological_rag_migrator}
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_migrator_password
secrets:
- postgres_migrator_password
networks: [data]
restart: "no"
upload-init:
build: ./backend
command: ["python", "-m", "app.tools.init_upload_storage"]
environment:
UPLOAD_ROOT: /data/uploads
user: "0:0"
network_mode: none
volumes:
- uploads_data:/data/uploads
read_only: true
cap_drop: [ALL]
cap_add: [CHOWN, FOWNER, DAC_OVERRIDE]
restart: "no"
model-gateway:
build: ./backend
command: ["uvicorn", "app.model_gateway:app", "--host", "0.0.0.0", "--port", "8000"]
environment:
<<: *rag-config
MODEL_GATEWAY_ALLOWED_TOKEN_FILES: >-
api=/run/secrets/model_gateway_api_token,worker=/run/secrets/model_gateway_worker_token
secrets:
- bailian_api_key
- model_gateway_api_token
- model_gateway_worker_token
networks: [model, egress]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)"]
restart: unless-stopped
api:
build: ./backend
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
depends_on:
migrate:
condition: service_completed_successfully
upload-init:
condition: service_completed_successfully
model-gateway:
condition: service_healthy
environment:
<<: [*runtime-config, *model-client-config]
secrets:
- postgres_app_password
- model_gateway_api_token
volumes:
- uploads_data:/data/uploads
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2)"]
interval: 10s
timeout: 3s
retries: 12
start_period: 10s
networks: [data, model]
restart: unless-stopped
gateway:
build: ./backend
command: ["uvicorn", "app.gateway:app", "--host", "0.0.0.0", "--port", "8000"]
depends_on:
api:
condition: service_healthy
networks: [ingress, data]
restart: unless-stopped
worker-local:
build: ./backend
command: ["python", "-m", "app.worker"]
depends_on:
migrate:
condition: service_completed_successfully
upload-init:
condition: service_completed_successfully
environment:
<<: *runtime-config
WORKER_CAPABILITIES: document_parse
secrets:
- postgres_app_password
volumes:
- uploads_data:/data/uploads
networks: [data]
restart: unless-stopped
worker-model:
build: ./backend
command: ["python", "-m", "app.worker"]
depends_on:
migrate:
condition: service_completed_successfully
model-gateway:
condition: service_healthy
environment:
<<: [*runtime-config, *model-client-config]
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token
MODEL_GATEWAY_CALLER: worker
WORKER_CAPABILITIES: embedding
secrets:
- postgres_app_password
- model_gateway_worker_token
networks: [data, model]
restart: unless-stopped
provider-smoke:
build: ./backend
command: ["python", "-m", "app.tools.provider_smoke"]
profiles: ["tools"]
depends_on:
model-gateway:
condition: service_healthy
environment: *model-client-config
secrets:
- model_gateway_api_token
networks: [model]
restart: "no"
seed-demo:
build: ./backend
command: ["python", "-m", "app.tools.seed_demo"]
profiles: ["tools"]
depends_on:
migrate:
condition: service_completed_successfully
environment:
<<: [*runtime-config, *model-client-config]
MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token
MODEL_GATEWAY_CALLER: worker
secrets:
- postgres_app_password
- model_gateway_worker_token
networks: [data, model]
restart: "no"
web:
build: ./frontend
depends_on:
gateway:
condition: service_healthy
ports:
- "127.0.0.1:8000:8080"
networks: [edge, ingress]
restart: unless-stopped
volumes:
postgres_data:
uploads_data:
networks:
edge:
driver: bridge
ingress:
driver: bridge
internal: true
data:
driver: bridge
internal: true
model:
driver: bridge
internal: true
egress:
driver: bridge
secrets:
postgres_bootstrap_password:
file: ./secrets/postgres_bootstrap_password
postgres_migrator_password:
file: ./secrets/postgres_migrator_password
postgres_app_password:
file: ./secrets/postgres_app_password
model_gateway_api_token:
file: ./secrets/model_gateway_api_token
model_gateway_worker_token:
file: ./secrets/model_gateway_worker_token
bailian_api_key:
file: ./secrets/bailian_api_key
```
实际实现固定数据库镜像 digest。`migrate` 只有一个实例执行;成功应用到 head 后退出码为 0随后在 `docker compose ps -a` 显示 `Exited (0)` 是一次性任务的正常终态不是服务暂停。API 和 Worker 不在启动时并发自动迁移。首次初始化脚本只在 `db` 容器内使用 bootstrap 凭据创建 `vector` 扩展、无超级用户权限的 migrator/app 登录角色和专属 schemamigrator 拥有 schema/DDLapp 仅获运行期所需的 DML/sequence 权限及默认权限API/Worker 永远不挂载 bootstrap 或 migrator Secret。最后一个初始化脚本只有在全部 SQL 事务和授权成功后,才在 `PGDATA` 内用同文件系统 rename 原子写入 `.rag-bootstrap-complete`healthcheck 同时检查 `pg_isready` 与该哨兵,防止临时初始化 server 提前放行迁移。Compose 的 `.env` 仅做变量插值,不会自动注入容器;后端从 `*_FILE` 读取 Secret不把明文密码放进 `.env``DATABASE_URL`
网络分为五类:`web` 位于普通 `edge` 和 internal `ingress`;入口 `gateway` 位于
`ingress + data`API 位于 internal `data + model`;数据库、迁移和 `worker-local` 位于
internal `data``worker-model` 位于 internal `data + model`;只有 `model-gateway` 位于
internal `model + egress``worker-local` 挂载上传卷但没有模型 Token/网络,`worker-model`
内部 Worker Token 但不挂载上传卷;两者的信任边界见
[ADR-0007](adr/0007-split-local-and-model-workers.md)。`provider-smoke` 和真实 `seed-demo` 都不
挂载百炼 Key、不加入 `egress``model-gateway` 不连接数据库、不挂载上传卷、不发布端口。
入口隔离见 [ADR-0004](adr/0004-secretless-web-ingress.md),模型出口隔离见
[ADR-0005](adr/0005-isolate-model-egress.md)。必须注意:普通 `edge` bridge 为 Web 提供宿主机
端口发布时,也给 Web 留有技术上的默认公网出口;生产仍需主机防火墙或出口代理只允许
`model-gateway` 到百炼域名。
### 2.3 后台任务为何不用 Redis
第一版用 PostgreSQL 任务表和租约。领取必须在一个事务中完成选择、fencing token 更新和返回,不能先 SELECT 再 UPDATE
```sql
WITH candidate AS (
SELECT id
FROM background_jobs
WHERE attempt < max_attempts
AND required_capability = ANY(:worker_capabilities)
AND (
(status = 'QUEUED' AND run_after <= now())
OR (status = 'RUNNING' AND lease_until < now())
)
ORDER BY priority DESC, run_after, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE background_jobs AS job
SET status = 'RUNNING',
lease_owner = :worker_id,
lease_token = gen_random_uuid(),
lease_until = now() + :lease_interval,
attempt = attempt + 1,
updated_at = now()
FROM candidate
WHERE job.id = candidate.id
RETURNING job.*;
```
`background_jobs` 还包含 `job_type``required_capability``resource_type/resource_id` 和版本化 payload普通 Worker 只领取解析/向量/评测能力,`ocr-worker` 只领取 OCR 能力。心跳、阶段完成和最终提交都必须同时匹配 `job_id + lease_owner + lease_token`;旧 Worker 即使恢复也无法覆盖新租约结果。捕获到任务失败时必须在同一事务分支:若当前 `attempt < max_attempts`,转回 `QUEUED` 并按指数退避设置 `run_after`;否则直接转 `FAILED`,绝不能留下已耗尽且不可领取的 QUEUED 任务。
普通 Worker 内置每 30 秒运行一次的 maintenance loop并用固定 PostgreSQL advisory lock 保证同一时刻只有一个实例执行 reaper。reaper 只兜底 Worker 崩溃来不及回写的 RUNNING 任务:租约过期且尚有重试次数则原子转回 `QUEUED`,已耗尽则转 `FAILED`。领取、reaper 和最终提交都使用 fencing 条件;测试必须强杀执行中的 Worker分别验证“有剩余次数后可重领”“最后一次崩溃后进入 FAILED”“旧 lease token 不能提交”。为领取路径建立覆盖 `required_capability/status/run_after/lease_until/priority/created_at` 的索引,并监控过期租约与失败率。这样才可证明崩溃恢复,同时避免增加 Redis、Celery 和另一套持久化语义。只有当队列吞吐、调度或跨服务 fan-out 有实测瓶颈时再新增消息系统,并记录 ADR。
## 3. 凭证生命周期
### 3.1 立即轮换
任何已出现在对话中的旧 Key
1. 在百炼北京地域控制台重置或删除;
2. 审查旧 Key 的调用和账单记录;
3. 新建最小权限 Key仅允许本项目所需模型和部署出口 IP
4. 新 Key 写入本机 `secrets/bailian_api_key`,权限设为仅部署用户可读;
5. 不使用命令行参数或会进入 shell history 的写法传递密钥;
6. 运行三模型最小探测;
7. 记录轮换时间和责任人,不记录 Key 值。
百炼文档说明北京业务空间 Key 可配置模型范围和 IP 白名单;生产应使用最小权限,见[API Key 权限](https://help.aliyun.com/zh/model-studio/get-api-key)。
### 3.2 应用读取
配置层支持 `*_FILE`
```python
def read_secret(file_env: str, value_env: str) -> str:
secret_file = os.getenv(file_env)
if secret_file:
return Path(secret_file).read_text(encoding="utf-8").strip()
return os.environ[value_env]
```
生产只允许文件/Secret Manager本地值环境变量仅作为临时兼容。`bailian_api_key` 只挂载到 `model-gateway`。API 与 Worker 分别挂载 `model_gateway_api_token``model_gateway_worker_token`;调用同时使用 Bearer token 和 `X-RAG-Caller`,服务端以常量时间比较并拒绝 token/身份不匹配。应用启动后不打印 Settings 的 Secret 字段,异常对象不得包含请求头。
Secret 或模型端点配置在运行中发生变化时,`model-gateway` 会原子清空旧 provider 与内部
token并进入 `restart_required`;旧、新 token 都不能在旧进程继续调用。Docker 的
`restart: unless-stopped` 不会仅因 unhealthy 自动重启,因此轮换操作必须协调执行
`docker compose restart model-gateway api worker-model`,待 readiness 恢复后再运行 provider
smoke。禁止只替换文件而让旧进程长期携带旧凭据。
### 3.3 Git 防泄漏
仓库已有三层控制:
- `.gitignore` 忽略 `.env``secrets/`、私钥、原始/私有数据和备份;
- `.githooks/pre-commit` 只扫描 staged 文件,发现长密钥格式即阻止提交;
- `.gitea/workflows/secret-scan.yml` 在 push/PR 后复检。
首次克隆执行:
```bash
make setup-hooks
make verify
```
扫描器只输出可疑文件名不回显密钥。它是防线而非保证Code Review 仍需检查配置、测试 fixture、截图、Notebook 输出和文档。
如果密钥进入 Git 历史,先轮换,再评估历史清理;顺序不能反过来。
## 4. 配置契约
### 4.1 非敏感配置
`.env.example` 只保留占位符:
```dotenv
BAILIAN_OPENAI_BASE_URL=https://<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
BAILIAN_NATIVE_BASE_URL=https://<workspace-id>.cn-beijing.maas.aliyuncs.com/api/v1
BAILIAN_RERANK_BASE_URL=https://<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-api/v1
DASHSCOPE_API_KEY_FILE=/run/secrets/bailian_api_key
EMBEDDING_MODEL=text-embedding-v4
EMBEDDING_DIMENSION=1024
RERANK_MODEL=qwen3-rerank
LLM_MODEL=deepseek-v4-flash
MODEL_GATEWAY_BASE_URL=http://model-gateway:8000
MODEL_GATEWAY_TOKEN_FILE=/run/secrets/model_gateway_api_token
MODEL_GATEWAY_CALLER=api
```
启动时 fail fast
- URL 使用 HTTPS
- 三个 URL 主机相同且地域为北京;
- Rerank 路径不是 `compatible-mode/v1`
- Embedding 维度为 schema 支持的 1024
- 生产没有默认密码和通配 CORS
- Secret 文件存在、非空且权限合理;
- 内部模型地址必须精确为 `http://model-gateway:8000`,不能通过配置改成公网或用户控制的 URL
- API/Worker 内部 token 必须不同caller 必须与 token 身份一致;
- `APP_ENV=production` 时关闭开发文档或增加管理员保护。
### 4.2 地域与计费方案
业务空间专属域名只能用所属工作空间的 Key北京、新加坡等地域 Key 不通用Token Plan/Coding Plan 的 Key 和 Base URL 也不能用于后端服务。错误组合通常表现为 401。官方说明见[Base URL 总览](https://help.aliyun.com/zh/model-studio/base-url)。
## 5. 首次部署与模型 Preflight
### 5.1 部署前
```text
1. 轮换 Key
2. 建立 secrets/ 文件(不提交)
3. 配置真实工作空间 URL不提交
4. make setup-hooks
5. make verify
6. docker compose config确认无密钥被渲染到日志
7. docker compose up -d --build
8. 检查 /health/live 和 /health/ready
9. 执行 provider-smoke
10. 导入无敏感演示数据
```
不要在支持工单、截图或 CI 日志中粘贴 `docker compose config` 的完整敏感输出。
### 5.2 三模型最小探测
管理员命令 `provider-smoke` 的预期行为:
1. Embedding一个短文本断言 1024 维;
2. Rerank一个 query、两个 documents、`top_n=1`,断言下标和分数;
3. Chat要求返回极短固定文本关闭思考并限制输出
4. 输出只含能力名、状态、模型、耗时、request ID 和脱敏错误码;
5. 不输出请求头、Key、完整 Base URL 查询参数和文档正文。
`GET {OPENAI_BASE}/models` 可作为辅助清单,但不能证明 Rerank 路径和三个模型权限都正常,最小实际调用才是最终验证。
截至 2026-07-13最近一次真实探测已到达百炼端点但 Embedding、Rerank、Chat 三项均返回 401。改由内部 `model-gateway` 路由后必须重新运行同一探测;已有 401 证明不了模型授权可用。必须核对 Key 所属工作空间、北京地域、端点和计费/模型权限后重新验收。401 不进入自动重试风暴,也不能把 Stage 1 标记完成。
## 6. 网络与接口安全
### 6.1 网络边界
- 只发布 Nginx 端口数据库、API、Worker 不直接暴露公网;
- Nginx 到 API 使用内部网络;
- `data``model` 网络启用 Docker internal 隔离Web 不加入API/Worker 只经 `model-gateway` 访问云服务,自身不加入 `egress`
- 生产入口启用 TLS、HSTS 和安全响应头;
- 出站只允许百炼/OSS 等必要域名;
- 数据库账号按迁移、应用、备份职责分权;
- 容器使用非 root 用户、只读根文件系统(需要写入的 volume 单独挂载);
- Docker socket 不挂入应用容器。
### 6.2 认证和授权
- 管理员、资料维护者、普通用户、研究者 RBAC
- Refresh Token 使用 HttpOnly、Secure、SameSite Cookie
- 每次检索 SQL 都包含授权知识库/文档过滤;
- 权限过滤必须在向量候选生成时生效;
- 文档下载、页图和原文 API 再做对象级权限校验;
- 可选 PostgreSQL Row-Level Security 作为纵深防御,不能代替应用层测试。
### 6.3 上传安全
- 扩展名和真实 MIME 双重验证;
- 流式上传,不把大文件整体放入内存;
- 限制文件大小、页数、压缩比和处理时间;
- 服务端 UUID 文件名,禁止用户路径;
- 拒绝宏、可执行文件和未知类型;
- 一期不允许用户输入任意 URL 抓文档,避免 SSRF
- 可选 ClamAV/云安全扫描;
- 加密 PDF 进入人工处理,不让 Worker 暴力破解。
## 7. RAG 特有安全
### 7.1 Prompt Injection
检索到的文档始终是不可信数据:
- 系统 Prompt 明确忽略文档内命令;
- 上下文使用结构化 `<source>` 包裹并编号;
- 模型无直接数据库、文件或网络工具权限;
- 不根据文档内容修改系统配置或权限;
- 回答引用编号由后端校验;
- 将注入样例纳入安全回归集。
### 7.2 数据出域
向量化、重排和生成都会把相应文本发送到百炼。因此:
- `cloud_processing_allowed=false` 的文档不可进入云模型链;
- 权限和保密字段在排队前检查Worker 再次检查;
- 日志与遥测不附带文档正文;
- 对个人信息和精确敏感坐标执行脱敏或拒绝;
- 若未来需要处理受限资料,必须采用审批后的本地模型部署并新增 ADR不能悄悄切换。
### 7.3 输出安全
- Markdown 清理 HTML 和危险 URL
- 对资源量、储量、投资、施工类问题显示专业免责声明;
- 生成内容不自动写回知识库;
- 用户反馈与更正进入人工审核,不直接成为新事实;
- 不让模型生成不存在的下载链接和文件路径。
## 8. 健康检查与可观测性
### 8.1 健康端点
- `/health/live`:进程事件循环可用,不检查外部模型;
- API `/health/ready`:当前验证数据库可用;后续再纳入迁移版本和存储路径对账;
- `model-gateway /health/ready`:只验证本地 Key/token/端点配置可加载且未漂移,不产生计费云调用;
- `/admin/providers/health`:按需调用三个模型,结果短期缓存;
- `/admin/index/health``documents.active_version_id`、searchable 切片投影、非空向量、文本哈希和 embedding profile 一致性。
百炼短时波动不应让编排器不断重启健康 API 容器。真实三能力是否可用只能由显式 provider smoke 证明,不能从 `model-gateway` readiness 推断。
### 8.2 日志脱敏
结构化日志保留 `trace_id`、阶段、状态、模型、Token、耗时、request ID。下列字段永久屏蔽
```text
authorization
api_key
cookie
password
secret
raw_document
embedding vector values
```
Provider 异常可能把请求信息嵌入消息,记录前使用 allowlist 选择安全字段,而不是对整段异常做不可靠字符串替换。
## 9. 备份、恢复和删除
### 9.1 备份
- PostgreSQL定期 `pg_dump`;生产可使用 WAL/PITR
- 原始文件:开发 volume 备份,生产 OSS 版本控制;
- 评测产物:配置、指标和脱敏预测与 Git commit 关联;
- Secret 不进入普通数据备份,使用密钥管理系统单独恢复流程;
- 每次正式演示前生成已验证的恢复点。
### 9.2 恢复演练
至少完成一次:
1. 在空白 Compose project 中启动数据库;
2. 恢复数据库和允许恢复的文件;
3. 校验 schema revision
4. 比较文档数、切片数、向量维度、哈希抽样;
5. 运行 20 道固定冒烟问题;
6. 保存恢复耗时和缺口。
只有生成备份文件而未恢复不算验收完成。
### 9.3 删除
用户删除文档时:
1. 事务内清空 `documents.active_version_id` 并令对应切片 `searchable=false`,阻断检索;
2. 创建物理删除任务;
3. 清理切片、文件和衍生缓存;
4. 保留最小审计记录和不可逆哈希,不保留原文;
5. 对备份中的保留周期和销毁策略做文档说明。
## 10. 上云演进
| 单机组件 | 生产替代 | 触发条件 |
|---|---|---|
| Docker volume | 阿里云 OSS | 多主机、备份和生命周期需求 |
| 单机 PostgreSQL | RDS PostgreSQL + pgvector | 高可用、PITR、容量需求 |
| DB 任务队列 | Redis/Celery 或云消息服务 | 实测队列吞吐/调度瓶颈 |
| pgvector | Qdrant/专用向量服务 | 百万级、高 QPS、复杂过滤瓶颈 |
| 本地日志 | 集中日志/指标 | 多实例和审计需求 |
任何演进都需要迁移计划、回滚方案、数据对账和 ADR不以“更云原生”为唯一理由。