1. 场景复现:你的HTTPS请求为何突然"不信任"了?

假设你正在为电商系统搭建微服务架构,用OpenResty作为API网关反向代理到支付服务。某天突然收到报警:"SSL certificate verify failed"。小张的配置文件是这样写的:

# 错误示例:未配置证书验证
server {
    listen 443 ssl;
    server_name gateway.example.com;
    
    ssl_certificate     /path/to/gateway.crt;
    ssl_certificate_key /path/to/gateway.key;

    location /payment/ {
        proxy_pass https://internal-payment-service/;
        # 缺少关键安全配置 ↓
    }
}

当请求经过这个网关转发时,OpenResty默认不会验证后端证书的有效性。这就好比快递员不核对收件人身份证,直接把包裹交给陌生人。当后端服务证书过期、域名不匹配或被中间人攻击时,系统就会毫无防备。

2. 核心配置参数解析:安全验证三剑客

2.1 proxy_ssl_verify

proxy_ssl_verify on;  # 启用证书验证(默认off)
proxy_ssl_verify_depth 2;  # 证书链验证深度
  • 深度验证示例:假设证书链是服务端证书 <- 中间CA <- 根CA,depth=2表示允许两级CA链

2.2 proxy_ssl_trusted_certificate

proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; # 信任的CA集合
  • 文件需要包含所有可能用到的CA证书
  • 建议将企业私有CA也加入此文件

2.3 proxy_ssl_server_name

proxy_ssl_server_name on; # 启用SNI扩展(应对多域名托管场景)

3. 完整示例演示:从错误到正确的配置进化

3.1 基础安全配置

server {
    listen 443 ssl;
    server_name gateway.example.com;
    
    # 网关自身证书
    ssl_certificate     /etc/ssl/gateway.crt;
    ssl_certificate_key /etc/ssl/gateway.key;

    location /payment/ {
        proxy_pass https://internal-payment-service/;
        
        # 安全配置三件套
        proxy_ssl_verify on;
        proxy_ssl_trusted_certificate /etc/ssl/trusted-cas.crt;
        proxy_ssl_server_name on;
        
        # 调试日志(生产环境建议关闭)
        proxy_ssl_session_reuse on;
        error_log /var/log/nginx/ssl_errors.log debug;
    }
}

3.2 进阶场景:私有CA认证

当后端服务使用自签名证书时,需要将私有CA加入信任链:

# 合并公有CA和私有CA
cat /etc/ssl/certs/ca-certificates.crt /path/to/private-ca.crt > /etc/ssl/trusted-cas.crt

3.3 证书验证失败时的Nginx日志解读

观察错误日志时,你可能会看到:

SSL_do_handshake() failed (SSL: error:14090086:SSL routines:ssl3_get_server_certificate:certificate verify failed)

这通常意味着:

  1. 证书链不完整(缺少中间CA)
  2. 证书已过期
  3. 域名不匹配
  4. 根CA未受信任

4. 关联技术与进阶方案:Lua脚本动态处理

当需要动态适配多套证书时,可以通过Lua脚本扩展:

location /dynamic-proxy/ {
    access_by_lua_block {
        local upstream = require "ngx.upstream"
        local hosts = {
            ["serviceA"] = { ca = "/path/ca1.crt" },
            ["serviceB"] = { ca = "/path/ca2.crt" }
        }
        
        local service_name = ngx.var.arg_service
        local ca_path = hosts[service_name].ca
        
        -- 动态设置信任证书
        ngx.var.proxy_ssl_trusted_certificate = ca_path
    }
    
    proxy_pass https://backend/;
}

5. 应用场景与选型建议

适用场景:

  1. 混合云架构中对接不同CA签发的服务
  2. 微服务间的mTLS通信
  3. 代理第三方HTTPS API时的安全验证

技术对比:

方案 优点 缺点
Nginx原生配置 性能最优,配置简单 缺少动态能力
Lua脚本扩展 灵活应对复杂场景 需要维护脚本逻辑
第三方鉴权服务 集中管理证书 增加网络延迟

6. 避坑指南:那些年我们踩过的证书坑

6.1 证书链不完整

错误现象:unable to get local issuer certificate 解决方法:

# 使用openssl检查证书链
openssl s_client -connect internal-payment-service:443 -showcerts

6.2 时钟不同步引发的"过期"

某次故障排查发现,虽然证书有效期到2025年,但网关服务器的BIOS时钟错误导致提前触发过期验证。

6.3 隐蔽的SAN扩展

当证书没有包含Subject Alternative Name时,即使Common Name正确也会验证失败。

7. 总结与最佳实践

通过本文的配置示例和故障分析,我们总结出以下经验:

  1. 强制验证:生产环境必须开启proxy_ssl_verify
  2. 证书管理
    • 使用统一的证书仓库
    • 设置自动续期提醒
  3. 监控体系
    # 定时检查证书有效期
    echo | openssl s_client -connect backend:443 2>/dev/null | openssl x509 -noout -dates
    
  4. 防御性编程:在Lua脚本中加入异常捕获
  5. 文档同步:每次证书变更都要更新对应配置说明

当你在OpenResty的证书验证之路上遇到问题时,不妨从这三个维度切入检查:

  1. 信任链:是否包含所有必要CA证书?
  2. 时间线:证书是否在有效期内?
  3. 身份认证:证书域名与请求目标是否匹配?

记住,良好的证书管理习惯就像系安全带——平时可能觉得麻烦,关键时刻能救命。