PAYATHON 2026

如何向客户分发、更新和授权本地 FastAPI 服务

支付阿杰

结论

这类场景不适合把“加密后的 Python 代码”直接交给客户,更合适的是搭建一套受控的边缘设备架构:

  • 通过容器、系统包或预装设备分发服务;
  • 云端授权服务签发短期运行租约,能否续租取决于订阅状态;
  • 使用签名制品和 A/B 更新机制发布版本;
  • 模型只保存在加密磁盘中,并尽量绑定 TPM、Secure Boot 和设备身份;
  • Supabase 的高权限凭据只放在云端,现场服务使用权限最小、可撤销的短期凭据;
  • 远程停用的目标应是“停止提供服务”,不要承诺远程删除全部代码和模型。

首先要明确安全边界。如果客户拥有设备的管理员权限和物理控制权,而模型又必须以明文形式参与本地推理,就不可能从技术上彻底阻止客户复制代码、提取内存或修改授权检查。代码混淆、PyInstaller、模型加密和 Docker 都只能增加提取成本,无法提供密码学意义上的绝对保护。

如果模型价值很高,最好交付由供应商管理的专用设备,并限制客户取得 root 权限、调试接口和启动介质控制权。如果允许客户在自有机器上安装,就需要接受模型最终可能被提取的风险,或者改用云端推理、可信执行环境等隔离能力更强的方案。

推荐架构

系统可以分为五部分:

  1. 现场运行服务

    FastAPI、推理运行时、本地设备接口和必要的网络功能都在现场运行。

  2. 设备身份

    每台设备首次注册时生成独立密钥。私钥应优先保存在 TPM 2.0、Secure Element 或设备硬件密钥库中,并设为不可导出。

  3. 授权服务

    云端授权服务维护客户、设备、订阅和租约状态。现场服务定期申请短期租约。租约到期且无法续期后,业务功能停止运行。

  4. 制品仓库和更新服务

    仓库保存签名后的容器镜像、系统包、模型包和版本清单。设备只安装通过签名验证的制品。

  5. 支付与订阅系统

    支付平台通过 webhook 更新授权服务中的订阅状态。现场设备不应直接根据支付平台的返回结果判断授权是否有效。

典型流程如下:

支付成功
  → 授权服务启用 entitlement
  → 设备注册并完成身份认证
  → 获得短期 lease
  → 下载签名的软件和模型
  → 启动 FastAPI 与本地推理
  → 定期续租

支付失败或订阅取消
  → entitlement 进入宽限期或撤销状态
  → 不再签发新 lease
  → 现有 lease 到期
  → 服务进入受限或停用状态

分发方式如何选择

供应商提供专用设备

这是保护模型并降低运维复杂度的最佳选择。出厂镜像中可以预装:

  • 精简 Linux;
  • Secure Boot;
  • TPM 2.0;
  • 全盘加密;
  • 容器运行时或 systemd 服务;
  • 更新代理;
  • 设备注册程序;
  • 防火墙和审计组件。

客户只负责供电和联网,不取得系统管理员权限。还可以加入机箱拆卸检测、禁用外部启动、锁定 BIOS/UEFI、关闭调试接口等措施,进一步增加提取难度。

全盘加密主要保护设备关机或磁盘被拆走后的数据。设备运行时,加密密钥已经可用,因此全盘加密本身无法防御拥有 root 权限的攻击者。

安装在客户已有机器上

可以采用以下任一种交付形式:

  • OCI/Docker 镜像;
  • Debian/RPM 系统包;
  • 由 PyInstaller、Nuitka 等工具生成的可执行程序;
  • Python wheel 或私有 PyPI 包。

在商业部署中,私有 PyPI 和 .zip 通常不适合作为主要保护手段,因为其中的源码、依赖和模型很容易被复制。生成可执行文件也只能增加逆向分析的难度。

如果客户机器支持 Docker,容器通常仍是最便于更新和回滚的方案。需要访问本机网络,并不意味着必须放弃容器。可以根据实际需求进行配置:

  • 在 Linux 上使用 --network host;
  • 使用 macvlan 或 ipvlan 为容器分配局域网地址;
  • 显式映射 USB、摄像头和 GPU 设备;
  • 通过 volume 挂载必要目录;
  • 只增加必需的 Linux capabilities。

例如:

services:
  edge-server:
    image: registry.example.com/edge/server:1.8.0
    network_mode: host
    restart: unless-stopped
    devices:
      - /dev/video0:/dev/video0
    volumes:
      - /var/lib/example-edge:/var/lib/example-edge
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

network_mode: host 的支持情况取决于操作系统和容器运行环境,尤其不能假设 Docker Desktop 的行为与原生 Linux 完全一致。也不要为了方便直接使用 --privileged,否则容器隔离基本等于失效。

Docker Swarm 更适合管理一组能够相互连接的节点,未必适用于大量分散在不同客户网络中、经常位于 NAT 后方的独立设备。对于这类场景,让更新代理和授权客户端主动向外连接通常更简单,也不需要开放入站管理端口。

订阅授权与远程停用

使用短期租约,不使用永久 license key

永久 license key 一旦交付,就很难可靠撤销。更合适的方式是由授权服务签发短期 lease,例如有效期为数小时或一天。lease 至少应包含:

{
  "device_id": "device-8f6c...",
  "customer_id": "customer-42",
  "features": ["cv-inference", "supabase-sync"],
  "issued_at": 1788192000,
  "expires_at": 1788278400,
  "model_version": "vision-2026-08",
  "software_channel": "stable"
}

授权服务使用私钥签名,现场程序只保存公钥。这样,现场设备无法自行伪造或延长 lease。

下面是简化后的验证示例:

from datetime import datetime, timezone

import jwt
from jwt import InvalidTokenError

PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----"""

def verify_lease(token: str, expected_device_id: str) -> dict:
    try:
        claims = jwt.decode(
            token,
            PUBLIC_KEY,
            algorithms=["EdDSA"],
            audience="example-edge",
            issuer="https://license.example.com",
            options={"require": ["exp", "iat", "iss", "aud", "device_id"]},
        )
    except InvalidTokenError as exc:
        raise RuntimeError("Invalid or expired lease") from exc

    if claims["device_id"] != expected_device_id:
        raise RuntimeError("Lease belongs to another device")

    expires_at = datetime.fromtimestamp(claims["exp"], tz=timezone.utc)
    if expires_at <= datetime.now(timezone.utc):
        raise RuntimeError("Lease expired")

    return claims

这里使用的算法仅作示例。实际实现应根据所选 JWT 库和密钥管理方案确定。私钥只能存放在授权服务或密钥管理系统中,不能写入客户端程序。

处理断网和支付失败

以下业务规则需要提前确定:

  • lease 的有效期;
  • 设备的续租频率;
  • 网络故障允许多长宽限期;
  • 支付失败后立即停用,还是等待账单宽限期结束;
  • 停用后保留哪些诊断和恢复功能;
  • 如何导出或保留客户数据。

例如,设备可以每 6 小时续租一次,lease 的有效期为 24 小时。支付失败后,授权服务停止签发新 lease,设备最迟在现有 lease 到期后停止推理。具体时长取决于业务和可用性要求,没有统一标准。

“断网数月仍能正常运行”和“付款失败后立即远程停用”无法同时实现。远程撤销依赖设备重新连接授权服务。允许离线的时间越长,撤销延迟也就越长。

授权检查也不能只靠隐藏 FastAPI 路由。推理、数据同步和敏感资源解密等多个环节都应统一检查授权状态:

from fastapi import Depends, FastAPI, HTTPException, status

app = FastAPI()

def require_entitlement() -> dict:
    lease = load_and_verify_current_lease()
    if "cv-inference" not in lease.get("features", []):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Subscription is not active",
        )
    return lease

@app.post("/infer")
def infer(payload: dict, lease: dict = Depends(require_entitlement)):
    return run_local_inference(payload)

更稳妥的设计是,没有有效租约时,设备无法取得短期数据凭据,或者无法解封模型加密密钥。即使有人绕过某个 API 检查,仍然需要突破其他安全边界。

不要依赖远程自删除

远程删除不适合作为可靠的授权机制,原因包括:

  • 设备可能已经断网;
  • 客户可以阻止删除命令;
  • 文件可能已经被备份;
  • SSD 和文件系统无法保证普通删除会安全擦除数据;
  • 删除程序可能影响客户的现场数据,带来运维和法律风险。

更合理的目标是停止续租、撤销凭据并关闭业务功能。对于供应商控制的设备,还可以执行 crypto-shredding,也就是销毁模型解密密钥,让磁盘上的密文失去使用价值。即便如此,已经复制出去的明文或内存内容仍然无法追回。

Supabase 凭据管理

不要把 Supabase service_role key 或其他长期高权限凭据放进交付给客户的程序。嵌入客户端的软件秘密最终都有可能被提取。

可以采用以下方案之一:

  • 现场设备只访问自建云端网关,再由网关访问 Supabase;
  • 设备通过硬件身份认证换取短期、最小权限的访问令牌;
  • 使用 Supabase Auth 和 Row Level Security,把设备限制在自己的 tenant、表和操作范围内;
  • 每台设备使用独立身份,以便单独撤销和审计。

订阅失效后,应同时执行以下操作:

  • 停止签发 lease;
  • 撤销该设备续领短期凭据的权限;
  • 禁用对应的设备或租户身份;
  • 继续执行数据库端 RLS 和服务端权限检查;
  • 记录撤销时间、撤销原因和设备最后在线时间。

无法访问 Supabase 不等于模型已经受到保护。如果推理完全在本地进行,撤销数据库权限并不能阻止修改后的程序继续使用已经复制的模型。

自动更新方案

更新包至少需要具备签名、完整性校验、原子切换和回滚能力。仅使用 HTTPS 还不够,因为仓库凭据泄漏、缓存污染或服务端配置错误仍可能导致错误制品被下发。

建议采用以下流程:

  1. CI 构建固定版本的制品,不复用版本号。
  2. 生成 SBOM,并扫描依赖和镜像。
  3. 使用独立发布密钥为镜像、软件包和模型清单签名。
  4. 设备拉取版本清单并验证签名。
  5. 将新版本下载到非活动分区或非活动容器槽位。
  6. 校验摘要和兼容性。
  7. 启动新版本并执行健康检查。
  8. 健康检查成功后切换流量,失败时自动回滚。

专用设备可以采用 A/B 系统分区。容器部署可以保留当前镜像和上一个稳定镜像。不要把可变的 latest 标签作为唯一版本依据,应固定 digest:

image: registry.example.com/edge/server@sha256:0123456789abcdef...

更新代理应作为独立、低权限的系统服务运行。不要让 FastAPI 进程拥有任意安装软件或执行 shell 命令的权限。

本地模型保护

可实际采用的分层措施

模型保护需要多层防护,不能依赖某个所谓的“模型加密库”:

  • 模型包在制品仓库和磁盘上都以密文保存;
  • 每台设备使用不同的数据加密密钥;
  • 密钥由 TPM 封装,并绑定 Secure Boot/PCR 状态;
  • 只有通过完整性验证的系统才能解封密钥;
  • 模型解密后尽量直接载入内存,不产生长期保存的明文文件;
  • 禁用或限制 ptrace、core dump、调试接口和交换分区;
  • 服务使用独立且无登录权限的系统用户运行;
  • 限制 root、SSH、外部启动和物理接口;
  • 对模型和程序签名,并在启动时检查完整性;
  • 在每个客户的模型制品中加入可追踪的水印或指纹;
  • 审计设备访问、模型版本和异常行为。

Linux 服务可以关闭 core dump,并进行基础加固:

[Service]
User=edge-service
Group=edge-service
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
RestrictSUIDSGID=true
LockPersonality=true
LimitCORE=0
ReadWritePaths=/var/lib/example-edge

这些选项能否使用,取决于 systemd 版本、模型运行时、GPU 驱动以及服务需要访问的设备。部署前必须逐项验证。

加密模型的现实限制

模型执行推理时,权重通常必须以运行时可以使用的形式出现在 CPU、GPU 或加速器内存中。只要客户控制操作系统或拥有足够高的权限,就可能:

  • 注入或修改 Python 代码;
  • hook 模型加载函数;
  • dump 进程内存;
  • 截获解密后的临时文件;
  • 调试推理运行时;
  • 修改授权校验逻辑。

因此,用 AES 加密模型文件,再把密钥写进 Python 程序,并没有实质安全性。攻击者可以同时取得密文、程序和密钥。

将 Python 打包、混淆、转换为 C/C++,或者改用专有模型格式,都能增加分析成本,但无法改变这个基本事实。

安全要求更高时的选择

如果模型泄漏造成的损失远高于现场推理节省的云成本,可以考虑:

  • 使用云端推理,只在现场执行预处理或低敏感部分;
  • 拆分模型,把关键层或关键后处理放在云端;
  • 使用受支持的 TEE、Confidential VM 或机密计算设备;
  • 使用带 Secure Boot、TPM 和远程证明的供应商专用硬件;
  • 使用专用推理芯片或安全模块,但需要确认它是否真正保护模型权重;
  • 为不同客户交付带有唯一指纹的模型,以便追查泄漏来源。

TEE 可以降低宿主操作系统直接读取模型内存的风险,但能否使用取决于硬件、驱动、GPU、模型框架和远程证明链。不能简单地认为“使用 TEE 就绝对安全”,尤其是 GPU 推理路径可能并不处于同一个可信边界内。

建议的落地顺序

第一阶段先处理授权和凭据:

  • 建立客户、设备、订阅和 entitlement 数据模型;
  • 接收支付 webhook;
  • 实现短期 lease;
  • 为每台设备建立独立身份;
  • 移除客户端中的 Supabase 长期高权限密钥;
  • 定义断网、欠费和恢复策略。

第二阶段完善交付可靠性:

  • 选择容器或系统包作为标准制品;
  • 建立私有制品仓库;
  • 为版本清单和制品签名;
  • 实现健康检查、灰度发布和自动回滚;
  • 保留紧急撤回缺陷版本的能力。

第三阶段加强模型保护:

  • 在专用设备上启用 Secure Boot、TPM 和全盘加密;
  • 使用设备级密钥加密模型;
  • 将密钥解封绑定到设备状态和有效授权;
  • 限制管理员权限、调试接口和物理访问;
  • 为不同客户的制品加入唯一模型指纹。

最后进行威胁建模,明确要防御的是普通用户误用、外部窃取、客户管理员逆向,还是具备实验室能力的物理攻击者。防御不同类型的攻击者,所需成本差别很大。

对于多数初创公司,一套现实且便于维护的组合是:专用 Linux 设备、容器化 FastAPI、TPM 设备身份、短期在线租约、签名 OTA 更新、最小权限 Supabase 访问、加密模型包和客户级模型指纹。这套方案可以可靠地管理订阅和更新,也能明显增加模型提取成本,但不能宣称它能在客户完全控制的现场设备上实现绝对防复制。

备注:内容仅供参考。