14.4.6. 运维:密钥、过期与故障排查

证书相关的工作有三个方面贯穿每一台已部署的设备:保护好已经写入摄像头的私钥、为证书过期的那一天提前做好规划,以及在实践中会遇到的为数不多的几种错误情形。

14.4.6.1. 保护私钥

每当摄像头出示证书时——无论是作为 TLS 服务器,还是作为 mTLS 中的客户端——其私钥都必须以明文 DER 的形式存放在设备上的文件系统或 ROMFS 中。以这种方式存储时,摄像头上运行的任何代码、以及任何能够物理接触它的人都能读取到它:通过 USB 大容量存储驱动器、REPL 提示符或原始闪存都可以。ROMFS 与只读标志只能防止修改,而无法防止提取应当假定设备上随附的任何私钥,都可被一个有决心且具备物理访问或代码访问能力的攻击者恢复出来。

这并不会让 TLS 变得毫无意义——它只是决定了你部署 TLS 的方式:

  • 每台设备使用唯一的密钥和证书。 切勿在整个设备群中烧录同一个共享密钥:那样的话,只要从其中一台设备提取出该密钥,攻击者就能冒充每一台摄像头。每台设备独立的密钥可将一次入侵的影响范围限制在那一台设备上,而你可以在服务器端将其吊销或禁用。

  • 让证书保持短有效期。 被盗的密钥只有在其证书仍然有效时才有用处;短有效期再加上例行的轮换,可以将损害限制在一定范围内(参见下文 证书过期与轮换)。

  • 只要能避免,就尽量不要在设备上放置任何机密信息。 如果你只需要验证服务器(仅服务器身份验证,而非 mTLS),那么作为客户端的摄像头只需存储 CA 证书——它是公开的——而不持有任何值得窃取的私钥。

  • 切勿在公开的固件镜像中随附密钥。 烘焙进你所分发的固件构建版本的 ROMFS 中的密钥并不是机密;任何下载该固件的人都能获得它。按设备逐一进行配置意味着在通用固件之后再写入密钥,而不是把它放在固件内部。

  • 限制影响范围(blast radius)。 将证书所认证的权限范围控制到最小(最小权限原则),并确保单个泄露的设备身份可以被吊销或禁用,而不会影响其余设备。

如果你的威胁模型中包含具备物理访问能力的攻击者,那么在设计时就应假定设备密钥终将泄露,并让系统能够在这种情况下继续安全运行,而不是假定在一个本就没有相应保护能力的硬件上能够将密钥保密。

14.4.6.2. 证书过期与轮换

每张证书都带有一个有效期窗口。一旦过期,ssl.CERT_REQUIRED 的对端就会拒绝连接——因此过期是一次实实在在、有计划可循的服务中断,而非理论上的风险。摄像头的时钟也必须正确,有效性检查才能被如实地评估。

  • 自签名证书。 你通过 -days 选定了有效期。当它失效时,你必须重新生成密钥/证书并重新部署:重新复制 DER 文件,或者在证书被烘焙进 ROMFS 的情况下重新构建并重新刷写 ROMFS 镜像。请选取一个你确实会记得去处理的 -days 值。

  • 公共 CA。 这类证书有意被设置为短有效期。Let's Encrypt 签发的是 90 天证书,并期望大约每 60 天进行一次自动续期;不存在“安装一次便一劳永逸”的选项。

更宏观的趋势是单向的:公开受信任的 TLS 证书的最大有效期在不断缩短。它曾经是 825 天,目前上限为 398 天,而 CA/Browser Forum 已经通过了一份时间表,将其逐步下调至到 2029 年约为 47 天。对设备设计而言,应得出的结论是:假定证书都是短有效期的,并且轮换必须自动化,或者至少要成为例行操作——不要推出一款依赖人工去更换一张十年期证书的产品。

在实践层面,就摄像头而言:应优先采用无需重新刷写即可更换证书的设计(可写文件系统加上远程更新通道,或者将摄像头作为客户端运行,让它信任一个由你集中轮换的 CA)。如果证书必须存放在 ROMFS 中,则需围绕其有效期来安排固件更新。

14.4.6.3. 故障排查

  • 时钟必须已设置。 一台自上电以来尚未设置过时钟的摄像头会在证书有效性检查中失败——请先调用 ntptime.settime()

  • 主机名必须匹配。 当客户端传入 server_hostname 时,它必须与证书的 subjectAltName(在较旧的协议栈上则是 CN)相匹配,否则验证会失败。

  • 格式错误。 复制到摄像头上的 PEM 文件无法加载——请先转换为 DER。

  • 证书已过期。 一个之前能正常工作、如今却因 OSError 而失败的连接,可能只是证书过期了——请检查有效期日期,并按需重新生成/重新部署。

  • Ed25519 密钥会失败。 请使用 ECDSA P-256/P-384 或 RSA,而不要使用 Ed25519。

  • 所有错误都是 OSError。MicroPython 并未实现 ssl.SSLError;TLS 故障(证书错误、过期、未知 CA、格式错误、握手失败)都会以 OSError 的形式抛出。