This page was machine-translated. Spotted a mistake?Help improve it.
Skip to content

安全与加固

SnapOtter 完全在你自己的基础设施上处理文件。它默认发送匿名、不含内容的产品分析和崩溃报告,以帮助改进项目。它绝不会发送你的文件、文件名、文件内容、OCR 输出、图像元数据或文档文本。可选的反馈仅在用户提交后才会发送,仅在分析启用时发送,且联系字段仅在获得明确的联系同意时才包含在内。管理员可以在 Settings > System > Privacy 下一键关闭分析和反馈采集,无需重新构建。文件处理始终留在你的容器内部。

容器以专用的非 root 用户(snapotter)运行,除最低必需集之外的所有 Linux 权能都被丢弃。完整的漏洞披露政策和安全架构,请参阅 GitHub 上的 SECURITY.md

容器加固

默认 docker-compose.yml 包含生产环境的安全加固。以下是对每个选项及其重要性的逐项说明:

yaml
services:
  SnapOtter:
    image: snapotter/snapotter:latest
    ports:
      # Bind to localhost only for internet-facing deployments:
      - "127.0.0.1:1349:1349"
    volumes:
      - SnapOtter-data:/data
      - SnapOtter-workspace:/tmp/workspace
    environment:
      - AUTH_ENABLED=true
      - DEFAULT_PASSWORD=change-me-immediately
      - RATE_LIMIT_PER_MIN=1000
      - DATABASE_URL=postgres://snapotter:snapotter@postgres:5432/snapotter
      - REDIS_URL=redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

    # --- Resource limits ---
    mem_limit: 6g            # Prevents runaway memory from crashing the host
    memswap_limit: 6g        # No swap - fail fast instead of degrading the host
    cpus: 4                  # Cap CPU usage to 4 cores
    pids_limit: 512          # Prevents fork bombs

    # --- Capability restrictions ---
    cap_drop:
      - ALL                  # Drop ALL Linux capabilities first
    cap_add:
      - CHOWN                # Needed for volume permission setup
      - SETUID               # Needed for gosu privilege drop (root -> snapotter)
      - SETGID               # Needed for gosu privilege drop
      - DAC_OVERRIDE         # Needed for volume permission setup
      - FOWNER               # Needed for volume permission setup

    # --- Logging ---
    logging:
      driver: json-file
      options:
        max-size: "50m"      # Rotate logs at 50 MB
        max-file: "5"        # Keep 5 rotated log files

    # --- Health check ---
    healthcheck:
      test: ["CMD", "curl", "-sf", "--max-time", "5", "http://localhost:1349/api/v1/health"]
      interval: 30s
      timeout: 5s
      start_period: 60s
      retries: 3

    shm_size: "2gb"          # Required for Python ML shared memory
    restart: unless-stopped

  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: snapotter
      POSTGRES_PASSWORD: snapotter
      POSTGRES_DB: snapotter
    volumes:
      - SnapOtter-pgdata:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U snapotter"]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 15s

  redis:
    image: redis:8-alpine
    command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
    volumes:
      - SnapOtter-redisdata:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 10s

volumes:
  SnapOtter-data:
  SnapOtter-workspace:
  SnapOtter-pgdata:
  SnapOtter-redisdata:

为何不设置 no-new-privileges

security_opt: [no-new-privileges:true] 被有意省略。入口点以 root 启动以修复卷的所有权,然后通过 gosu 降权到 snapotter 用户,而这需要 setuid。一旦降权完成,进程就以 snapotter 运行,除上面列出的五项外的所有权能都被移除。

如果你使用 Kubernetes 或 Docker 的 --user 标志直接以非 root 运行(绕过 gosu),那么启用 no-new-privileges 是安全的。

为何不设置 read_only

没有设置 read_only: true,因为 PUID/PGID 重映射会在启动时写入 /etc/passwd/etc/group。如果你使用 Docker 的 --user 标志或 Kubernetes 的 runAsUser 而非 PUID/PGID,你可以安全地启用只读根文件系统。

网络隔离

在正常运行期间,容器不发起任何出站网络连接。所有文件处理都使用捆绑的库在本地进行。

Browser  -->  Reverse Proxy (TLS)  -->  SnapOtter container  -->  (nothing)

唯一的例外是 AI 模型下载:当用户通过 UI 安装 AI 功能包时,容器会从 Hugging Face 下载预构建的包归档,外加来自 GitHub Releases、Google Storage 和 PyPI 的少量单独模型文件。这些下载每个包只发生一次,并存储在 /data 卷中。

防火墙建议:

场景出站规则
隔离网络(无 AI)阻止容器的所有出站流量
需要 AI 包安装期间允许对 huggingface.co*.xethub.hf.cocdn-lfs.huggingface.cogithub.comobjects.githubusercontent.comstorage.googleapis.compypi.orgfiles.pythonhosted.org 的 HTTPS,之后阻止
AI 安装完成后阻止所有出站流量 - 模型已在本地缓存

包归档由 Hugging Face 的 Xet 存储提供,它通过 *.xethub.hf.co 端点并行传输,正是它让数 GB 的包下载变得快速。如果你的防火墙允许 huggingface.co 但阻止 *.xethub.hf.co,安装仍会成功,但会回退到较慢的单流下载,所以请将 Xet 主机加入允许列表以保持在快速路径上。完全离线的安装可以跳过这一切,改用 离线包导入

关于反向代理配置(Nginx、Traefik、Caddy、Cloudflare Tunnels),请参阅 部署指南

Docker 密钥

对于生产部署,请避免将密钥作为明文环境变量传递。入口点支持 Docker 的 _FILE 约定:将密钥挂载为文件,并将对应的 _FILE 变量设置为其路径。

支持的密钥:

变量_FILE 等价项
DEFAULT_PASSWORDDEFAULT_PASSWORD_FILE
COOKIE_SECRETCOOKIE_SECRET_FILE
OIDC_CLIENT_SECRETOIDC_CLIENT_SECRET_FILE
S3_ACCESS_KEY_IDS3_ACCESS_KEY_ID_FILE
S3_SECRET_ACCESS_KEYS3_SECRET_ACCESS_KEY_FILE
SNAPOTTER_LICENSE_KEYSNAPOTTER_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 部署

入口点会检测容器是否已经以非 root 运行(例如通过 Kubernetes 的 runAsUser),并自动跳过 gosu 降权。在这种情况下,它无法自行 chown 挂载的卷,因此会验证它们是否可写,若不可写则提前退出并给出可操作的指引 - 关于 fsGroup 和外来 UID 设置(TrueNAS、OpenShift),请参阅 存储权限

推荐的 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: falsedrop: [ALL] 权能。

关于资源规格,请参阅 硬件要求

备份与恢复

持久化状态分布在两个卷中:

内容是否关键?
SnapOtter-pgdataPostgreSQL 数据库(用户、设置、流水线、任务、审计日志)
/data(应用卷)用户上传的文件、AI 模型、Python venv部分(见下文)

/data 卷内:

路径内容是否关键?
/data/uploads//data/outputs/用户文件和处理结果
/data/ai/下载的 AI 模型文件否(可重新下载)
/data/venv/Python 虚拟环境否(启动时重建)

数据库备份

在栈运行期间使用 pg_dump 备份数据库:

bash
# Dump the database
docker exec SnapOtter-postgres pg_dump -U snapotter snapotter > backup.sql

# Restore into a fresh database
cat backup.sql | docker exec -i SnapOtter-postgres psql -U snapotter snapotter

或者,停止栈并对 SnapOtter-pgdata 卷做快照:

bash
docker compose down
docker run --rm -v SnapOtter-pgdata:/data -v $(pwd)/backup:/backup \
  alpine tar czf /backup/snapotter-pgdata.tar.gz -C /data .

用户文件备份

bash
# Snapshot the app data volume (excluding re-downloadable AI models)
docker run --rm -v SnapOtter-data:/data -v $(pwd)/backup:/backup \
  alpine tar czf /backup/snapotter-files.tar.gz \
    --exclude='ai' --exclude='venv' -C /data .

AI 模型跨所有包总计约 24 GB。由于它们可重新下载,请从备份中排除 /data/ai//data/venv/ 以节省空间。只有数据库和用户文件是关键的。

合规产物

每个 SnapOtter 发布都包含以下安全产物:

产物格式获取位置
SBOM(CycloneDX)JSONGitHub Release 资产:snapotter-v{version}-sbom.cdx.json
SBOM(SPDX)JSONGitHub Release 资产:snapotter-v{version}-sbom.spdx.json
漏洞扫描Trivy JSONGitHub Release 资产:snapotter-v{version}-trivy.json
漏洞扫描SARIFGitHub Security 选项卡
静态分析CodeQL(JS/TS + Python)GitHub Security 选项卡,每周 + 每 PR 运行
依赖审查GitHub 原生每 PR 检查,在新增高危项时失败
Python 依赖审计pip-audit每次推送的 CI 运行日志
安全政策Markdown仓库中的 SECURITY.md
依赖更新Dependabot针对 npm、pip、Docker、Actions 的自动化每周 PR

运行你自己的扫描:

从发布中下载 SBOM,并用你偏好的工具扫描它:

bash
# Scan with Grype using the CycloneDX SBOM
grype sbom:snapotter-v1.17.2-sbom.cdx.json

# Scan with Trivy using the SPDX SBOM
trivy sbom snapotter-v1.17.2-sbom.spdx.json

# Scan the Docker image directly
trivy image snapotter/snapotter:1.17.2

INFO

SBOM 和漏洞扫描反映的是该次发布所发布的确切镜像。部署后安装的 AI 模型包不包含在 SBOM 中,因为它们是在运行时下载的。