Files
SnapOtter/apps/docs/zh-CN/guide/security.md
T
SnapOtterandGitHub 5f21588f6c chore: prepare the 2.2.0 release (#660)
Bumps every version surface to 2.2.0, fixes a latent version-coupling bug in the
OCR runtime tests, and stops an absent GPU runner from silently stalling a
release.

Version surfaces: scripts/sync-version.sh covers the 11 workspaces, APP_VERSION,
and the docs release commands across all locales. Root package.json plus the
three surfaces the script never reaches are done by hand: the DOCKERHUB.md banner
and tag table, the docker-tags.md pinning table in 21 locales, and the example
runtimeVersion in tools/image/ocr.md in 21 locales. The release-notes archive step
is deliberately not pre-run, so the notes text stays editable until the release.

Latent bug: runtime-state rejects any runtime whose compatibility.snapotterVersion
is not exactly APP_VERSION, and five fixtures pinned the literal 2.1.0. Since
semantic-release rewrites APP_VERSION on every release, the first PR after any
bump would have gone red for a reason nobody would trace to the release. The
fixtures now derive from APP_VERSION.

GPU runner: sign-ocr-index needs verify-ocr-nvidia on self-hosted hardware, and
the gated manifest job needs ai-bundles, so a missing runner queued instead of
failing and produced no image tags. preflight-gpu-runner claims the same labels
with no dependencies, so it is scheduled first and validates the GPU before the
90-minute build. An API preflight is impossible because listing self-hosted
runners needs Administration:read, which GITHUB_TOKEN cannot hold, so RELEASE.md
carries the maintainer-side check.
2026-07-27 22:09:31 +08:00

268 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.
---
description: "SnapOtter 的安全加固指南。涵盖容器安全、网络隔离、Docker 密钥、Kubernetes 部署和合规产物。"
i18n_source_hash: 9ff337fa0417
i18n_provenance: machine
i18n_output_hash: 5cdae11497f9
i18n_hash_version: 2
---
# 安全与加固 {#security-hardening}
SnapOtter 完全在你自己的基础设施上处理文件。它默认发送匿名、不含内容的产品分析和崩溃报告,以帮助改进项目。它绝不会发送你的文件、文件名、文件内容、OCR 输出、图像元数据或文档文本。可选的反馈仅在用户提交后才会发送,仅在分析启用时发送,且联系字段仅在获得明确的联系同意时才包含在内。管理员可以在 Settings > System > Privacy 下一键关闭分析和反馈采集,无需重新构建。文件处理始终留在你的容器内部。
容器以专用的非 root 用户(`snapotter`)运行,除最低必需集之外的所有 Linux 权能都被丢弃。完整的漏洞披露政策和安全架构,请参阅 GitHub 上的 [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md)。
## 容器硬化 {#container-hardening}
规范的 [CPU](https://github.com/snapotter-hq/SnapOtter/blob/main/docker/docker-compose.yml) 和 [GPU](https://github.com/snapotter-hq/SnapOtter/blob/main/docker/docker-compose-gpu.yml) Compose 文件是事实来源。不要将缩写示例复制到生产中;从您验证的发布标签部署文件。
两个堆栈都应用以下控制:
- 内存、交换、CPU 和 PID 限制包含失控的本机处理。
- 每个服务都会放弃所有 Linux 功能。该应用程序仅添加回 `CHOWN, SETUID, SETGID, DAC_OVERRIDE, FOWNER, KILL` 来实现卷所有权、单向 `gosu` 身份删除以及优雅的信号转发。 PostgreSQL 和 Redis 仅接收其官方入口点所需的子集。
- `security_opt: [no-new-privileges:true]` 防止应用程序、PostgreSQL 和 Redis 容器中的进程获得额外权限。这仍然与 `gosu` 兼容:入口点以 root 身份开始,准备卷,并且仅下降到专用的 `snapotter` 用户。
- PostgreSQL 和 Redis 图像输入由摘要固定。应用程序同样应该固定到经过验证的发布标签或摘要,而不是 `latest`
- 健康检查、有界 JSON 日志轮换、持久的 Redis AOF 和重启策略在规范文件中集中定义。
对于面向互联网的部署,将端口 1349 绑定到环回并在维护的反向代理处终止 TLS。生成唯一的 PostgreSQL 和 Redis 凭据,将机密存储在受保护的文件或机密管理器中,并立即更改初始管理员密码。
### 为什么 `read_only` 没有设置 {#why-read-only-is-not-set}
未设置 `read_only: true`,因为 PUID/PGID 重新映射在启动时写入 `/etc/passwd``/etc/group`。如果您使用 Docker 的 `--user` 标志或 Kubernetes `runAsUser` 而不是 PUID/PGID,则可以安全地启用只读根文件系统。
## 网络隔离{#network-isolation}
文件处理是本地的,但默认安装**不是无出口系统**。启用遥测功能时,匿名产品分析使用 PostHog,崩溃报告使用 Sentry。设置 `SNAPOTTER_TELEMETRY=0`(或在“设置”>“系统”>“隐私”下禁用分析)以关闭两者。 SnapOtter 绝不会在这些事件中包含上传的文件、文件名、OCR 输出、文档文本或其他文件内容。
其他出站流量是功能驱动的:AI 捆绑包/模型安装下载签名发布输入; URL导入获取用户请求的公共URL;并显式配置的 OIDC、SAML、OpenTelemetry、webhooks、S3 兼容存储或类似集成会联系管理员选择的目标。运行时模型下载默认处于禁用状态。仅在明确选择启用自动回退下载时设置 `SNAPOTTER_ALLOW_MODEL_DOWNLOAD=1`。[离线捆绑导入](/zh-CN/guide/deployment) 可以在没有运行时模型出口的情况下提供 AI 功能。
**防火墙建议:**
|设想|出站规则|
|---|---|
|气隙|设置`SNAPOTTER_TELEMETRY=0``SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0`,使用离线AI捆绑导入,禁用URL导入和外部集成,然后阻止出口|
|默认遥测|允许浏览器/网络日志列出的 PostHog 和 Sentry 端点;如果策略不允许,则禁用遥测|
|需要 AI 捆绑包|安装过程中,允许HTTPS到`huggingface.co, *.xethub.hf.co, cdn-lfs.huggingface.co, github.com, objects.githubusercontent.com, storage.googleapis.com, pypi.org, files.pythonhosted.org`;然后阻止这些主机|
|外部集成|仅允许管理员准确配置的 OIDC/SAML/OTLP/webhook/对象存储目标|
捆绑包档案由 Hugging Face 的 Xet 存储提供,该存储通过 `*.xethub.hf.co` 端点并行传输,这使得多 GB 捆绑包下载速度更快。如果您的防火墙允许 `huggingface.co` 但阻止 `*.xethub.hf.co`,安装仍然会成功,但会回退到较慢的单流下载,因此将 Xet 主机列入白名单以保持快速路径。完全离线安装可以跳过所有这些并使用[离线捆绑导入](/zh-CN/guide/deployment)。
有关反向代理配置(Nginx、Traefik、Caddy、Cloudflare Tunnels),请参阅[部署指南](/zh-CN/guide/deployment#reverse-proxy)。
## Docker 密钥 {#docker-secrets}
对于生产部署,请避免将密钥作为明文环境变量传递。入口点支持 Docker 的 `_FILE` 约定:将密钥挂载为文件,并将对应的 `_FILE` 变量设置为其路径。
**支持的密钥:**
| 变量 | `_FILE` 等价项 |
|---|---|
| `DEFAULT_PASSWORD` | `DEFAULT_PASSWORD_FILE` |
| `COOKIE_SECRET` | `COOKIE_SECRET_FILE` |
| `OIDC_CLIENT_SECRET` | `OIDC_CLIENT_SECRET_FILE` |
| `S3_ACCESS_KEY_ID` | `S3_ACCESS_KEY_ID_FILE` |
| `S3_SECRET_ACCESS_KEY` | `S3_SECRET_ACCESS_KEY_FILE` |
| `SNAPOTTER_LICENSE_KEY` | `SNAPOTTER_LICENSE_KEY_FILE` |
**使用 Docker Compose 密钥的示例:**
```yaml
services:
SnapOtter:
image: snapotter/snapotter:latest
environment:
- AUTH_ENABLED=true
- DEFAULT_USERNAME=admin
- DEFAULT_PASSWORD_FILE=/run/secrets/snapotter_password
- COOKIE_SECRET_FILE=/run/secrets/cookie_secret
secrets:
- snapotter_password
- cookie_secret
secrets:
snapotter_password:
file: ./secrets/snapotter_password.txt
cookie_secret:
file: ./secrets/cookie_secret.txt
```
::: tip
Docker Compose 密钥(不使用 Swarm)需要 Compose v2.23 或更高版本。
:::
## Kubernetes 部署 {#kubernetes-deployment}
入口点会检测容器是否已经以非 root 运行(例如通过 Kubernetes 的 `runAsUser`),并自动跳过 gosu 降权。在这种情况下,它无法自行 chown 挂载的卷,因此会验证它们是否可写,若不可写则提前退出并给出可操作的指引 - 关于 `fsGroup` 和外来 UID 设置(TrueNAS、OpenShift),请参阅 [存储权限](/zh-CN/guide/deployment#storage-permissions)。
**推荐的 Pod SecurityContext**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: snapotter
spec:
replicas: 1
selector:
matchLabels:
app: snapotter
template:
metadata:
labels:
app: snapotter
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
containers:
- name: snapotter
image: snapotter/snapotter:latest
ports:
- containerPort: 1349
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "4"
memory: 6Gi
livenessProbe:
httpGet:
path: /api/v1/health
port: 1349
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /api/v1/health
port: 1349
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: data
mountPath: /data
- name: workspace
mountPath: /tmp/workspace
volumes:
- name: data
persistentVolumeClaim:
claimName: snapotter-data
- name: workspace
emptyDir:
medium: Memory
sizeLimit: 2Gi
```
由于 `runAsUser: 999` 是在 Pod 层级设置的,入口点会完全跳过 gosu。这样就能在无冲突的情况下使用 `allowPrivilegeEscalation: false``drop: [ALL]` 权能。
关于资源规格,请参阅 [硬件要求](/zh-CN/guide/deployment#hardware-requirements)。
## 备份与恢复 {#backup-and-recovery}
生产 Compose 堆栈定义了四个卷。在进行协调备份之前停止入口并让活动作业完成,以便 PostgreSQL、Redis 和文件状态描述相同的时间点。
|体积|内容|康复治疗|
|---|---|---|
|`SnapOtter-pgdata`|PostgreSQL 用户、设置、管道、作业、文件元数据和审核日志|批判的;使用快速故障逻辑转储进行可移植恢复|
|`SnapOtter-data`|保存的库对象、日志和 AI 状态 (`/data/files, /data/logs, /data/ai, /data/ai/venv`)|备份整个卷;为了节省空间,故意省略所有 AI 状态并重新安装其捆绑包|
|`SnapOtter-redisdata`|Redis AOF 用于持久的 BullMQ 队列状态|暂停应用程序并强制`SAVE`后备份;需要准确地恢复排队的工作|
|`SnapOtter-workspace`|临时对象存储键 (`/tmp/workspace/uploads, /tmp/workspace/outputs`)|所有作业清空或取消后不要进行备份;当工作处于活动状态时切勿丢弃它|
Compose 通常在卷名称前加上项目名称作为前缀。从已安装的容器中解析真实的源卷,而不是假设显示名称(例如 `SnapOtter-data`)是 Docker 卷名称。
### 数据库备份{#database-backup}
使用 PostgreSQL 的自定义存档格式并在将备份视为完整之前验证存档:
```bash
docker exec SnapOtter-postgres \
pg_dump --format=custom --no-owner -U snapotter snapotter > snapotter.dump
test -s snapotter.dump
docker exec -i SnapOtter-postgres pg_restore --list < snapotter.dump >/dev/null
# Restore only into a fresh/disposable target first; any SQL error fails the command.
docker exec -i SnapOtter-postgres \
pg_restore --exit-on-error --clean --if-exists --no-owner \
-U snapotter -d snapotter < snapotter.dump
```
通过将每个备份恢复到隔离堆栈、检查数据库记录和文件校验和以及启动应用程序来测试每个备份。存储库的 `tests/qa/backup-restore-drill.sh` 会针对显式 `QA_IMAGE` 自动执行该发布门。
如果您的平台采用崩溃一致的卷快照,请首先停止整个堆栈,并将所有关键卷快照为一组。来自正在运行的容器的原始 PostgreSQL 数据目录副本不是受支持的逻辑备份。
### 文件和队列备份 {#file-and-queue-backup}
在捕获文件和队列卷之前暂停应用程序。使用 `docker inspect` 解析实际卷名称,强制 Redis 保留其当前状态,并在保留所有权和权限的情况下进行归档:
```bash
docker stop SnapOtter
docker exec SnapOtter-redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning SAVE
docker stop SnapOtter-redis
DATA_VOLUME="$(docker inspect SnapOtter --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
REDIS_VOLUME="$(docker inspect SnapOtter-redis --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
install -d -m 700 backup
docker run --rm -v "$DATA_VOLUME:/source:ro" -v "$PWD/backup:/backup" \
alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce tar czf /backup/snapotter-data.tar.gz -C /source .
docker run --rm -v "$REDIS_VOLUME:/source:ro" -v "$PWD/backup:/backup" \
alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce tar czf /backup/snapotter-redis.tar.gz -C /source .
sha256sum backup/snapotter-*.tar.gz > backup/SHA256SUMS
```
应用前重启Redis。如果您有意排除 `/data/ai`,请删除整个 AI 子树,而不是保留不带模型或虚拟环境的 `installed.json` 记录。保持备份文件加密、访问受控,并与运行 SnapOtter 的主机分开。
## 合规工件 {#compliance-artifacts}
每个 SnapOtter 版本都包含以下安全工件:
| 人工制品 | 格式 | 在哪里可以找到它 |
|---|---|---|
| 释放主体绑定 | 规范 JSON + GitHub 证明 | [GitHub发布](https://github.com/snapotter-hq/SnapOtter/releases)资产:`snapotter-v{version}-release-subjects.json` |
| 归档 SBOM | CycloneDX 和 SPDX JSON | 释放资产:`snapotter-v{version}-archive-linux-{arch}-sbom.{cdx,spdx}.json` |
| 图片 SBOM | CycloneDX 和 SPDX JSON | 释放资产:`snapotter-v{version}-image-linux-{arch}-sbom.{cdx,spdx}.json` |
| 漏洞扫描 | Trivy JSON | 发布具有匹配 `archive-linux-{arch}``image-linux-{arch}` 前缀的资产 |
| 漏洞扫描 | SARIF | [GitHub 安全](https://github.com/snapotter-hq/SnapOtter/security) 选项卡 |
| 静态分析 | CodeQL (JS/TS + Python) | [GitHub 安全](https://github.com/snapotter-hq/SnapOtter/security) 选项卡,每周运行 + 每个 PR |
| 依赖性审查 | GitHub 本机 | 按 PR 检查,高严重性添加失败 |
| Python依赖审计 | pip-audit | CI 在每次推送时运行日志 |
| 安全政策 | Markdown | 存储库中的 [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md) |
| 依赖项更新 | Dependabot | npm、pip、Docker、Actions 的自动每周 PR |
**运行您自己的扫描:**
下载发布主题清单并验证它是否已由发布工作流程证明:
```bash
gh attestation verify snapotter-v2.2.0-release-subjects.json \
--repo snapotter-hq/SnapOtter \
--signer-workflow snapotter-hq/SnapOtter/.github/workflows/release.yml
```
清单中分别记录了 `releaseTag``releaseCommit``workflowTriggerCommit`。验证 `releaseCommit` 是否是从不可变标记中剥离的提交,然后验证存档、映像、SBOM 的 SHA-256 摘要,或根据 `subjects` 中的条目进行扫描。这种区别是有意为之的:签出新创建的发布提交不会更改工作流的 OIDC 凭证中的提交标识。
您还可以直接扫描下载的 SBOM 或图像:
```bash
# Scan with Grype using the CycloneDX SBOM
grype sbom:snapotter-v2.2.0-image-linux-amd64-sbom.cdx.json
# Scan with Trivy using the SPDX SBOM
trivy sbom snapotter-v2.2.0-image-linux-amd64-sbom.spdx.json
# Scan the Docker image directly
trivy image snapotter/snapotter:2.2.0
```
::: info
图像 SBOMs 和扫描反映了针对该版本发布的具体架构特定图像。存档 SBOMs 和扫描分别描述预建存档。部署后安装的 AI 模型包不包含在这些 SBOMs 中,因为它们是在运行时下载的。
:::