Envoy 上游多证书支持:custom_tls_certificate_selector 与 max_session_keys=0 解锁客户端 TLS 多证书
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
本篇文章聚焦 Envoy 在客户端(上游)TLS 上下文中引入的一项新能力:当CommonTlsContext显式配置了custom_tls_certificate_selector且UpstreamTlsContext.max_session_keys设为 0 时,客户端上下文可以携带多张tls_certificates证书,并由自定义选择器在握手阶段根据服务器 Hello 与传输套接字选项动态选证。读完本文,你将理解该能力的启用条件、底层握手与选证实现原理、证书选择器与证书映射器的扩展生态,并获得可直接落地的 upstream 与 downstream 配置示例。
背景:为什么客户端上下文此前只允许单张证书
在 Envoy 的 TLS 架构中,服务端(downstream)上下文历来支持在同一CommonTlsContext下关联多张证书,用于同时承载 RSA 与 ECDSA 证书、以及基于 SNI 的证书选择(见 tls.proto 中tls_certificates字段的注释说明)。但客户端(upstream)上下文长期被限制为只允许单张证书。
这一限制并非没有道理。在 client_context_impl.cc 的构造逻辑中,Envoy 对证书数量做了显式校验:
// If a custom TLS certificate selector is used and maxSessionKeys is set to 0 // then allow multiple certificates. // // newSSL() installs a cached session before certificate selection callback, // and is only keyed by SNI, so it's possible that a session created for // cert A can be resumed by a connection that would choose cert B. Therefore // to prevent incorrect TLS session resumption, maxSessionKeys should be 0. if (!(config.tlsCertificateSelectorFactory() && config.maxSessionKeys() == 0) && tls_contexts_.size() != 1) { creation_status = absl::InvalidArgumentError("Client TLS context supports only a single certificate"); return; }从源码注释可以清晰看到限制的根因:newSSL()在证书选择回调之前就安装了缓存的 TLS 会话,而会话缓存仅以 SNI 作为键。如果同一 SNI 下可能选择证书 A 或证书 B,那么为证书 A 创建的会话有可能被本应选择证书 B 的连接复用,造成错误的会话恢复。因此,除非显式关闭会话恢复,否则多证书会引入安全隐患。
核心变更:自定义选择器 + max_session_keys=0 解锁多证书
本次 changelog 记录(tls__multiple-upstream-certificates-with-certificate-selector.rst)正式放开了这一限制:
Allow multiple
tls_certificatesin a client context, forCommonTlsContext, when acustom_tls_certificate_selectoris explicitly defined withmax_session_keysset to 0.
即满足以下两个条件同时成立时,客户端上下文允许配置多张证书:
CommonTlsContext中显式配置了custom_tls_certificate_selector(自定义 TLS 证书选择器);UpstreamTlsContext.max_session_keys显式设置为 0。
这与 tls.proto 中tls_certificates字段的官方注释完全对应:
Only a single TLS certificate is supported in client contexts unless
custom_tls_certificate_selectoris explicitly defined withmax_session_keysset to 0. In server contexts, Multiple TLS certificates can be associated with the same context to allow both RSA and ECDSA certificates and support SNI-based selection.
关键配置字段详解
CommonTlsContext.custom_tls_certificate_selector
该字段是config.core.v3.TypedExtensionConfig类型的扩展点(tls.proto),官方注释对其行为做了精确定义:
- downstream TLS 套接字:基于 TLS ClientHello 选择证书;若为空,则回退到原生选择逻辑——从证书的 DNS SAN 或 Subject Common Name 提取服务器名模式来匹配 SNI;
- upstream TLS 套接字:基于 TLS ServerHello 以及传输套接字选项(transport socket options)选择证书。
该字段归属的扩展类别为envoy.tls.certificate_selectors(下游)与envoy.tls.upstream_certificate_selectors(上游)。
UpstreamTlsContext.max_session_keys
该字段在 tls.proto 中定义为google.protobuf.UInt32Value:
Maximum number of session keys (Pre-Shared Keys for TLSv1.3+, Session IDs and Session Tickets for TLSv1.2 and older) to be stored for session resumption. Defaults to 1, setting this to 0 disables session resumption.
- 默认值:1,即默认开启会话恢复缓存;
- 设为 0:完全禁用会话恢复,这也是多证书场景的强制要求。
在客户端实现中,max_session_keys_控制两处行为:构造时通过SSL_CTX_sess_set_new_cb安装新会话回调(client_context_impl.cc),以及newSsl()时从上下文缓存或按 SNI 维度设置会话(client_context_impl.cc)。当max_session_keys_ == 0时这些路径全部被跳过,从而杜绝多证书下的错误会话复用。
源码级原理:握手期间的选证回调
启用多证书与自定义选择器后,上游握手的选证发生在 BoringSSL 的证书回调(cert callback)中。构造时若存在自定义选择器工厂,Envoy 会创建上游选择器并通过SSL_CTX_set_cert_cb注册回调(client_context_impl.cc):
if (add_selector) { if (auto factory = config.tlsCertificateSelectorFactory(); factory) { tls_certificate_selector_ = factory->createUpstreamTlsCertificateSelector(*this); SSL_CTX_set_cert_cb( tls_contexts_[0].ssl_ctx_.get(), [](SSL* ssl, void*) -> int { return static_cast<ClientContextImpl*>(SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))) ->selectTlsContext(ssl); }, nullptr); } }selectTlsContext(SSL*)是上游选证的核心入口(client_context_impl.cc),它从 SSL 扩展信息中读取证书选择状态机,然后调用选择器:
- 状态为
NotStarted时发起选择,调用tls_certificate_selector_->selectTlsContext(*ssl, *transport_socket_options, callback); - 选择器同步返回Success时,直接完成选证并返回 1(握手继续);
- 返回Pending(例如等待 SDS 证书异步到达)时返回 -1(握手暂停);
- 返回Failed时返回 0(握手失败)。
选择结果通过SelectionResult承载(Success/Pending/Failed三种状态),该状态机支持异步场景——证书尚未就绪时握手可以被挂起,待证书就绪后由回调唤醒恢复。这一机制正是 Envoy 内置 on-demand 选择器实现"握手期间按需拉取 SDS 证书"的基础。
证书选择器与证书映射器扩展生态
内置 on-demand 选择器
custom_tls_certificate_selector最典型的落地实现是 on-demand 证书选择器,扩展名为envoy.tls.certificate_selectors.on_demand_secret(同时注册了下游与上游两个工厂,见 config.h)。其配置结构定义于 config.proto:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
config_source | config.core.v3.ConfigSource | 是 | 证书 SDS 配置源 |
certificate_mapper | config.core.v3.TypedExtensionConfig | 是 | 计算秘密资源名的扩展函数:下游基于 ClientHello,上游基于 transport socket options 与 ServerHello |
prefetch_secret_names | repeated string | 否 | 配置加载时即开始拉取的秘密资源名列表,父资源无需等待拉取完成即可初始化 |
其工作流程为:握手期间根据对端 Hello 消息推导出秘密名 → 发起 SDS 资源请求 → 握手暂停 → 收到 SDS 响应后使用所提供证书恢复握手;若 SDS 服务器指示资源被删除,则握手失败并停止对该资源的订阅。官方文档还建议 on-demand SDS 配合DELTA_GRPC使用,以便在数据面管理秘密删除(见 secret.rst)。
内置证书映射器(certificate mappers)
certificate_mapper字段支持的扩展(位于 cert_mappers 目录):
envoy.tls.certificate_mappers.sni:以 SNI 作为 SDS 秘密名(sni/config.h);envoy.tls.certificate_mappers.static_name:使用静态名称(static_name/config.h);envoy.tls.certificate_mappers.filter_state_override:从下游过滤器链写入的 filter state 中读取动态值(filter_state_override/config.h)。
on-demand 选择器统计指标
on-demand 选择器会为下游监听器(listener.<stat_prefix>.on_demand_secret.*)与上游集群(cluster.<stat_prefix>.on_demand_secret.*)产生以下指标(见 config.h 与 secret.rst):
| 指标名 | 类型 | 说明 |
|---|---|---|
cert_requested | Counter | 新建 SDS 订阅的总次数 |
cert_updated | Counter | 证书更新总次数 |
cert_active | Gauge | 当前活跃的证书订阅与证书数 |
配置示例
示例一:上游多证书 + 自定义选择器(核心新能力)
以下UpstreamTlsContext配置利用filter_state_override映射器动态选证,展示了本次新能力的最小形态:显式定义custom_tls_certificate_selector,同时将max_session_keys设为 0 以允许携带多张证书(配置结构参考 secret.rst 的上游示例):
tls_context: common_tls_context: custom_tls_certificate_selector: name: on-demand typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_selectors.on_demand_secret.v3.Config config_source: api_config_source: api_type: DELTA_GRPC grpc_services: - envoy_grpc: cluster_name: some_xds_cluster certificate_mapper: name: filter_state_override typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_mappers.filter_state_override.v3.Config default_value: "default_secret" # 客户端上下文多证书,仅在配置了自定义选择器且 max_session_keys=0 时合法 tls_certificates: - certificate_chain: filename: /etc/envoy/certs/cert_a.pem private_key: filename: /etc/envoy/certs/key_a.pem - certificate_chain: filename: /etc/envoy/certs/cert_b.pem private_key: filename: /etc/envoy/certs/key_b.pem max_session_keys: 0要让上游的filter_state_override生效,下游过滤器链需要先写入对应的 filter state 值(secret.rst):
name: envoy.filters.network.set_filter_state typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.set_filter_state.v3.Config on_new_connection: - object_key: envoy.tls.certificate_mappers.on_demand_secret factory_key: envoy.hashable_string format_string: text_format_source: inline_string: my_secret_name shared_with_upstream: ONCE示例二:下游基于 SNI 的按需选证(对照参考)
下游场景中,custom_tls_certificate_selector配合 SNI 映射器可从 ClientHello 推导秘密名(secret.rst)。注意下游示例同时显式关闭了无状态与有状态会话恢复:
common_tls_context: custom_tls_certificate_selector: name: on-demand typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_selectors.on_demand_secret.v3.Config config_source: api_config_source: api_type: DELTA_GRPC grpc_services: - envoy_grpc: cluster_name: some_xds_cluster certificate_mapper: name: sni typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_mappers.sni.v3.SNI default_value: "default_host" prefetch_secret_names: - default_host disable_stateless_session_resumption: true disable_stateful_session_resumption: trueprefetch_secret_names用于在收到任何请求之前提前拉取常用证书,适合高频域名;静态名称映射器(envoy.tls.certificate_mappers.static_name)则适用于秘密名固定、仅需按需拉取的场景。
注意事项与限制
- 会话恢复必须禁用:这是硬性约束而非建议。由于客户端会话缓存仅以 SNI 为键,多证书 + 会话恢复可能导致为证书 A 建立的会话被本应选择证书 B 的连接复用,因此
max_session_keys必须为 0;对 on-demand 证书,官方文档明确"Session resumption is currently not supported for on-demand certificates"(secret.rst)。 - SDS 更新联动:通过 on-demand SDS 获取的证书与普通 TLS 证书一样应用父上下文的全部设置;若父 TLS 上下文发生动态更新(如验证上下文 SDS 更新),on-demand 证书上下文也会同步更新,握手恢复时使用最新版本的 CA 秘密(secret.rst)。
- xDS 协议选择:建议使用 DELTA_GRPC 管理秘密删除;使用普通 GRPC xDS 协议时,每个映射秘密的订阅会一直保持活跃,直到父资源(监听器或集群)被删除。
- 字段互斥关系:
tls_certificate_provider_instance存在时tls_certificates被忽略;tls_certificates存在时tls_certificate_sds_secret_configs被忽略(tls.proto)。 - 校验失败行为:若不满足"自定义选择器 + max_session_keys=0"而配置了多张证书,配置加载会直接报错
Client TLS context supports only a single certificate,上下文创建失败。
测试验证参考
该能力及 on-demand 选择器在上游/下游两种形态下均有集成测试覆盖:参数化的upstream_selector_标志贯穿握手、SDS 交互与统计断言(integration_test.cc),配置解析与扩展工厂注册则在 config_test.cc 中验证。如需深入源码,可继续阅读 client_context_impl.h、ssl_handshaker.cc 与 default_tls_certificate_selector.cc。
总结
本 changelog 为 Envoy 客户端 TLS 上下文补齐了与服务端对称的多证书能力:只要显式配置custom_tls_certificate_selector并将max_session_keys置 0(即禁用会话恢复),即可在CommonTlsContext.tls_certificates中声明多张证书,由选择器在握手阶段依据 ServerHello 与 transport socket options 动态选证。结合内置的 on-demand 选择器与 SNI / static_name / filter_state_override 三类证书映射器,上游侧可以按租户、按 SNI 或按下游携带的动态元数据灵活选择出站证书,为多租户网关与双向 TLS 场景提供了安全且可扩展的配置路径。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考