Prhub

#45890 [Rust Frontend] Add static HTTPS and mTLS support for HTTP and gRPC

原始 PR 作者 tahsintunan 合并时间 2026-06-30 09:46 文件变更 16 提交数 22 评论 15 代码增减 +1942 / -125

执行摘要

Rust 前端添加静态 HTTPS 与 mTLS 支持

Rust 前端需要具备与 Python 前端同等级的 SSL 支持,包括 HTTPS 和 mTLS,以适配生产部署中的加密与身份验证需求。同时修复 gRPC 服务默认暴露在所有网络接口的安全问题。参见 tracking issue #44280。

值得精读 tls.rs 中的 build_server_builder 函数,它展示了如何在 Rust 中安全配置 OpenSSL(证书链、私钥验证、mTLS、密码套件)。同时关注 listener.rs 中通过 tls-listener 透明添加 TLS 层的设计模式,对类似项目有参考价值。讨论中关于 TLS 库选择的决策(OpenSSL vs native-tls vs rustls)值得仔细理解。

讨论亮点
  • TLS 库选择讨论:BugenZhao 询问为何使用 rustls(之前刻意避免)。作者回复因 native-tls 不支持服务器端 mTLS,而 rustls 支持。后作者改用 OpenSSL 以复用已有依赖。结论:采用 OpenSSL,不新增 rustls。

  • 使用 tls-listener crate 与 native-tls 后端:BugenZhao 建议使用 tls-listener crate 简化实现,并考虑 native-tls。作者解释 native-tls 不支持 mTLS,故保留 OpenSSL。结论:使用 tls-listener 但底层用 OpenSSL。

  • gRPC 共享 TLS 配置与超时:njhill 建议 gRPC 复用 HTTP 的 TLS 配置,并添加 TLS 握手超时。作者实现并回复。结论:gRPC 与 HTTP 共享 TLS 配置,添加 60s 握手超时和 keep-alive 超时。

  • 自动安全扫描提醒:depthfirst-app 提醒未显式设置最小协议版本。作者回复 mozilla_intermediate_v5 隐式要求 TLS 1.2。结论:无需修改,接受当前配置。

实现拆解

  1. 新增 tls 模块rust/src/server/src/tls.rs):基于 OpenSSL 构建 SslAcceptorBuilder,支持证书链加载、私钥加载(支持合并 PEM)、mTLS 客户端证书验证(--ssl-cert-reqs)、自定义密码套件(--ssl-ciphers)。默认使用 mozilla_intermediate_v5 基线,高于 Python 前端默认的 AEAD-only 策略。

  2. 统一 Listener 层rust/src/server/src/listener.rs):重构 Listener 类型,引入 ListenerIo 枚举(TCP/Unix),集成 tls-listener crate 以透明地在 accept 连接后执行 TLS 握手。新增 grpc_bind_host 函数确保 gRPC 在非 TCP 绑定模式下默认使用 loopback。

  3. 核心服务整合rust/src/server/src/lib.rs):在 serve_with_router_extension 中,先构建 TLS 配置(若提供),然后创建 TLS 包装的 Listener。HTTP 和 gRPC 共享同一 TLS 配置。gRPC 服务通过 tls_incoming 函数接收已做 TLS 的连接。添加了 TLS 握手超时(60s)和 HTTP/2 keepalive 常量。

  4. gRPC 模块扩展rust/src/server/src/grpc/mod.rs):新增 GrpcTlsStream 包装器实现 tower::Service,使 gRPC 服务能直接使用 TLS 连接。修改 incoming 函数返回 ListenerIo 类型,并新增 tls_incoming 函数进行 TLS 握手。

  5. CLI 与配置rust/src/cmd/src/cli.rsrust/src/server/src/config.rs):添加 --ssl-certfile--ssl-keyfile--ssl-ca-certs--ssl-cert-reqs--ssl-ciphers 参数。新增 TlsConfig 结构体及其验证逻辑(如 keyfile 缺少 certfile 时报错、mTLS 必须提供 CA)。Keep-alive 超时也通过环境变量 VLLM_HTTP_TIMEOUT_KEEP_ALIVE 暴露。

  6. 测试配套:新增 tls_tests.rs 端到端测试,使用自签名 CA 和证书验证 TLS 握手与 mTLS;扩展 grpc/tests.rs 增加 TLS 测试用例;cli/tests.rs 增加配置参数验证。

文件 模块 状态 重要度
rust/src/server/src/tls.rs TLS 模块 added 8.6
rust/src/server/src/lib.rs 服务入口 modified 8.64
rust/src/server/src/listener.rs 监听器层 modified 8.37
rust/src/server/src/config.rs 配置层 modified 7.03
rust/src/cmd/src/cli.rs CLI 参数 modified 7.46
rust/src/server/src/grpc/mod.rs gRPC 扩展 modified 7.97

关键符号

build_server_builder configure_client_auth grpc_bind_host enable_tcp_nodelay validate tls_incoming

关键源码片段

rust/src/server/src/tls.rs core-logic

核心 TLS 配置模块,负责构建 OpenSSL SslAcceptor,支持证书链、私钥、mTLS 和密码套件。

//! OpenSSL server-config construction for TLS termination.
//! Builds an SslContext from uvicorn-style ssl_* arguments.use std::path::Path;
use std::time::Duration;
use anyhow::{Context as _, Result};
use openssl::ssl::{
    AlpnError, SslAcceptor, SslAcceptorBuilder, SslContext, SslContextBuilder,
    SslFiletype, SslMethod, SslOptions, SslVerifyMode, select_next_proto,
};
use crate::config::TlsConfig;/// Time a client has to complete the TLS handshake before connection is dropped.
pub(crate) const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(60);/// ALPN wire bytes for HTTP/2 (length-prefixed).
const ALPN_H2: &[u8] = b"\x02h2";/// Build the shared OpenSSL acceptor from validated TlsConfig.
/// Uses Mozilla intermediate v5 baseline (forward-secret AEAD, TLS 1.2+).
fn build_server_builder(tls: &TlsConfig) -> Result<SslAcceptorBuilder> {
    let cert_file = tls.cert_file.as_deref()
        .context("--ssl-certfile is required to enable TLS")?;    let mut builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server())
        .context("failed to initialize TLS")?;
    builder.set_options(SslOptions::CIPHER_SERVER_PREFERENCE);    // Load the full certificate chain (leaf + intermediates)
    ensure_exists(cert_file, "--ssl-certfile")?;
    builder.set_certificate_chain_file(cert_file)
        .with_context(|| format!("failed to parse certificate chain in --ssl-certfile {cert_file:?}"))?;    // When key_file is unset, read key from the certificate file (combined PEM)
    let key_file = tls.key_file.as_deref().unwrap_or(cert_file);
    ensure_exists(key_file, "private key file")?;
    builder.set_private_key_file(key_file, SslFiletype::PEM)
        .with_context(|| format!("failed to parse private key in {key_file:?}"))?;
    builder.check_private_key()
        .context("the certificate and private key do not match")?;    configure_client_auth(&mut builder, tls)?;    if let Some(ciphers) = tls.ciphers.as_deref().filter(|c| !c.is_empty()) {
        builder.set_cipher_list(ciphers)
            .with_context(|| format!("invalid --ssl-ciphers {ciphers:?}"))?;
    }    Ok(builder)
}/// Configure client certificate verification for mutual TLS.
fn configure_client_auth(builder: &mut SslAcceptorBuilder, tls: &TlsConfig) -> Result<()> {
    if tls.cert_reqs > 0 {
        // ca_certs is required when cert_reqs > 0
        let ca_file = tls.ca_certs.as_deref()
            .context("--ssl-ca-certs is required when --ssl-cert-reqs > 0")?;
        ensure_exists(ca_file, "--ssl-ca-certs")?;
        builder.set_ca_file(ca_file)
            .context("failed to load --ssl-ca-certs")?;
        let verify_mode = if tls.cert_reqs == 1 {
            SslVerifyMode::PEER // optional client cert
        } else {
            SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT // required client cert
        };
        builder.set_verify(verify_mode);
    }
    Ok(())
}/// Build the HTTP SslContext (without ALPN, matching uvicorn).
pub(crate) fn build_server_config(tls: &TlsConfig) -> Result<SslContext> {
    let builder = build_server_builder(tls)?;
    Ok(builder.build())
}
rust/src/server/src/lib.rs core-logic

服务主入口,集成 TLS listener 构建与 gRPC 共享配置,添加超时常量。

/// Choose the gRPC listener host. It follows the HTTP TCP host when there is
/// one; otherwise (unix socket or inherited fd) it defaults to IPv4 loopback
/// rather than all interfaces, so the side-car is never accidentally
/// network-exposed.
fn grpc_bind_host(listener_mode: &HttpListenerMode) -> &str {
    match listener_mode {
        HttpListenerMode::BindTcp { host, .. } => host.as_str(),
        HttpListenerMode::BindUnix { .. } | HttpListenerMode::InheritedFd { .. } => "127.0.0.1",
    }
}// In serve_with_router_extension:
// Build TLS config upfront, so bad cert/key fail fast before engine handshake.
let tls_config = config.tls.as_ref().map(tls::build_server_config)
    .transpose().context("failed to build TLS config")?;
let tls_grpc_config = config.tls.as_ref().map(tls::build_grpc_server_config)
    .transpose().context("failed to build gRPC TLS config")?;// Bind the HTTP listener (possibly with TLS wrapping)
let listener = if let Some(ref tls_cfg) = tls_config {
    let acceptor = tls_cfg.clone().into_acceptor()?; // simplified
    Listener::bind_tls(&config.listener_mode, acceptor).await?
} else {
    Listener::bind(&config.listener_mode).await?
};
rust/src/server/src/listener.rs core-logic

引入统一的 ListenerIo 类型,集成 tls-listener crate 实现透明 TLS 握手。

/// Runtime listener I/O type which is either a TCP stream or a Unix-domain stream.
#[derive(Debug)]
#[enum_derive(tokio1::AsyncRead, tokio1::AsyncWrite)]
pub enum ListenerIo {
    Tcp(TcpStream),
    Unix(UnixStream),
}/// Runtime listener address type.
#[derive(Debug)]
pub enum ListenerAddr {
    Tcp(SocketAddr),
    Unix(tokio::net::unix::SocketAddr),
}impl Connected for ListenerIo {
    type ConnectInfo = TcpConnectInfo;    fn connect_info(&self) -> TcpConnectInfo {
        match self {
            Self::Tcp(stream) => stream.connect_info(),
            Self::Unix(_) => TcpConnectInfo {
                local_addr: None,
                remote_addr: None,
            },
        }
    }
}/// Attempt to set TCP_NODELAY on the accepted TCP stream.
fn enable_tcp_nodelay(stream: TcpStream) -> TcpStream {
    if let Err(err) = stream.set_nodelay(true) {
        tracing::trace!(error = %err, "failed to enable TCP_NODELAY on accepted TCP connection");
    }
    stream
}

评论区精华

TLS 库选择:rustls vs OpenSSL 设计

BugenZhao 询问为何使用 rustls(之前刻意避免)。作者回复因 native-tls 不支持服务器端 mTLS,而 rustls 支持。后作者改用 OpenSSL 以复用已有依赖。

结论:采用 OpenSSL,不新增 rustls。 · 已解决

使用 tls-listener crate 与 native-tls 后端 设计

BugenZhao 建议使用 tls-listener crate 简化实现,并考虑 native-tls。作者解释 native-tls 不支持 mTLS,故保留 OpenSSL。

结论:使用 tls-listener 但底层用 OpenSSL。 · 已解决

gRPC 共享 TLS 配置与超时 正确性

njhill 建议 gRPC 复用 HTTP 的 TLS 配置,并添加 TLS 握手超时。作者实现并回复。

结论:gRPC 与 HTTP 共享 TLS 配置,添加 60s 握手超时和 keep-alive 超时。 · 已解决

自动安全扫描:设置最小 TLS 版本 安全

depthfirst-app 提醒未显式设置最小协议版本。作者回复 mozilla_intermediate_v5 隐式要求 TLS 1.2。

结论:无需修改,接受当前配置。 · 已解决

风险与影响

  • 核心路径变更:TLS 模块插入了连接处理流程,可能影响非 TLS 路径稳定。
  • 安全配置复杂度:新增的 TLS 配置项(证书路径、mTLS 模式)如果错误配置可能导致启动失败或安全弱化,但 PR 实现了严格的启动验证。
  • 默认地址变更:gRPC 绑定地址从 0.0.0.0 改为 127.0.0.1,可能破坏依赖外部访问 gRPC 的部署,用户需显式设置 --host
  • 超时变化:默认 5s keep-alive 超时可能断开长时间空闲连接(如 streaming 请求),用户可通过 VLLM_HTTP_TIMEOUT_KEEP_ALIVE=0 禁用。
  • 密码套件收紧:默认密码套件更严格(AEAD-only),旧客户端可能需要升级或使用 --ssl-ciphers 放宽。

用户:可以通过 --ssl-* 参数直接启用 HTTPS 和 mTLS,无需额外反向代理;gRPC 服务也获得加密能力。系统:Rust 前端与 Python 前端功能对齐,但默认密码套件更严格,可能影响老旧客户端兼容性。gRPC 默认 loopback 提升安全性。团队:需维护 OpenSSL 依赖和 TLS 配置验证逻辑;后续计划支持证书热重载(见 #47362)。

核心路径变更 安全配置 默认地址变更 超时变化

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论