如何向客户分发、更新和授权本地 FastAPI 服务
结论
这类场景不适合把“加密后的 Python 代码”直接交给客户,更合适的是搭建一套受控的边缘设备架构:
- 通过容器、系统包或预装设备分发服务;
- 云端授权服务签发短期运行租约,能否续租取决于订阅状态;
- 使用签名制品和 A/B 更新机制发布版本;
- 模型只保存在加密磁盘中,并尽量绑定 TPM、Secure Boot 和设备身份;
- Supabase 的高权限凭据只放在云端,现场服务使用权限最小、可撤销的短期凭据;
- 远程停用的目标应是“停止提供服务”,不要承诺远程删除全部代码和模型。
首先要明确安全边界。如果客户拥有设备的管理员权限和物理控制权,而模型又必须以明文形式参与本地推理,就不可能从技术上彻底阻止客户复制代码、提取内存或修改授权检查。代码混淆、PyInstaller、模型加密和 Docker 都只能增加提取成本,无法提供密码学意义上的绝对保护。
如果模型价值很高,最好交付由供应商管理的专用设备,并限制客户取得 root 权限、调试接口和启动介质控制权。如果允许客户在自有机器上安装,就需要接受模型最终可能被提取的风险,或者改用云端推理、可信执行环境等隔离能力更强的方案。
推荐架构
系统可以分为五部分:
-
现场运行服务
FastAPI、推理运行时、本地设备接口和必要的网络功能都在现场运行。
-
设备身份
每台设备首次注册时生成独立密钥。私钥应优先保存在 TPM 2.0、Secure Element 或设备硬件密钥库中,并设为不可导出。
-
授权服务
云端授权服务维护客户、设备、订阅和租约状态。现场服务定期申请短期租约。租约到期且无法续期后,业务功能停止运行。
-
制品仓库和更新服务
仓库保存签名后的容器镜像、系统包、模型包和版本清单。设备只安装通过签名验证的制品。
-
支付与订阅系统
支付平台通过 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 还不够,因为仓库凭据泄漏、缓存污染或服务端配置错误仍可能导致错误制品被下发。
建议采用以下流程:
- CI 构建固定版本的制品,不复用版本号。
- 生成 SBOM,并扫描依赖和镜像。
- 使用独立发布密钥为镜像、软件包和模型清单签名。
- 设备拉取版本清单并验证签名。
- 将新版本下载到非活动分区或非活动容器槽位。
- 校验摘要和兼容性。
- 启动新版本并执行健康检查。
- 健康检查成功后切换流量,失败时自动回滚。
专用设备可以采用 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 访问、加密模型包和客户级模型指纹。这套方案可以可靠地管理订阅和更新,也能明显增加模型提取成本,但不能宣称它能在客户完全控制的现场设备上实现绝对防复制。
备注:内容仅供参考。
