Envoy 证书管理实践:静态证书、SDS 动态更新与文件系统密钥轮转
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
Envoy 通过两类机制管理 TLS 证书:基于CommonTlsContext的静态引用证书,以及通过 Secret Discovery Service(SDS)下发的动态证书。本文基于 证书管理文档 与 SDS 配置文档,结合仓库源码,完整讲解证书的配置方式、三种典型配置示例、文件系统密钥轮转(含watched_directory原子替换方案)、SDS 统计指标,以及面向多租户场景的按需证书(on-demand certificates)能力。读完本文,你可以直接在 Envoy 中落地静态/动态证书方案,并理解证书轮转在数据面内的实现细节。
两种证书管理机制总览
Envoy 的证书管理在架构上分为两类,二者的核心差异在于是否会自动重新加载证书:
- 静态证书:通过
CommonTlsContext直接引用(例如tls_certificates字段内联证书或指定文件路径)。这类证书不会自动重新加载,更换证书需要重启代理,或者重新下发引用它们的 cluster/listener 配置。 - SDS 证书:通过 Secret Discovery Service 引用。证书既可以引用本地文件(当所在父目录发生 move 事件时自动重载),也可以通过外部 SDS 服务器主动推送新证书。
静态证书与热重启(hot restart)
对于静态证书,官方推荐借助热重启在不丢流量的前提下换上新证书。Envoy 的热重启由独立的重启器脚本实现,仓库中对应 hot-restarter.py,新进程与旧进程共享监听端口,完成平滑切换。适用前提是:你希望避免 SDS 基础设施(gRPC 服务器、mTLS 认证等)的复杂度,且证书轮换频率不高——此时“配置下发 + 热重启”是最简单的运维路径。
SDS 带来的运维收益
SDS 文档 明确给出了引入动机:没有 SDS 时,在 k8s 部署中证书必须创建为 Secret 并挂载进代理容器;证书过期后需要更新 Secret 并重新部署容器。而使用 SDS 后,中心 SDS 服务器把证书推送到所有 Envoy 实例,证书过期时服务器推送新证书即可,Envoy 立即启用新证书,无需重新部署。
SDS 配置要素与失败语义
SdsSecretConfig 的两种引用方式
SdsSecretConfig用于指定 secret,其name为必填字段:
- 若
sds_config字段为空,则name指向 bootstrapstatic_resources中声明的secrets(即 SDS 配置文档 中的Example one); - 否则
sds_config作为一个ConfigSource指向远程 SDS 服务器。使用远程 SDS 服务时,api_config_source必须指定grpc_service,因为 SDS 只支持 gRPC。
SdsSecretConfig被CommonTlsContext中的两个字段使用:
tls_certificate_sds_secret_configs:通过 SDS 获取TlsCertificate(服务端/客户端证书);validation_context_sds_secret_config:通过 SDS 获取CertificateValidationContext(CA 校验上下文)。
证书未就绪时的行为(重要失败语义)
SDS 文档对“取证书失败”时的行为有明确约定,这是生产排障时最需要了解的语义:
- Listener:若其服务器证书需要远程 SDS 获取,在证书取到之前不会被标记为 active,端口不会打开。如果因连接失败或响应数据非法而获取失败,listener 会被标记为 active、端口会打开,但连接到该端口的连接会被 reset。
- Upstream cluster:类似地,需要远程 SDS 客户端证书的 cluster 在证书取到前不会被标记为 active 也不会被使用;获取失败后 cluster 会被标记为 active,但路由到该 cluster 的请求会被拒绝。
- 静态 cluster 使用 SDS 时,若需要额外定义 SDS cluster(使用 Google gRPC 则不需要),该 SDS cluster 必须在引用它的静态 cluster之前定义。
SDS 连接的安全性要求
Envoy 与 SDS 服务器之间的连接必须是安全的,两种典型方案:
- 在同一主机上运行 SDS 服务器,通过Unix Domain Socket连接;
- 否则连接必须使用带认证客户端证书的TLS(mTLS)。
当前用于认证的凭证类型包括:mTLS(此时 SDS 连接所用的客户端证书必须静态配置,构成典型的“先静态引导、后动态轮转”模式)与AWS IAM SigV4。SDS 服务器需要实现 gRPC 服务SecretDiscoveryService(服务定义见 sds.proto),其交互协议与其他 xDS 服务一致。
示例一:static_resources 中声明 secrets
下面的示例展示在 bootstrapstatic_resources中声明 secret,并在 cluster 与 listener 中按名字引用:
static_resources: secrets: - name: server_cert tls_certificate: certificate_chain: filename: certs/servercert.pem private_key: filename: certs/serverkey.pem - name: client_cert tls_certificate: certificate_chain: filename: certs/clientcert.pem private_key: filename: certs/clientkey.pem - name: validation_context validation_context: trusted_ca: filename: certs/cacert.pem verify_certificate_hash: E0:F3:C8:CE:5E:2E:A3:05:F0:70:1F:F5:12:E3:6E:2E:97:92:82:84:A2:28:BC:F7:73:32:D3:39:30:A1:B6:FD clusters: - connect_timeout: 0.25s load_assignment: cluster_name: local_service_tls ... transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext common_tls_context: tls_certificate_sds_secret_configs: - name: client_cert listeners: .... filter_chains: transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext common_tls_context: tls_certificate_sds_secret_configs: - name: server_cert validation_context_sds_secret_config: name: validation_context要点解析:
secrets静态资源中声明了 3 个 secret:client_cert、server_cert、validation_context。此处证书不是远程获取的,而是引用本地文件;verify_certificate_hash展示了如何固定校验特定 CA 证书的哈希值。- cluster 配置在
tls_certificate_sds_secret_configs中按名字引用client_cert作为 mTLS 客户端证书。 - listener 的
DownstreamTlsContext在tls_certificate_sds_secret_configs中引用server_cert,并在validation_context_sds_secret_config中引用validation_context(例如用于对客户端证书做 CA 校验)。 - 注意:虽然名为 “sds_secret_configs”,但引用 bootstrap 内静态 secret 时并不涉及网络传输,本质是按名字间接引用,方便 listener/cluster 复用同一份证书定义。
示例二:从远程 SDS 服务器获取证书
完整可运行的示例配置见 sds-source-example.yaml,其核心结构如下:
static_resources: listeners: - name: listener_0 address: socket_address: address: 0.0.0.0 port_value: 8000 filter_chains: - transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext common_tls_context: tls_certificate_sds_secret_configs: - name: server_cert sds_config: api_config_source: api_type: GRPC grpc_services: - envoy_grpc: cluster_name: sds_server_mtls validation_context_sds_secret_config: name: validation_context sds_config: api_config_source: api_type: GRPC grpc_services: - envoy_grpc: cluster_name: sds_server_uds clusters: - name: sds_server_mtls typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} load_assignment: cluster_name: sds_server_mtls endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 8234 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext common_tls_context: tls_certificates: - certificate_chain: filename: certs/servercert.pem private_key: filename: certs/serverkey.pem - name: sds_server_uds typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} load_assignment: cluster_name: sds_server_uds endpoints: - lb_endpoints: - endpoint: address: pipe: path: /tmp/uds_path - name: example_cluster load_assignment: cluster_name: local_service_tls endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 8443 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext common_tls_context: tls_certificate_sds_secret_configs: - name: client_cert sds_config: api_config_source: api_type: GRPC grpc_services: - google_grpc: target_uri: unix:/tmp/uds_path stat_prefix: sds_uds_server这个示例展示了三种访问 SDS 服务器的方式,SDS 服务器提供client_cert、server_cert、validation_context三个 secret:
| 用法 | 传输通道 | 认证方式 | 说明 |
|---|---|---|---|
clusterexample_cluster的client_cert | google_grpc+ UDSunix:/tmp/uds_path | UDS(本机) | 直接指定target_uri,不需要 Envoy cluster |
| listener 的server_cert | envoy_grpc+ clustersds_server_mtls(127.0.0.1:8234) | mTLS | sds_server_mtls用静态配置的客户端证书(certs/servercert.pem/serverkey.pem)与 SDS 服务器做 mTLS,体现“引导凭证必须静态”的原则 |
| listener 的validation_context | envoy_grpc+ clustersds_server_uds(/tmp/uds_path) | UDS | 通过本地 Unix Domain Socket 连接 |
两个细节值得注意:envoy_grpc通道要求 cluster 显式启用 HTTP/2(示例中通过typed_extension_protocol_options的HttpProtocolOptions设置http2_protocol_options: {});而google_grpc通道绕过 Envoy cluster,由底层 gRPC 客户端直连目标 URI,因此 SDS 文档才说“使用 Google gRPC 时不需要定义 SDS cluster”。
密钥轮转(Key Rotation)与文件系统监视
原理:inotify 监视父目录的 move 事件
当 gRPC SDS 不可行或不期望时(例如轮转 SDS 自身引导凭证的场景),SDS 支持对引用了文件系统路径的 secret 做文件系统轮转。目前支持两种 secret 类型:TlsCertificate与CertificateValidationContext。
默认行为是:包含 secret 的目录会被监视文件系统 move 事件。例如/foo/bar/baz/cert.pem会监视其父目录/foo/bar/baz。通过TlsCertificate.watched_directory与CertificateValidationContext.watched_directory字段可以显式控制被监视的目录,允许把监视点上移到路径的祖先目录(如/foo/bar),这对实现通用的密钥轮转方案非常有用。
从源码结构可以印证这一机制:SdsApi在装载 secret 时,若tls_certificate带有watched_directory,会创建Config::WatchedDirectory并注册回调(见 sds_api.cc 中secret.tls_certificate().has_watched_directory()分支,validation_context侧逻辑类似,见 sds_api.cc)。
一个重要的实现细节在 sds_api.cc 的onWatchUpdate()中:文件系统事件触发后会重新读取全部文件内容并计算哈希,只有哈希变化才触发更新;而且为了防止“读取过程中恰好发生轮转”导致读到新旧混合状态,代码会用最多 5 次有界重试反复加载文件直到两次哈希一致,若仍不一致会打印Unable to atomically refresh secrets due to > 5 non-atomic rotations observed的告警;若解析失败则记录告警并递增key_rotation_failed计数器。这解释了为什么官方推荐用原子 move 而非直接改写文件来替换证书。
符号链接轮转方案(watched_directory 实战)
一个改进原子性的常见轮转方案是:维护活跃符号链接/certs/current,用原子 move 操作替换该符号链接。此时监视点需要建在证书的祖父目录上,Envoy 通过watched_directory支持这种方案:
resources: - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" name: tls_sds tls_certificate: certificate_chain: filename: /certs/current/sds_cert.pem private_key: filename: /certs/current/sds_key.pem watched_directory: path: /certsresources: - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" name: validation_context_sds validation_context: trusted_ca: filename: /certs/current/cacert.pem watched_directory: path: /certs对应的证书轮转命令只需两步:
ln -s <path to new secrets> /certs/new && mv -Tf /certs/new /certs/currentmv -Tf对符号链接本身做原子替换,/certs目录上收到的 move 事件即触发 Envoy 重载,全程无需重启、无需重新下发配置。
xDS gRPC 连接的证书轮转(解决自举问题)
Envoy 与 xDS 服务器之间的 gRPC 连接自身的证书管理存在自举问题:SDS 服务器无法管理“连接它自己所需的证书”。SDS 文档 给出的方案是从文件系统引导 xDS 连接凭证——证书与密钥文件通过 inotify 监视并自动重载,无需重启;相比之下,Example two中用远程 SDS 下发的 xDS 凭证在更新后需要重启才能重载。
cluster 侧配置(path_config_source指向本地 SDS 配置文件):
clusters: - name: control_plane type: LOGICAL_DNS connect_timeout: 1s load_assignment: cluster_name: control_plane endpoints: - lb_endpoints: - endpoint: address: socket_address: address: controlplane port_value: 8443 typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} transport_socket: name: "envoy.transport_sockets.tls" typed_config: "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext" common_tls_context: tls_certificate_sds_secret_configs: name: tls_sds sds_config: path_config_source: path: /etc/envoy/tls_certificate_sds_secret.yaml validation_context_sds_secret_config: name: validation_context_sds sds_config: path_config_source: path: /etc/envoy/validation_context_sds_secret.yaml/etc/envoy/tls_certificate_sds_secret.yaml(客户端证书链与私钥路径):
resources: - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" name: tls_sds tls_certificate: certificate_chain: filename: /certs/sds_cert.pem private_key: filename: /certs/sds_key.pem/etc/envoy/validation_context_sds_secret.yaml(用于校验 xDS 服务器证书的 CA 包路径):
resources: - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" name: validation_context_sds validation_context: trusted_ca: filename: /certs/cacert.pem在上例中监视点建立在/certs,该目录内的文件移动会触发更新;若改用前文/certs/current符号链接方案,则配合watched_directory: /certs实现更高原子性的轮转。
可观测性:SDS 相关统计指标
SSL socket factory 计数器
SSL socket factory 输出以下 SDS 相关统计,均为 counter 类型:
Downstream listener(命名空间listener.<LISTENER_IP>.server_ssl_socket_factory.*):
| 名称 | 说明 |
|---|---|
| ssl_context_update_by_sds | SSL 上下文被更新的总次数 |
| downstream_context_secrets_not_ready | 因 SSL 证书为空而被 reset 的下游连接数 |
Upstream cluster(命名空间cluster.<CLUSTER_NAME>.client_ssl_socket_factory.*):
| 名称 | 说明 |
|---|---|
| ssl_context_update_by_sds | SSL 上下文被更新的总次数 |
| upstream_context_secrets_not_ready | 因 SSL 证书为空而被 reset 的上游连接数 |
在源码中,这些计数器定义于 ssl_socket.h 的统计宏中;当 SDS 更新触达 socket 时分别在 server_ssl_socket.cc 与 client_ssl_socket.cc 处递增,与上文失败语义(连接被 reset)一一对应。
SDS 订阅统计
每个 SDS 订阅在sds.<SECRET_NAME>.*命名空间下维护标准 xDS 订阅统计树(根在sds.,通过XDS_RESOURCE_NAME标签区分,见 sds_api.cc 中stats.createScopeWithTaggedName("sds", ...)的实现),此外还追踪:
| 名称 | 说明 |
|---|---|
| key_rotation_failed | 文件系统密钥轮转在 SDS 更新之外失败的总次数 |
排障经验:若 SDS 推送正常但证书行为异常,先看sds.<SECRET_NAME>.*下的版本与更新计数;若文件系统轮转失败,key_rotation_failed会增长,并伴随日志Failed to reload certificates: <原因>(对应 sds_api.cc 中捕获EnvoyException的分支)。
按需证书(On-demand Certificates)
默认情况下,SDS 证书获取会阻塞引用它的 listener 与 cluster 的初始化。在某些场景(尤其是多租户部署中,单个 listener 或上游 cluster 需要向对端呈现多种证书)更合理的做法是:先接受连接,再根据对端 hello 消息中的字段(如 SNI)按需请求证书。Envoy 提供envoy.tls.certificate_selectors.on_demand_secret扩展(实现位于 cert_selectors 目录):暂停 TLS 握手,对缺失的证书发起 SDS 请求,收到响应后继续握手。
关键行为:
- 按需获取的证书配置方式与普通 TLS 证书相同,例如父上下文的所有设置都会应用;若父 TLS 上下文发生动态更新(如 validation context 的 SDS 更新),按需证书上下文也会同步更新,因此握手恢复时使用的是最新版本的 CA secret。
- 建议配合DELTA_GRPC使用:xDS 响应中的资源删除会取消数据面对该 secret 名的订阅;而普通 GRPC xDS 协议下,每个已映射 secret 的订阅会一直活跃,直到父资源(listener 或 cluster)被删除。
- 限制:按需证书目前不支持 session resumption。
相关统计:downstream 在listener.<stat_prefix>.on_demand_secret.*命名空间,upstream 在cluster.<stat_prefix>.on_demand_secret.*:
| 名称 | 类型 | 说明 |
|---|---|---|
| cert_requested | Counter | 新建 SDS 订阅总数 |
| cert_updated | Counter | 证书更新总数 |
| cert_active | Gauge | 活跃的证书订阅与证书数 |
配置示例
使用 SNI 字段作为 SDS 请求中的 secret 名的downstreamTLS 上下文(snimapper 带default_value,并预取默认证书):
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" # Starts fetching the secret prior to any requests. prefetch_secret_names: - default_host disable_stateless_session_resumption: true disable_stateful_session_resumption: true与常规 SDS 证书配置等效、但不阻塞listener 启动的 downstream 配置:连接会被接受并在 TLS 握手期间暂停,证书到达后再恢复。此例使用static_namemapper 固定 secret 名:
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: static_name typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_mappers.static_name.v3.StaticName name: secret_0 prefetch_secret_names: - secret_0 disable_stateless_session_resumption: true disable_stateful_session_resumption: trueupstream侧可以使用从下游 listener 传递的动态 filter state 值作为 secret 名(filter_state_overridemapper):
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"上述 upstream 的 filter state override 要生效,需要在下游 filter chain 中写入对应值,例如:
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注意两个示例中的disable_stateless_session_resumption/disable_stateful_session_resumption:因为按需证书不支持 session resumption,配置时应显式关闭会话恢复。
小结:如何选择证书管理方案
| 场景 | 推荐方案 |
|---|---|
| 证书轮换低频、可接受短暂重启 | 静态CommonTlsContext+ 热重启 |
| 需要不重新部署更新证书 | 远程 gRPC SDS(mTLS 或 UDS 保护通道) |
| 轮转 SDS 引导凭证本身、无法用 gRPC SDS | 文件系统 SDS +watched_directory符号链接原子轮转 |
| 多租户、单 listener/cluster 呈现大量证书 | on-demand certificate selector + DELTA_GRPC |
所有示例中的字段语义可进一步对照 SDS 配置文档、完整示例 sds-source-example.yaml,以及 SDS 客户端实现 source/common/secret/sds_api.cc 与其订阅管理层 secret_manager_impl.cc;TLS 上下文的构建逻辑位于 source/extensions/transport_sockets/tls/ 下的upstream_config与downstream_config实现中。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考