OCSP Stapling 配置实战:证书吊销状态检查加固

OCSP Stapling 是 HTTPS 站点在证书吊销状态检查上最容易被忽略的一环:默认配置下浏览器要自行向 CA 的 OCSP 服务器发起查询,既暴露了访问行为,又拖慢了握手。本文给出 Nginx 开启 OCSP Stapling 与 stapling_verify 的完整配置、resolver 与证书链要求、Must-Staple 证书签发步骤,以及用 openssl s_client 确认 staple 是否真正生效的方法。

适用场景

  • 面向公网提供 HTTPS 服务的站点,希望缩短 TLS 握手时间、减少首字节延迟。
  • 对访问者隐私有要求,不希望每次握手都把域名与客户端 IP 直接暴露给第三方 CA 的 OCSP 服务器。
  • 证书被吊销后需要让浏览器立刻拒绝访问,而不是等到客户端自行查询或完全依赖 CRL。
  • 正在做 TLS 加固基线(HSTS、mTLS、TLS 1.2/1.3)的站点,补齐证书状态检查这一项。

前置条件

  • Nginx 1.3.7 及以上;ssl_stapling_verify 需要 1.3.7+,完整链校验建议 1.11.0+。
  • 已安装 OpenSSL 1.1.1+ 命令行工具,用于手工验证与证书签发。
  • 服务器证书文件包含完整证书链(站点证书 + 中间 CA 证书),只配站点证书会导致 stapling 静默失败。
  • 服务器能够解析并访问 CA 的 OCSP responder 地址,出方向 80 端口未被防火墙封禁。
  • 若站点前置了 CDN 或云 WAF,需确认该产品是否自行提供 stapling;边缘回源握手与终端用户握手是两回事。

原理说明

证书吊销状态有三种告知方式:CRL(证书吊销列表,文件体积大、更新滞后)、OCSP(在线证书状态协议,实时但每次查询都要联网)、OCSP Stapling(由服务器代为查询并缓存结果)。

未开启 Stapling 时,客户端拿到服务器证书后需要自己向 CA 的 OCSP responder 发一次 HTTP 请求。这带来三个问题:一是握手多一个 RTT,客户端要等 OCSP 响应才能完成校验;二是隐私泄露,CA 能记录下「谁在访问哪个域名」;三是可用性依赖,OCSP 服务器一旦不可达,客户端的处理策略各不相同,可能直接中断握手。

开启 Stapling 后,流程变为:Nginx 定期向 OCSP responder 查询并缓存带时间戳的签名响应,在 TLS 握手时通过 status_request 扩展(Certificate Status Request)把这份响应一并发送给客户端。客户端只需用 CA 的公钥验证签名与有效期,无需再联网。由于响应由 CA 签名,服务器无法伪造,因此不存在被中间人篡改的可能。

在此之上还有 Must-Staple:在证书里写入 TLS Feature 扩展(OID 1.3.6.1.5.5.7.1.24,值 status_request(5))。带此扩展的证书要求客户端必须收到有效的 staple,否则拒绝连接。它把「可选优化」变成了「强制校验」,代价是服务器上的 OCSP 查询一旦失败,站点会直接不可访问。

操作步骤

第一步:确认证书链与 OCSP responder 地址

先看清手上的证书是否具备开启条件:

# 查看 OCSP responder 地址,输出必须非空
openssl x509 -in /etc/nginx/ssl/example.com.crt -noout -ocsp_uri

# 确认证书链完整(应能看到至少一张中间 CA 证书)
grep -c 'BEGIN CERTIFICATE' /etc/nginx/ssl/example.com.crt

# 查看证书主题与有效期
openssl x509 -in /etc/nginx/ssl/example.com.crt -noout -subject -dates

如果 ocsp_uri 返回为空,说明该 CA 不提供 OCSP 服务(部分 CA 已转向纯 CRL 方案),此时可以跳过 Stapling,直接依赖 CRL 与短期证书策略。如果 BEGIN CERTIFICATE 计数为 1,说明当前文件只有站点证书,需要补上中间证书,例如把 CA 提供的 ca-bundle.crt 追加到同一文件:

cat /etc/nginx/ssl/example.com.crt /etc/nginx/ssl/ca-bundle.crt > /etc/nginx/ssl/example.com.fullchain.crt

随后配置中统一使用 fullchain 文件。

第二步:配置 Nginx OCSP Stapling

编辑站点 vhost,在 server 块中补齐以下指令:

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/example.com.fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # DNS 解析器:用于解析 CA 的 OCSP responder 域名
    resolver 223.5.5.5 119.29.29.29 valid=300s;
    resolver_timeout 5s;

    # 信任链文件:用于校验 OCSP 响应签名,必须是完整的证书链
    ssl_trusted_certificate /etc/nginx/ssl/example.com.fullchain.crt;

    # 开启 OCSP Stapling 并校验响应签名
    ssl_stapling on;
    ssl_stapling_verify on;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
}

参数要点:

  • resolver 必须显式配置。Nginx 不会使用 /etc/resolv.conf 的配置来解析 OCSP responder 域名,漏配这一项是最常见的失败原因。国内环境建议同时配置运营商 DNS 与公共 DNS。
  • ssl_trusted_certificate 应指向完整链文件,且与 ssl_certificate 使用的证书属于同一签发链,否则 ssl_stapling_verify 校验必然失败。
  • ssl_stapling_verify on 建议开启,它会验证 OCSP 响应的签名与链路,避免把伪造响应转发给客户端。
  • server_name 或泛域名场景下,每个 server 块都要独立配置,ssl_stapling 不会自动继承跨块生效。

第三步:Must-Staple(可选,谨慎启用)

若希望客户端强制校验 staple,需要在申请证书阶段就把扩展写进 CSR。生成密钥与 CSR:

# 1) 生成私钥
openssl genrsa -out example.com.muststaple.key 2048

# 2) 编写带 TLS Feature 扩展的配置文件
cat > muststaple.cnf <<'EOF'
[ req ]
default_bits       = 2048
prompt             = no
distinguished_name = dn
req_extensions     = v3_req

[ dn ]
CN = example.com

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage         = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05
EOF

# 3) 生成 CSR
openssl req -new -key example.com.muststaple.key -out example.com.muststaple.csr -config muststaple.cnf

# 4) 校验 CSR 中确实带上了 TLS Feature 扩展
openssl req -in example.com.muststaple.csr -noout -text | grep -A2 '1.3.6.1.5.5.7.1.24'

其中 DER:30:03:02:01:05status_request(5) 的 DER 编码。把 CSR 提交给 CA 签发后,用下面的命令确认扩展已写入证书:

openssl x509 -in example.com.muststaple.crt -noout -text | grep -A2 'TLS Feature'

重要提醒:启用 Must-Staple 前,必须先确保 Stapling 稳定工作至少一周。一旦服务器无法获取或缓存失效,带此扩展的证书会让浏览器直接拒绝连接,影响面等同于证书过期。

第四步:缓存与性能调优

Nginx 每个 worker 进程都会独立缓存 OCSP 响应,并在响应中的 Next Update 时间前刷新。可调项有限,但有几处值得注意:

  • Nginx 默认在握手时如果本地没有缓存,会立即返回不带 staple 的响应,同时异步去查询。因此服务重启后的第一次握手通常拿不到 staple,属正常现象。
  • resolvervalid=300s 控制 DNS 缓存时长,取值过大会导致 OCSP responder 更换 IP 后解析不到;取值过小会增加 DNS 查询压力,实践中 300 秒较为平衡。
  • OCSP 响应缓存在内存中,重启或 reload Nginx 会清空缓存,若使用 Must-Staple,建议在低峰期执行 reload。

第五步:吊销演练

规则配置完成后,建议在测试环境用一张临时证书走一遍吊销流程,确认吊销后浏览器确实拦截:

# 1) 查看证书的序列号
openssl x509 -in test.example.com.crt -noout -serial

# 2) 在 CA 后台执行吊销(此处以自建 CA 为例,公有 CA 请用其管理控制台)
openssl ca -config openssl.cnf -revoke test.example.com.crt -crl_reason keyCompromise
openssl ca -config openssl.cnf -gencrl -out crl.pem

# 3) 查询 OCSP 状态,期望返回 revoked
openssl ocsp -issuer ca.crt -cert test.example.com.crt \
  -url "$(openssl x509 -in test.example.com.crt -noout -ocsp_uri)" -resp_text | grep -i 'Cert Status'

吊销生效存在传播延迟,取决于 CA 刷新 OCSP 响应的频率,通常为几分钟到数小时。演练时不要把它当成 Nginx 配置错误。

配置验证

验证 Stapling 是否真正生效,用 openssl s_client-status 参数:

# 语法校验
nginx -t && nginx -s reload

# 第一次连接(可能未命中缓存,属正常)
echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null \
  | sed -n '/OCSP Response Status/,/OCSP Response Data/p'

# 第二次连接(缓存已建立,应能看到完整响应)
echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null \
  | sed -n '/OCSP Response Status/,/-----END/p'

成功时输出应包含:

OCSP Response Status: successful (0x0)
OCSP Response Data:
    OCSP Response Type: Basic OCSP Response
    Responder Id: C = US, O = Example CA, CN = ocsp.example-ca.com
    Cert Status: good
    This Update: Sep 17 02:00:00 2026 GMT
    Next Update: Sep 24 02:00:00 2026 GMT

判定标准:出现 OCSP Response Status: successful (0x0)Cert Status: good 才算成功。若输出为 OCSP response: no response sent,按下节排查。

补充两项检查:

# 确认 Nginx 错误日志中没有 stapling 相关报错
grep -i 'stapling\|ocsp' /var/log/nginx/error.log | tail -20

# 确认配置已生效(应输出 on / on)
nginx -T 2>/dev/null | grep -E 'ssl_stapling'

常见问题(FAQ)

Q1:验证时提示 OCSP response: no response sent,怎么排查?

按以下顺序定位,覆盖了绝大多数情况:

  • 是否已 reload:配置变更后必须 nginx -s reload,用 nginx -T | grep ssl_stapling 确认生效。
  • 是否只连了一次:冷启动第一次握手通常不带 staple,连续执行两次命令再判断。
  • resolver 是否配置:这是最高频的原因,Nginx 不读系统 DNS 配置,漏配时无法解析 responder 域名,错误日志中会出现 no resolver defined to resolve ...
  • 证书链是否完整:只有站点证书时,ssl_stapling_verify on 因无法构建信任链而失败,错误日志提示 ssl_stapling: issuer certificate not found
  • 出方向 80 端口是否放通:OCSP responder 走 HTTP 协议,被防火墙拦截时表现为查询超时。
  • CA 是否仍提供 OCSP:先执行 openssl x509 -noout -ocsp_uri,返回空说明该 CA 不支持。

Q2:开启 stapling 后浏览器就一定能发现证书被吊销吗?

不一定。Stapling 只解决「浏览器知不知道吊销状态」,不解决「什么时候知道」。OCSP 响应本身有 Next Update 有效期,通常为数小时至数天;在 CA 刷新响应之前,服务器缓存里仍是可以正常访问的旧状态。此外部分客户端在 staple 缺失时会静默降级为「不检查吊销」,而不是中断连接——这正是 Must-Staple 存在的意义。因此对安全性要求高的业务,推荐组合是:短期证书(有效期缩短到 90 天以内)+ OCSP Stapling + Must-Staple + 证书到期监控

Q3:站点前面有 CDN,还需要在源站配 stapling 吗?

两者是独立的握手,用户与 CDN 边缘之间的 stapling 由 CDN 提供,源站配置不会传递给终端用户。可以先验证边缘是否已开启:

echo | openssl s_client -connect www.example.com:443 -servername www.example.com -status 2>/dev/null \
  | grep -E 'OCSP Response Status|Cert Status'

若 CDN 已提供,源站可以不开;若 CDN 不提供,可向厂商提交配置需求,或在源站开启以保证边缘回源链路的证书校验效率。需要注意 CDN 通常只缓存单张证书,泛域名或 SAN 证书场景下要确认边缘返回的链与终端命中域名一致。

总结

OCSP Stapling 的配置量很小,但生效条件很挑:完整的证书链、显式配置的 resolver、正确的信任链文件,三者缺一不可。落地路径建议为:先补全证书链并确认 CA 提供 OCSP 服务,再配置 ssl_stapling_verify onssl_trusted_certificate,用 openssl s_client -status 连续两次验证缓存建立,最后视业务风险决定是否启用 Must-Staple。对签发 CA 已停用 OCSP 的站点,无需强求,把精力放在短期证书与到期监控上收益更高。