Caloogy 文章
English

Fail-closed:出错时宁可停也不放行

日期
2025 年 10 月 22 日
系列
方法论
阅读
4 分钟
主题
Fail closedFail safeAvailabilityCircuit breaker

一句话版本

系统总会出错:数据源连不上、校验程序崩溃、配置文件读不出来。这时只有两种选择:放行(fail-open,保可用性)或拒绝(fail-closed,保安全性)。 对于和钱、权限、身份有关的决策,默认应该 fail-closed:检查不了,就当没通过。难点在于把"出错"和"正常的空结果"区分开,并且让 fail-closed 不至于把系统变得一碰就停。

全文分三层:

  1. 通俗理解:门禁停电时,门该开还是该锁
  2. 实际用处:防火墙、杀毒软件、熔断器、名单更新
  3. 原理与工程要点:识别"出错"、数据源异常的保护、自动止损、可用性的代价

第一层:通俗理解

1.1 门禁停电了

一栋楼的电子门禁停电了,门应该:

没有哪种绝对正确,要看出错时最怕的是什么:怕人出不去,还是怕坏人进来。

1.2 软件里的同一个问题

def is_allowed(address):
    try:
        return address not in fetch_blocklist()
    except NetworkError:
        return True   # ← fail-open:名单拉不到,就当没问题

这段代码看起来"更友好":网络抖动时用户仍能继续操作。但攻击者只要想办法让名单服务拉不到(比如攻击它、或者在网络上干扰它),所有检查就都失效了。

改成 fail-closed:

    except NetworkError:
        return False  # 检查不了,就当没通过

第二层:实际用处

场景 通常的选择 原因
防火墙规则 默认拒绝,只放行白名单 漏一条规则就是一个洞
金融交易风控服务超时 拒绝或转人工 放过一笔欺诈交易的代价远大于拒绝一笔正常交易
证书吊销检查(OCSP)失败 浏览器通常 fail-open("软失败") 否则 CA 服务器一抖动,全网 HTTPS 都打不开。这也是证书吊销机制长期被诟病"形同虚设"的原因
电路熔断器 过载时断开 宁可停电,不能起火
软件熔断器(circuit breaker) 下游故障时快速失败 防止故障层层蔓延,拖垮整个系统

OCSP 的例子说明:fail-closed 不是免费的,它把可用性压力转嫁到了依赖的服务上。


第三层:原理与工程要点

3.1 先识别"出错"

最隐蔽的 fail-open 不是 except: return True,而是把出错误当成正常的空结果:

看起来像 实际上
名单里没有这个地址 名单文件是空的(下载失败写了个空文件)
余额是 0 节点返回了错误,被解析成了 0
没有命中任何风控规则 规则配置没加载上
签名校验通过 校验函数因为异常被跳过了

所以:区分"成功地得到一个空结果"和"没能得到结果",后者必须显式报错。

3.2 数据源更新的保护

外部名单(诈骗地址、制裁名单、恶意域名)需要定期更新。更新本身也可能被攻击:上游仓库被投毒,推送一份几乎是空的名单,下游自动接受后,所有检查形同虚设。

保护措施:

3.3 不一致时停下

在多源交叉验证里,两个独立来源给出不同答案时,正确的反应是停下,而不是挑一个看起来顺眼的。这是 fail-closed 在冗余系统里的应用。

3.4 自动止损

更激进的 fail-closed 是:发现系统可能已被攻破时,主动让它停止服务。比如发现前端页面和预期不符,就自动下线页面,宁可所有人暂时用不了,也不让任何人在可能被篡改的页面上签名(见《Bybit事件与前端完整性》)。

设计要点:

3.5 代价与平衡

fail-closed 会让系统更"脆":任何一个依赖抖一下,用户就用不了。平衡的办法:


总结

层次 要点
直觉 出错时最怕什么?怕坏人进来,就锁门
原则 安全决策:检查不了就当没通过
陷阱 把"出错"误当成"空结果";名单被投毒缩水
平衡 按风险分级;依赖做冗余;失败说清楚;监控拒绝率