土法炼钢兴趣小组的算法知识备份

【网络工程】反向代理模式:TLS 终止、透传与重加密

文章导航

分类入口
network
标签入口
#reverse-proxy#tls#nginx#haproxy#envoy#sni

目录

你在反向代理上配置 HTTPS,看似简单——ssl_certificate 一加就完事。但当你开始面对这些问题时,才发现 TLS 在代理层的处理方式直接影响架构的安全性、性能和运维复杂度:

这些问题的答案取决于你选择哪种 TLS 处理模式。本文系统解剖反向代理的三种 TLS 模式——终止(Termination)、透传(Passthrough)与重加密(Re-encryption),从架构原理到生产配置,给出工程选型的完整依据。

一、三种模式的架构对比

反向代理处理 TLS 流量有三种基本模式,每种模式的加解密边界不同,决定了安全性、性能和运维复杂度的权衡。

1.1 TLS 终止(TLS Termination)

代理层解密 TLS,以明文 HTTP 转发给后端:

客户端 ──TLS──→ [代理层] ──HTTP──→ 后端服务
         加密         明文

代理层持有证书和私钥,负责 TLS 握手。后端服务只处理 HTTP 明文请求,不需要配置证书。

核心特征: - 代理层可以检查 HTTP 内容(路由、缓存、WAF、日志) - 后端不需要 TLS 配置,运维简单 - 代理层到后端的链路是明文,依赖网络隔离保证安全

1.2 TLS 透传(TLS Passthrough)

代理层不解密 TLS,直接将加密流量转发给后端:

客户端 ──TLS──────────────────────→ 后端服务
         代理层只做 TCP 转发

代理层不持有证书,也不参与 TLS 握手。它只根据 TCP 连接信息(通常是 SNI)将流量路由到正确的后端。

核心特征: - 端到端加密,代理层看不到明文 - 代理层无法做 HTTP 层路由、缓存或 WAF - 后端服务必须配置证书和处理 TLS 握手 - 对合规要求(如数据不能在中间节点解密)的场景有价值

1.3 TLS 重加密(Re-encryption / TLS Bridging)

代理层解密 TLS,检查或修改内容后,再用新的 TLS 连接转发给后端:

客户端 ──TLS──→ [代理层] ──TLS──→ 后端服务
         加密        重新加密

代理层持有前端证书用于接收客户端连接,同时作为客户端与后端建立独立的 TLS 连接。

核心特征: - 代理层可以检查 HTTP 内容(与终止模式相同) - 代理层到后端的链路也是加密的 - 需要管理两套证书(前端 + 后端) - CPU 开销最大(两次 TLS 握手)

1.4 三种模式对比

维度 TLS 终止 TLS 透传 TLS 重加密
代理层解密
后端链路加密 是(端到端)
HTTP 层路由 支持 不支持 支持
WAF/缓存 支持 不支持 支持
后端证书管理 不需要 需要 需要
CPU 开销 低(代理层)
适用场景 大多数 Web 服务 合规/端到端加密 安全敏感+需要 L7

二、TLS 终止:最常见的部署模式

TLS 终止是最广泛使用的模式。代理层(Nginx、HAProxy、Envoy、云 LB)承担 TLS 握手,后端服务只处理明文 HTTP。

2.1 Nginx TLS 终止配置

# /etc/nginx/conf.d/app.conf
server {
    listen 443 ssl http2;
    server_name app.example.com;

    # 证书配置
    ssl_certificate     /etc/nginx/certs/app.example.com.crt;
    ssl_certificate_key /etc/nginx/certs/app.example.com.key;

    # TLS 协议与密码套件
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # 会话复用
    ssl_session_cache shared:TLS:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;  # TLS 1.3 下使用 PSK

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/certs/chain.pem;
    resolver 8.8.8.8 8.8.4.4 valid=300s;

    # 安全头
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # 转发到后端(明文 HTTP)
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# HTTP → HTTPS 重定向
server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

upstream backend {
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    keepalive 64;
}

2.2 HAProxy TLS 终止配置

# /etc/haproxy/haproxy.cfg
global
    # SSL/TLS 全局设置
    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11
    tune.ssl.default-dh-param 2048

frontend https_front
    bind *:443 ssl crt /etc/haproxy/certs/app.example.com.pem alpn h2,http/1.1
    bind *:80
    
    # HTTP → HTTPS 重定向
    http-request redirect scheme https unless { ssl_fc }
    
    # HSTS
    http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    
    # 传递客户端信息
    http-request set-header X-Forwarded-Proto %[ssl_fc,iif(https,http)]
    http-request set-header X-Real-IP %[src]
    
    default_backend app_servers

backend app_servers
    balance roundrobin
    option httpchk GET /health
    server app1 10.0.1.10:8080 check
    server app2 10.0.1.11:8080 check

2.3 传递客户端 TLS 信息

TLS 终止后,后端服务看不到原始的 TLS 连接信息。代理层需要通过 HTTP 头传递关键信息:

# Nginx 传递 TLS 信息到后端
proxy_set_header X-Forwarded-Proto  $scheme;
proxy_set_header X-SSL-Protocol     $ssl_protocol;
proxy_set_header X-SSL-Cipher       $ssl_cipher;

# 传递客户端证书信息(mTLS 场景)
proxy_set_header X-SSL-Client-Cert   $ssl_client_escaped_cert;
proxy_set_header X-SSL-Client-DN     $ssl_client_s_dn;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;

注意安全风险:这些头可以被客户端伪造。必须确保代理层清除客户端发送的同名头:

# 先清除客户端可能伪造的头
proxy_set_header X-SSL-Client-Cert   "";
proxy_set_header X-SSL-Client-DN     "";
proxy_set_header X-SSL-Client-Verify "";

# 然后设置真实值
set $ssl_client_dn_real $ssl_client_s_dn;
proxy_set_header X-SSL-Client-DN $ssl_client_dn_real;

2.4 终止模式的安全考量

TLS 终止后,代理层到后端的链路是明文。需要额外措施保证安全:

网络隔离

# 后端服务只监听内网接口
# 应用配置
bind_address = "10.0.1.10"  # 不要用 0.0.0.0

# iptables 限制只允许代理层访问
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP

Kubernetes 中的 NetworkPolicy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-ingress-only
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: ingress-nginx
      ports:
        - port: 8080

三、TLS 透传:端到端加密

TLS 透传模式下,代理层不解密流量,只做 TCP 层转发。代理层通过 TLS ClientHello 中的 SNI(Server Name Indication)字段判断目标后端。

3.1 SNI 的工作原理

SNI 是 TLS 扩展(RFC 6066),客户端在 ClientHello 消息中携带目标服务器的域名。由于 ClientHello 是明文发送的,代理层可以在不解密 TLS 的情况下读取 SNI:

ClientHello:
  Version: TLS 1.2
  Random: ...
  Session ID: ...
  Cipher Suites: ...
  Extensions:
    server_name: app.example.com   ← 代理层读取这个字段
    ...

代理层根据 SNI 值将 TCP 连接转发到对应的后端。

3.2 Nginx Stream 模块透传

Nginx 的 stream 模块(非 http 模块)支持 TLS 透传:

# /etc/nginx/nginx.conf
stream {
    # 根据 SNI 路由的 map
    map $ssl_preread_server_name $backend_pool {
        app1.example.com  backend_app1;
        app2.example.com  backend_app2;
        default           backend_default;
    }

    # 后端服务器组
    upstream backend_app1 {
        server 10.0.1.10:443;
        server 10.0.1.11:443;
    }

    upstream backend_app2 {
        server 10.0.2.10:443;
        server 10.0.2.11:443;
    }

    upstream backend_default {
        server 10.0.3.10:443;
    }

    server {
        listen 443;
        
        # 启用 SNI 预读
        ssl_preread on;
        
        # 根据 SNI 选择后端
        proxy_pass $backend_pool;
        
        # 透传设置
        proxy_connect_timeout 5s;
        proxy_timeout 300s;
    }
}

关键点:ssl_preread on 让 Nginx 读取 ClientHello 中的 SNI,但不进行 TLS 解密。

3.3 HAProxy TCP 模式透传

# /etc/haproxy/haproxy.cfg
frontend tls_passthrough
    bind *:443
    mode tcp
    
    # 等待 ClientHello 以读取 SNI
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }
    
    # 根据 SNI 路由
    use_backend app1_backend if { req_ssl_sni -i app1.example.com }
    use_backend app2_backend if { req_ssl_sni -i app2.example.com }
    
    # 支持 SNI 通配符匹配
    use_backend wildcard_backend if { req_ssl_sni -m end .internal.example.com }
    
    default_backend default_backend

backend app1_backend
    mode tcp
    server app1-a 10.0.1.10:443 check
    server app1-b 10.0.1.11:443 check

backend app2_backend
    mode tcp
    server app2-a 10.0.2.10:443 check
    server app2-b 10.0.2.11:443 check

backend default_backend
    mode tcp
    server default 10.0.3.10:443 check

3.4 Envoy TLS 透传

# envoy-passthrough.yaml
static_resources:
  listeners:
    - name: tls_passthrough
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 443
      listener_filters:
        - name: envoy.filters.listener.tls_inspector
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
      filter_chains:
        - filter_chain_match:
            server_names:
              - "app1.example.com"
          filters:
            - name: envoy.filters.network.tcp_proxy
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
                stat_prefix: app1
                cluster: app1_cluster
        - filter_chain_match:
            server_names:
              - "app2.example.com"
          filters:
            - name: envoy.filters.network.tcp_proxy
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
                stat_prefix: app2
                cluster: app2_cluster
  clusters:
    - name: app1_cluster
      connect_timeout: 5s
      type: STRICT_DNS
      load_assignment:
        cluster_name: app1_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: app1.internal
                      port_value: 443
    - name: app2_cluster
      connect_timeout: 5s
      type: STRICT_DNS
      load_assignment:
        cluster_name: app2_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: app2.internal
                      port_value: 443

Envoy 的 tls_inspector Listener Filter 读取 ClientHello 中的 SNI 和 ALPN,用于 Filter Chain 匹配。

3.5 透传模式的限制

透传模式的限制是本质性的——代理层看不到 HTTP 内容:

无法做的事情:
├── HTTP 路由(基于 Path / Header / Cookie)
├── HTTP 缓存
├── HTTP 压缩
├── WAF / 内容检查
├── 速率限制(基于 HTTP 请求)
├── 请求/响应修改(Header 注入等)
├── HTTP 级别的日志和指标
└── HTTP/2 多路复用感知的负载均衡

能做的事情仅限于 TCP 层:

可以做的事情:
├── SNI 路由
├── 基于源 IP 的 ACL
├── TCP 层连接限制
├── TCP 层健康检查
└── 连接级别的负载均衡

3.6 ECH 对 SNI 路由的影响

TLS 1.3 的 Encrypted Client Hello(ECH,前身 ESNI)扩展会加密 SNI 字段,这将直接影响 TLS 透传模式的 SNI 路由能力:

传统 ClientHello:
  server_name: app.example.com  ← 明文,代理可读

ECH ClientHello:
  encrypted_client_hello: <加密数据>  ← 代理无法读取
  server_name: public.example.com     ← Outer SNI(通常是 CDN 域名)

ECH 大规模部署后,基于 SNI 的透传路由将面临挑战。可能的应对方案:

  1. 使用 Outer SNI 路由到共享前端,前端解密后做 L7 路由
  2. 改用 TLS 终止模式
  3. 使用其他标识(如目标 IP)进行路由

四、TLS 重加密:L7 可见性 + 全链路加密

重加密模式结合了终止和透传的优点:代理层解密以获得 HTTP 层可见性,然后重新加密转发给后端。

4.1 Nginx 重加密配置

# 前端:接收客户端 TLS
server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate     /etc/nginx/certs/frontend.crt;
    ssl_certificate_key /etc/nginx/certs/frontend.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        # 重加密转发到后端
        proxy_pass https://backend_tls;
        proxy_ssl_verify on;
        proxy_ssl_trusted_certificate /etc/nginx/certs/backend-ca.crt;
        proxy_ssl_certificate     /etc/nginx/certs/proxy-client.crt;
        proxy_ssl_certificate_key /etc/nginx/certs/proxy-client.key;
        proxy_ssl_protocols TLSv1.2 TLSv1.3;
        proxy_ssl_server_name on;
        proxy_ssl_name app-backend.internal;

        # 标准代理头
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

upstream backend_tls {
    server 10.0.1.10:8443;
    server 10.0.1.11:8443;
    keepalive 32;
}

关键参数说明:

4.2 HAProxy 重加密配置

frontend https_front
    bind *:443 ssl crt /etc/haproxy/certs/frontend.pem alpn h2,http/1.1
    default_backend app_reencrypt

backend app_reencrypt
    balance roundrobin
    
    # 后端使用 TLS
    server app1 10.0.1.10:8443 ssl \
        verify required \
        ca-file /etc/haproxy/certs/backend-ca.crt \
        crt /etc/haproxy/certs/proxy-client.pem \
        sni str(app-backend.internal) \
        alpn h2,http/1.1 \
        check check-ssl check-sni app-backend.internal \
        inter 3s fall 3 rise 2
    
    server app2 10.0.1.11:8443 ssl \
        verify required \
        ca-file /etc/haproxy/certs/backend-ca.crt \
        crt /etc/haproxy/certs/proxy-client.pem \
        sni str(app-backend.internal) \
        alpn h2,http/1.1 \
        check check-ssl check-sni app-backend.internal \
        inter 3s fall 3 rise 2

4.3 Envoy 重加密配置

# envoy-reencrypt.yaml
static_resources:
  listeners:
    - name: frontend
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 443
      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_certificates:
                  - certificate_chain:
                      filename: /etc/envoy/certs/frontend.crt
                    private_key:
                      filename: /etc/envoy/certs/frontend.key
                alpn_protocols:
                  - h2
                  - http/1.1
          filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress
                route_config:
                  virtual_hosts:
                    - name: app
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: backend_tls
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
    - name: backend_tls
      connect_timeout: 5s
      type: STRICT_DNS
      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: /etc/envoy/certs/proxy-client.crt
                private_key:
                  filename: /etc/envoy/certs/proxy-client.key
            validation_context:
              trusted_ca:
                filename: /etc/envoy/certs/backend-ca.crt
          sni: app-backend.internal
      load_assignment:
        cluster_name: backend_tls
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: app-backend.internal
                      port_value: 8443

4.4 重加密的性能开销

重加密模式的 CPU 开销是终止模式的近两倍,因为每个请求涉及两次 TLS 处理:

# 测试环境:4 核 CPU,RSA 2048 证书
# 使用 wrk 测试不同模式下的请求吞吐量

# TLS 终止模式(proxy → backend HTTP)
wrk -t4 -c100 -d30s https://app.example.com/api
# Requests/sec: 45,230

# TLS 重加密模式(proxy → backend HTTPS)
wrk -t4 -c100 -d30s https://app.example.com/api
# Requests/sec: 28,150  (约 62% 的终止模式吞吐量)

# 使用 ECDSA P-256 证书(更高效的密钥交换)
# TLS 终止:52,800 req/s
# TLS 重加密:38,600 req/s  (约 73%)

优化重加密性能的关键手段:

# 1. 使用 TLS 会话复用减少完整握手
upstream backend_tls {
    server 10.0.1.10:8443;
    keepalive 64;  # 保持 TLS 连接不断开
}

# 2. 后端使用 ECDSA 证书(比 RSA 快 3-10 倍)
# 后端 openssl 命令生成 ECDSA 证书
# openssl ecparam -genkey -name prime256v1 -out backend.key
# openssl req -new -x509 -key backend.key -out backend.crt -days 365

# 3. 代理层到后端固定 TLS 版本减少协商开销
proxy_ssl_protocols TLSv1.3;  # 只用 TLS 1.3

# 4. 内网环境可用较低的安全级别
proxy_ssl_ciphers TLS_AES_128_GCM_SHA256;  # AES-128 比 AES-256 快

4.5 重加密 vs 终止 + 网络隔离

两种方案达到类似的安全目标,但工程权衡不同:

维度 重加密 终止 + 网络隔离
安全保证 传输层加密 网络层隔离
合规证明 容易(TLS 审计) 需要网络拓扑审计
CPU 开销
证书管理 两套证书 一套证书
运维复杂度 低(但需要网络管理)
故障排查 代理层可抓包看明文 后端链路明文可抓包

建议:大多数场景用 TLS 终止 + 网络隔离已经足够。重加密适用于合规要求传输层加密的场景(金融、医疗)或零信任网络架构。

五、SNI 路由工程

SNI(Server Name Indication)路由是在同一个 IP 和端口上服务多个域名的核心机制。无论是 TLS 终止还是透传模式都依赖 SNI。

5.1 SNI 路由的协议基础

SNI 位于 TLS ClientHello 消息的扩展中。代理层在接收到 ClientHello 后,根据 SNI 值选择证书(终止模式)或选择后端(透传模式):

TCP 连接建立后:
1. 客户端发送 ClientHello
   ├── 包含 SNI: "app.example.com"
   └── 包含支持的密码套件列表

2. 代理层读取 SNI
   ├── 终止模式:选择对应域名的证书,完成 TLS 握手
   └── 透传模式:选择对应后端,透传 TCP 流

3. 后续数据按选定的路径传输

5.2 多域名 TLS 终止

Nginx 根据 SNI 自动选择证书:

# 域名 A
server {
    listen 443 ssl http2;
    server_name app-a.example.com;
    ssl_certificate     /etc/nginx/certs/app-a.crt;
    ssl_certificate_key /etc/nginx/certs/app-a.key;
    
    location / {
        proxy_pass http://backend_a;
    }
}

# 域名 B
server {
    listen 443 ssl http2;
    server_name app-b.example.com;
    ssl_certificate     /etc/nginx/certs/app-b.crt;
    ssl_certificate_key /etc/nginx/certs/app-b.key;
    
    location / {
        proxy_pass http://backend_b;
    }
}

# 通配符域名
server {
    listen 443 ssl http2;
    server_name *.internal.example.com;
    ssl_certificate     /etc/nginx/certs/wildcard-internal.crt;
    ssl_certificate_key /etc/nginx/certs/wildcard-internal.key;
    
    location / {
        proxy_pass http://backend_internal;
    }
}

Nginx 选择证书的优先级:

  1. 精确匹配 server_name
  2. 前缀通配符 *.example.com
  3. 后缀通配符 app.*
  4. 正则表达式 ~^app\d+\.example\.com$
  5. default_server

5.3 混合模式:部分终止 + 部分透传

实际生产环境中,不同域名可能需要不同的 TLS 处理方式。Nginx 可以在 streamhttp 模块间配合实现混合模式:

# stream 模块:入口分流
stream {
    map $ssl_preread_server_name $tls_route {
        # 需要 L7 处理的域名 → 本地 HTTP 模块
        app.example.com         127.0.0.1:8443;
        api.example.com         127.0.0.1:8443;
        
        # 需要透传的域名 → 直接到后端
        secure.example.com      10.0.2.10:443;
        legacy.example.com      10.0.3.10:443;
    }

    server {
        listen 443;
        ssl_preread on;
        proxy_pass $tls_route;
    }
}

# http 模块:处理需要 L7 路由的请求
http {
    server {
        listen 127.0.0.1:8443 ssl http2;
        server_name app.example.com;
        ssl_certificate /etc/nginx/certs/app.crt;
        ssl_certificate_key /etc/nginx/certs/app.key;

        location / {
            proxy_pass http://backend_app;
        }
    }

    server {
        listen 127.0.0.1:8443 ssl http2;
        server_name api.example.com;
        ssl_certificate /etc/nginx/certs/api.crt;
        ssl_certificate_key /etc/nginx/certs/api.key;

        location /v1/ {
            proxy_pass http://backend_api_v1;
        }
        location /v2/ {
            proxy_pass http://backend_api_v2;
        }
    }
}

HAProxy 实现类似功能更简洁:

frontend tls_front
    bind *:443
    mode tcp
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }

    # 需要 L7 处理的域名 → TLS 终止
    use_backend bk_terminate if { req_ssl_sni -i app.example.com }
    use_backend bk_terminate if { req_ssl_sni -i api.example.com }
    
    # 需要透传的域名 → 直接转发
    use_backend bk_passthrough_secure if { req_ssl_sni -i secure.example.com }
    
    default_backend bk_terminate

# TLS 终止后端(指向自身的 HTTPS frontend)
backend bk_terminate
    mode tcp
    server loopback 127.0.0.1:8443 send-proxy-v2

# 透传后端
backend bk_passthrough_secure
    mode tcp
    server secure1 10.0.2.10:443 check

# 内部 HTTPS frontend(接收终止后的流量)
frontend https_internal
    bind 127.0.0.1:8443 ssl crt /etc/haproxy/certs/ accept-proxy
    mode http
    
    use_backend bk_app if { hdr(host) -i app.example.com }
    use_backend bk_api if { hdr(host) -i api.example.com }

backend bk_app
    mode http
    server app1 10.0.1.10:8080 check

backend bk_api
    mode http
    server api1 10.0.1.20:8080 check

5.4 无 SNI 的客户端处理

部分老旧客户端不发送 SNI(如 Windows XP 的 IE、某些物联网设备)。代理层需要处理这种情况:

# stream 模块中处理无 SNI 的情况
stream {
    map $ssl_preread_server_name $backend {
        app1.example.com  backend_app1;
        app2.example.com  backend_app2;
        ""                backend_default;  # 无 SNI 时的默认路由
        default           backend_default;
    }
}
# HAProxy 检测 SNI 是否存在
frontend tls_front
    bind *:443
    mode tcp
    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }
    
    # 无 SNI → 默认后端
    use_backend bk_default unless { req_ssl_sni -m found }
    
    use_backend bk_app1 if { req_ssl_sni -i app1.example.com }
    default_backend bk_default

六、代理层证书管理

反向代理是证书管理的关键节点。证书过期是最常见的生产事故之一——证书管理的自动化程度直接决定服务可用性。

6.1 证书类型选择

证书类型 优点 缺点 适用场景
单域名 安全性最高 每个域名一个证书 核心生产域名
通配符 *.example.com 管理简单 只覆盖一级子域名 多子域名场景
SAN 多域名 一个证书多个域名 变更需要重新签发 少量固定域名
ECDSA 密钥短、性能好 极老客户端不支持 新部署首选
RSA 兼容性最好 密钥长、性能差 需要兼容老设备

双证书配置(ECDSA + RSA 回退):

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

    # ECDSA 证书(优先)
    ssl_certificate     /etc/nginx/certs/app-ecdsa.crt;
    ssl_certificate_key /etc/nginx/certs/app-ecdsa.key;

    # RSA 证书(回退)
    ssl_certificate     /etc/nginx/certs/app-rsa.crt;
    ssl_certificate_key /etc/nginx/certs/app-rsa.key;
}

Nginx 会根据客户端支持的密码套件自动选择 ECDSA 或 RSA 证书。

6.2 ACME 自动化证书管理

使用 Let’s Encrypt + certbot 实现证书自动签发和续期:

# 初次签发(HTTP-01 挑战)
certbot certonly \
  --webroot -w /var/www/certbot \
  -d app.example.com \
  -d api.example.com \
  --email ops@example.com \
  --agree-tos \
  --non-interactive

# Nginx 配置 ACME 验证路径
# server {
#     listen 80;
#     server_name app.example.com;
#     
#     location /.well-known/acme-challenge/ {
#         root /var/www/certbot;
#     }
#     
#     location / {
#         return 301 https://$host$request_uri;
#     }
# }

自动续期脚本:

#!/bin/bash
# /etc/cron.d/certbot-renew
# 每天检查续期,证书到期前 30 天自动续期

certbot renew --quiet --deploy-hook "nginx -s reload"

# 验证续期结果
if [ $? -ne 0 ]; then
    echo "Certificate renewal failed" | \
    mail -s "ALERT: Cert renewal failure on $(hostname)" ops@example.com
fi

6.3 HAProxy 的证书热加载

HAProxy 支持通过 Runtime API 热加载证书,无需 reload:

# 连接到 HAProxy Runtime API
echo "show ssl cert" | socat stdio /var/run/haproxy/admin.sock

# 更新证书(不中断连接)
echo -e "set ssl cert /etc/haproxy/certs/app.pem <<\n$(cat new-app.pem)\n" | \
  socat stdio /var/run/haproxy/admin.sock

echo "commit ssl cert /etc/haproxy/certs/app.pem" | \
  socat stdio /var/run/haproxy/admin.sock

# 验证新证书
echo "show ssl cert /etc/haproxy/certs/app.pem" | \
  socat stdio /var/run/haproxy/admin.sock

6.4 Kubernetes 中的证书管理

cert-manager 自动管理 Ingress 证书:

# cert-manager ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
---
# Ingress 自动申请证书
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-svc
                port:
                  number: 8080

6.5 证书监控与告警

#!/bin/bash
# check-certs.sh — 检查证书过期时间并告警

WARN_DAYS=30
CRIT_DAYS=7

check_cert() {
    local host=$1
    local port=${2:-443}
    
    expiry=$(echo | openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null | \
             openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
    
    if [ -z "$expiry" ]; then
        echo "UNKNOWN: Cannot connect to $host:$port"
        return 3
    fi
    
    expiry_epoch=$(date -d "$expiry" +%s)
    now_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
    
    if [ "$days_left" -lt "$CRIT_DAYS" ]; then
        echo "CRITICAL: $host cert expires in $days_left days ($expiry)"
        return 2
    elif [ "$days_left" -lt "$WARN_DAYS" ]; then
        echo "WARNING: $host cert expires in $days_left days ($expiry)"
        return 1
    else
        echo "OK: $host cert expires in $days_left days ($expiry)"
        return 0
    fi
}

# 检查所有域名
for domain in app.example.com api.example.com cdn.example.com; do
    check_cert "$domain"
done

Prometheus + Blackbox Exporter 监控:

# prometheus.yml
scrape_configs:
  - job_name: tls-probe
    metrics_path: /probe
    params:
      module: [tls_connect]
    static_configs:
      - targets:
          - app.example.com:443
          - api.example.com:443
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115
# alerting rules
groups:
  - name: tls-alerts
    rules:
      - alert: TLSCertExpiringSoon
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 14
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "TLS cert for {{ $labels.instance }} expires in {{ $value | humanizeDuration }}"
      
      - alert: TLSCertExpiryCritical
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 3
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "TLS cert for {{ $labels.instance }} expires in {{ $value | humanizeDuration }}"

七、生产模式选型决策

7.1 决策流程

选择 TLS 处理模式的决策可以按以下流程进行:

需要 HTTP 层处理(路由/缓存/WAF)吗?
├── 否 → 需要端到端加密吗?
│         ├── 否 → TLS 终止(最简单)
│         └── 是 → TLS 透传
└── 是 → 需要后端链路加密吗?
          ├── 否 → TLS 终止
          └── 是 → TLS 重加密

7.2 场景匹配

场景一:标准 Web 应用

推荐:TLS 终止

理由:
- 需要 HTTP 路由、缓存、安全头注入
- 后端通常在同一个 VPC/集群内
- 网络隔离足以保证内部链路安全
- 运维最简单,性能最好

场景二:金融合规应用

推荐:TLS 重加密

理由:
- 合规要求全链路加密(PCI-DSS / SOC2)
- 需要 L7 功能(WAF、请求日志)
- 可以接受额外的 CPU 和证书管理成本

场景三:第三方服务代理

推荐:TLS 透传

理由:
- 代理层不需要检查第三方的加密流量
- 证书由第三方管理
- 只需要基于 SNI 的路由

场景四:Service Mesh 环境

推荐:mTLS 重加密(Sidecar 自动管理)

理由:
- Istio/Linkerd 的 Sidecar 自动处理 mTLS
- 应用代码不感知 TLS
- 证书由 Mesh 控制平面自动轮换
- 既有零信任安全又有 L7 可观测性

7.3 混合部署策略

大型系统通常混合使用多种模式:

互联网流量 ──TLS──→ [边缘代理/CDN]
                      │
                      ├──TLS 终止──→ [API 网关] ──HTTP──→ 普通微服务
                      │
                      ├──TLS 终止──→ [API 网关] ──mTLS──→ 金融微服务
                      │
                      └──TLS 透传──→ [第三方合作伙伴系统]

Kubernetes Ingress + Service Mesh 混合模式:

# Ingress 层做 TLS 终止(面向公网)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: public-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
    - hosts: ["app.example.com"]
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-svc
                port:
                  number: 8080

---
# Istio PeerAuthentication 控制 mesh 内 mTLS
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT  # mesh 内所有流量强制 mTLS

八、TLS 处理的故障排查

8.1 常见故障分类

故障现象 可能原因 排查方法
SSL_ERROR_RX_RECORD_TOO_LONG 后端返回 HTTP 但代理期望 HTTPS 检查 proxy_pass 协议
upstream SSL certificate verify error 后端证书不受信任 检查 CA 证书路径
no shared cipher 密码套件不兼容 对比前后端密码套件
SNI mismatch SNI 与证书 CN/SAN 不匹配 检查 proxy_ssl_name
handshake timeout 后端 TLS 响应慢 检查后端 TLS 性能

8.2 调试命令

# 1. 检查代理层收到的 SNI
openssl s_client -servername app.example.com -connect proxy:443 </dev/null 2>&1 | \
  grep -E "(subject|issuer|verify)"

# 2. 检查后端证书链
openssl s_client -connect backend:8443 -CAfile /path/to/ca.crt </dev/null 2>&1 | \
  grep -E "(Verify|depth|subject)"

# 3. 测试 SNI 路由是否正确
# 通过指定不同的 -servername 验证路由到不同后端
curl -v --resolve app1.example.com:443:10.0.0.1 https://app1.example.com/
curl -v --resolve app2.example.com:443:10.0.0.1 https://app2.example.com/

# 4. 查看完整的 TLS 握手过程
openssl s_client -connect proxy:443 -servername app.example.com \
  -msg -state -debug 2>&1 | head -50

# 5. 检查代理层支持的密码套件
nmap --script ssl-enum-ciphers -p 443 proxy.example.com

# 6. 验证证书有效期和 SAN
echo | openssl s_client -servername app.example.com -connect proxy:443 2>/dev/null | \
  openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

8.3 Wireshark 分析 TLS 握手

用 Wireshark 分析 TLS 问题的关键过滤器:

# 只看 TLS 握手
tls.handshake

# 看 ClientHello 中的 SNI
tls.handshake.extensions_server_name

# 看证书交换
tls.handshake.type == 11

# 看握手失败
tls.alert_message

# 完整 TLS 会话(从 ClientHello 到 Application Data)
tls.handshake.type == 1 || tls.handshake.type == 2 || \
tls.handshake.type == 11 || tls.handshake.type == 14 || \
tls.change_cipher_spec || tls.alert_message

使用 SSLKEYLOGFILE 解密 TLS 流量以调试代理层问题:

# Nginx 配置(需要 OpenSSL 1.1.1+ 支持)
# 在 nginx.conf 中:
# env SSLKEYLOGFILE=/tmp/tls-keys.log;

# curl 也支持
SSLKEYLOGFILE=/tmp/keys.log curl https://app.example.com/

# 在 Wireshark 中加载密钥文件:
# Preferences → Protocols → TLS → (Pre)-Master-Secret log filename

8.4 代理层 TLS 性能诊断

# 1. 测量 TLS 握手时间
curl -w "DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nTotal: %{time_total}s\n" \
  -o /dev/null -s https://app.example.com/

# 典型输出:
# DNS: 0.012s
# TCP: 0.025s
# TLS: 0.089s      ← TLS 握手时间
# Total: 0.145s

# 2. 持续监控 TLS 握手延迟
while true; do
    tls_time=$(curl -w "%{time_appconnect}" -o /dev/null -s https://app.example.com/)
    tcp_time=$(curl -w "%{time_connect}" -o /dev/null -s https://app.example.com/)
    handshake=$(echo "$tls_time - $tcp_time" | bc)
    echo "$(date '+%H:%M:%S') TLS handshake: ${handshake}s"
    sleep 5
done

# 3. 批量测试 TLS 新建连接性能
openssl s_time -connect proxy:443 -new -time 10
# 输出新建连接数/秒,反映 TLS 握手性能

# 4. 测试 TLS 会话复用性能
openssl s_time -connect proxy:443 -reuse -time 10
# 复用连接数/秒应远高于新建连接数

九、总结

反向代理的 TLS 处理模式不是一个单选题——它需要根据安全要求、性能预算和运维能力综合决策。

我的工程建议

  1. 默认选择 TLS 终止。大多数场景下,TLS 终止 + 内网隔离已经足够安全。它给你完整的 L7 可见性,运维也最简单。不要因为”全链路加密更安全”就盲目选择重加密——安全是风险管理,不是加密层数竞赛。

  2. 合规驱动时才用重加密。PCI-DSS、HIPAA 等合规框架可能要求传输层加密。这时候重加密是正确选择,但要做好性能测试和证书管理自动化。

  3. 透传模式是特殊场景的工具。当你真的不能在中间节点解密流量时(第三方系统、极端合规要求),透传模式是唯一选择。但要接受失去 L7 能力的代价。

  4. SNI 路由会被 ECH 改变。如果你的架构重度依赖 SNI 透传路由,需要开始关注 ECH 的部署进展,规划迁移方案。

  5. 证书管理自动化是必须的。无论选哪种模式,手动管理证书都是定时炸弹。ACME + cert-manager 应该是默认选择。

  6. Service Mesh 正在改变格局。Istio/Linkerd 的 Sidecar mTLS 让应用代码完全不感知 TLS,同时提供零信任安全和 L7 可观测性。如果你已经在用 Service Mesh,让 Mesh 来处理服务间 TLS 是最自然的选择。


上一篇:Envoy 架构剖析:xDS、Filter Chain 与热重启

下一篇:代理性能调优:缓冲、Keepalive 与连接复用

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-08-31 · network

【网络工程】HAProxy 工程:高级配置、ACL 与运维

系统讲解 HAProxy 的工程实践:Frontend/Backend/Listen 配置模型、ACL 规则引擎的高级用法、Runtime API 的动态管理能力、多线程模型与性能调优、SSL 终止与健康检查策略,建立 HAProxy 从配置到生产运维的完整体系。


By .