你在反向代理上配置
HTTPS,看似简单——ssl_certificate
一加就完事。但当你开始面对这些问题时,才发现 TLS
在代理层的处理方式直接影响架构的安全性、性能和运维复杂度:
- 后端服务需不需要看到明文?
- 客户端证书(mTLS)怎么传递给上游?
- 代理层做 TLS 终止后,后端之间的流量安全吗?
- 多个域名共用一个 IP,SNI 路由怎么实现?
这些问题的答案取决于你选择哪种 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 DROPKubernetes 中的 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: 443Envoy 的 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 的透传路由将面临挑战。可能的应对方案:
- 使用 Outer SNI 路由到共享前端,前端解密后做 L7 路由
- 改用 TLS 终止模式
- 使用其他标识(如目标 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;
}
关键参数说明:
proxy_ssl_verify on:验证后端证书,防止中间人攻击proxy_ssl_trusted_certificate:后端证书的 CA 证书proxy_ssl_certificate/key:代理层作为客户端的证书(mTLS 场景)proxy_ssl_server_name on:发送 SNI 到后端
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: 84434.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 选择证书的优先级:
- 精确匹配
server_name - 前缀通配符
*.example.com - 后缀通配符
app.* - 正则表达式
~^app\d+\.example\.com$ default_server
5.3 混合模式:部分终止 + 部分透传
实际生产环境中,不同域名可能需要不同的 TLS
处理方式。Nginx 可以在 stream 和
http 模块间配合实现混合模式:
# 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
fi6.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.sock6.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: 80806.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"
donePrometheus + 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 filename8.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 处理模式不是一个单选题——它需要根据安全要求、性能预算和运维能力综合决策。
我的工程建议:
默认选择 TLS 终止。大多数场景下,TLS 终止 + 内网隔离已经足够安全。它给你完整的 L7 可见性,运维也最简单。不要因为”全链路加密更安全”就盲目选择重加密——安全是风险管理,不是加密层数竞赛。
合规驱动时才用重加密。PCI-DSS、HIPAA 等合规框架可能要求传输层加密。这时候重加密是正确选择,但要做好性能测试和证书管理自动化。
透传模式是特殊场景的工具。当你真的不能在中间节点解密流量时(第三方系统、极端合规要求),透传模式是唯一选择。但要接受失去 L7 能力的代价。
SNI 路由会被 ECH 改变。如果你的架构重度依赖 SNI 透传路由,需要开始关注 ECH 的部署进展,规划迁移方案。
证书管理自动化是必须的。无论选哪种模式,手动管理证书都是定时炸弹。ACME + cert-manager 应该是默认选择。
Service Mesh 正在改变格局。Istio/Linkerd 的 Sidecar mTLS 让应用代码完全不感知 TLS,同时提供零信任安全和 L7 可观测性。如果你已经在用 Service Mesh,让 Mesh 来处理服务间 TLS 是最自然的选择。
上一篇:Envoy 架构剖析:xDS、Filter Chain 与热重启
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】代理性能调优:缓冲、Keepalive 与连接复用
系统剖析反向代理层的性能瓶颈与调优方法——Proxy Buffer 内存控制、上下游 Keepalive 参数协调、HTTP/2 连接复用行为、代理层 CPU/内存/连接数监控与容量规划。
【网络工程】网关选型对比:Nginx vs HAProxy vs Envoy vs Traefik
从架构模型、配置方式、协议支持、可观测性、扩展机制和性能特征六个维度,系统对比 Nginx、HAProxy、Envoy 和 Traefik 四大代理/网关,给出基于场景的选型依据。
【网络工程】Nginx 架构深度剖析:事件模型、Worker 与 Upstream
系统剖析 Nginx 的架构设计:Master-Worker 进程模型的工程细节、epoll 事件驱动机制、Upstream 负载均衡与连接池管理、内存池与缓冲区设计、共享内存与 Worker 间通信,建立 Nginx 从配置到内核的完整理解。
【网络工程】HAProxy 工程:高级配置、ACL 与运维
系统讲解 HAProxy 的工程实践:Frontend/Backend/Listen 配置模型、ACL 规则引擎的高级用法、Runtime API 的动态管理能力、多线程模型与性能调优、SSL 终止与健康检查策略,建立 HAProxy 从配置到生产运维的完整体系。