HAProxy 是一款终止 TLS 的 TCP 和 HTTP 负载均衡器,它在边缘解密流量,并将纯 HTTP 转发给后端服务器。这使得它成为整个链路中唯一需要证书的机器,并且它要求证书采用特定的格式:一个文件,包含证书、任何中间证书以及私钥。
本指南涵盖整个流程:生成 CSR、组装 PEM 文件、编写前端和后端配置、检查配置,以及在不中断现有连接的情况下重新加载。所有命令均在 HAProxy 3.4 上运行,这是当前的长期支持分支,于 2026 年 6 月发布,维护至 2031 年第二季度。
HAProxy 需要什么:单一 PEM 文件
大多数服务器将证书、证书链和密钥作为三个独立的设置项。而 HAProxy 只需要一个。其配置手册将 crt 关键字描述为指定”一个包含所需证书和任何相关私钥的 PEM 文件”,该文件通过拼接 PEM 文件构建,并补充说”如果您的 CA 需要中间证书,也可以拼接到该文件中”。
在开始之前,有两个行为值得了解,因为它们能为您节省后续的工作量:
- 密钥可以放在证书旁边,而不必放在证书内部。 如果文件中没有私钥,HAProxy 会查找同一路径下附加 .key 后缀的文件。因此 mydomain.pem 加上 mydomain.pem.key 与一个合并文件效果相同。
- 您可以将 crt 指向一个目录。 HAProxy 会加载该目录下的每一个文件,并根据请求使用 SNI 选择合适的证书。这正是您如何用一个前端服务多个站点,而无需为每个站点单独写一条 bind 行。
生成 CSR 和私钥
CSR 是您提交给证书颁发机构的编码请求。在 HAProxy 机器上生成它,或者在任何可以安全保存私钥的地方生成,因为私钥永远不会离开您这边。
openssl req -new -newkey rsa:2048 -nodes
-keyout mydomain.key -out mydomain.csr
-subj "/C=US/ST=California/L=San Jose/O=Your Company/CN=mydomain.com"
-addext "subjectAltName=DNS:mydomain.com,DNS:www.mydomain.com"
-addext 这一行在实际操作中并非可选项。浏览器多年前就已停止将主机名与通用名称(Common Name)进行匹配,而只读取主题备用名称(Subject Alternative Name),因此仅包含 CN 的 CSR 生成的证书在所有当前浏览器中都会验证失败。请列出证书必须覆盖的每一个名称,包括裸域名和 www 形式(如果两者都提供服务)。
如果您希望针对每个字段单独输入提示,可以省略 -subj 和 -addext 选项。无论哪种方式,最终您都会得到两个文件:需要提交的 mydomain.csr,以及需要妥善保存的 mydomain.key。在提交之前,请通过我们的 CSR 解码器或在本地确认 CSR 中包含您预期的内容:
openssl req -noout -text -verify -in mydomain.csr
检查 SAN 条目是否已列出,以及签名是否通过验证。如果您根本不想使用命令行,我们的 CSR 生成器可以在浏览器中生成相同的一对文件。有关底层命令的更多背景信息,请参阅我们的 OpenSSL 命令指南。
获取证书签发
将 CSR 提交给 CA,选择与您要保护的对象相匹配的证书类型,并完成验证。单域证书覆盖一个主机名,通配符证书覆盖所有一级子域名,而多域名(SAN)证书则覆盖一系列不相关的名称。在负载均衡器的场景下,多域名证书是常见选择,因为一个 HAProxy 实例通常需要为多个站点提供服务。
CA 会返回一个包含您的证书和中间证书链的压缩包,通常以 CA 捆绑包文件的形式提供。两者都已经是 PEM 格式,这正是您所需要的。现在就应该为续期做好规划,而不要拖到以后:自 2026 年 3 月 15 日起,公开信任的 TLS 证书有效期最长不得超过 200 天,2027 年 3 月将降至 100 天,2029 年 3 月将降至 47 天,因此手动更换证书很快将变得不再切实可行。
构建 HAProxy 将读取的 PEM 文件
创建一个用于存放证书的目录,并在那里组装文件。从一开始就把所有内容集中放在一处;如果将其分散放在主目录、/etc/haproxy 和 /etc/ssl 之间,就很容易出现您编辑了一个文件而 HAProxy 却读取另一个文件的情况。
sudo mkdir -p /etc/haproxy/certs
sudo chmod 700 /etc/haproxy/certs
依次拼接证书、中间证书,然后是私钥。重定向操作必须以 root 身份运行,因此请通过管道输出给 tee,而不要写成 sudo cat ... > /etc/haproxy/certs/...,后者会因权限错误而失败,因为 shell 会以您自己的用户身份打开输出文件,早于 sudo 生效之前:
cat mydomain.crt intermediate.crt mydomain.key
| sudo tee /etc/haproxy/certs/mydomain.pem > /dev/null
该文件现在以明文形式包含您的私钥,因此在进一步操作之前请先锁定它:
sudo chown root:root /etc/haproxy/certs/mydomain.pem
sudo chmod 600 /etc/haproxy/certs/mydomain.pem
仅 root 权限在这里是正确的做法,而不是一种阻碍。HAProxy 启动时使用超级用户权限,其手册指出这是为了让它随后能够切换到自己的非特权用户,而它正是在启动期间读取证书。没有必要放宽权限让 haproxy 用户能够读取密钥,您也不应该这样做。
如果您在另一台机器上生成了 CSR,请先将文件复制过去,然后从传输目录中删除副本:
scp mydomain.crt intermediate.crt mydomain.key sysadmin@haproxy-server:/home/sysadmin/
配置 HAProxy
在服务器本身的终端编辑器中打开 /etc/haproxy/haproxy.cfg,比如 nano 或 vim。请就地编辑,而不要在工作站上编辑,这样您就不会重新加载一个与您测试过的文件不同的文件。
前端配置
一个前端可以同时接受纯 HTTP 和 HTTPS。为重定向绑定端口 80,为证书绑定端口 443,并将其余所有内容发送到后端:
frontend web_frontend
mode http
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/mydomain.pem alpn h2,http/1.1
http-request redirect scheme https code 301 unless { ssl_fc }
default_backend web_servers
alpn h2,http/1.1 提供 HTTP/2 支持,并回退至 HTTP/1.1。重定向只有在请求不是通过 TLS 到达时才会触发,这正是 ssl_fc 所测试的内容,因此端口 443 上的请求会直接通过。
将所有 TLS 选项放在同一条 bind 行上
这正是 HAProxy 配置中最常出错的地方,而且它是悄无声息地失败。许多指南通常将 TLS 加固作为第二步呈现,展示为端口 443 新增一条 bind 行并附加额外选项。如果您添加了这样一条新行,而不是编辑现有的那一行,最终您会为同一端口配置两条 bind 行,而 HAProxy 并不会报错。它会正常启动,并在端口 443 上打开两个独立的监听套接字,二者的 TLS 设置各不相同。哪个连接会落到哪个套接字上并不受您控制,因此您的加固措施大约只能覆盖一半的流量。
配置检查同样无法捕捉到这个问题,其官方文档也解释了原因:-c “仅对配置文件执行检查,并在尝试绑定之前退出”。重复监听器是一种绑定时才会出现的状况,因此语法检查永远无法发现它。
请改为设置全局默认值,这样文件中的每一条 bind 行都会继承这些设置,也就没有什么可以重复配置的了:
global
ssl-default-bind-options ssl-min-ver TLSv1.2 prefer-client-ciphers
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
在您将这段配置复制到任何地方之前,有一点值得了解:ssl-min-ver 按照 HAProxy 自身的说法,默认值本来就已经是 TLSv1.2。将其设置为 TLSv1.2 并不会改变任何行为,只是记录了这一意图,这没有问题,但它并不像人们常说的那样是一项安全改进。真正起作用的是两个密码套件设置,它们之所以分开是有意为之的:ssl-default-bind-ciphers 适用于 TLS 1.2 及以下版本,ssl-default-bind-ciphersuites 适用于 TLS 1.3。如果只设置前者,您的 TLS 1.3 套件仍将保持默认设置。
后端配置
为每台服务器指定一个真实地址。这一点看起来简单,实际上非常重要:
backend web_servers
mode http
balance roundrobin
option httpchk GET /
server web1 10.0.0.11:80 check
server web2 10.0.0.12:80 check
像 server web1 :80 check 这样省略地址的服务器配置行不会引发错误。HAProxy 会接受它,并将缺失的地址解析为 0.0.0.0,其手册将其视为一个特殊值,意味着连接会被转发到与客户端连接的相同 IP 地址。而该地址正是 HAProxy 自身,因此后端会悄无声息地指向 HAProxy 自己的端口 80,也就是您刚配置的前端。流量会回环进入重定向,而不会到达任何应用程序,而健康检查看起来却是正常的,因为确实有东西在监听。如果负载均衡器对每一个 HTTPS 请求都以重定向到自身作为响应,请首先检查服务器配置行。
由于 HAProxy 终止了 TLS,后端接收到的是端口 80 上的纯 HTTP,因此不需要自己的证书。如果策略要求在这一环节也进行加密,请在服务器配置行中添加 ssl verify required 及一个 CA 文件,并将其指向端口 443。
将原始协议方案传递给后端。由于后端现在接收的是纯 HTTP,像 WordPress、Django 和 Rails 这样会生成绝对 URL 的应用程序,将会生成以 http:// 开头的 URL,这会表现为混合内容问题,或应用程序自身产生的重定向循环。在前端添加一行:
http-request set-header X-Forwarded-Proto https if { ssl_fc }
并在后端添加一行,这样也会传递真实的客户端 IP 地址:
option forwardfor
大多数框架随后还需要设置为信任这些请求头;这部分配置是在应用程序中完成的,而不是在 HAProxy 中。
应用配置之前先进行检查
切勿在未经验证的文件上重启负载均衡器。请先进行验证:
sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg
加上 -V 参数后,成功时它会打印 Configuration file is valid 并返回退出状态零。如果不加该参数,成功时不会有任何输出。无论文件是否有效,任何警告都会被报告出来,因此请阅读输出内容,而不要仅凭没有出现红色文字就认为一切正常。
重新加载而非重启
sudo systemctl reload haproxy
在负载均衡器上,这一区别是实实在在的。重新加载会启动一个新进程,并向旧进程发出信号,让其”完成正在处理的工作后再退出”,因此已经在进行中的请求会正常完成。而重启会向旧进程发出信号,让其”立即终止,而不管是否完成正在处理的工作”,这会切断实时连接,包括上传和长时间运行的 API 调用。仅在重新加载无法应用相关变更时才使用重启,比如更改了全局部分的进程设置之后。
确认服务已恢复并在两个端口上都处于监听状态:
sudo systemctl status haproxy
sudo ss -tlnp | grep haproxy
这里您需要的是每个端口恰好一个监听套接字。如果端口 443 上出现两个,说明您遇到了上文所述的重复 bind 行问题。
验证证书是否正确提供
从服务器本身检查 HAProxy 实际提供的内容,包括证书链:
openssl s_client -connect mydomain.com:443 -servername mydomain.com < /dev/null
阅读输出顶部的 Certificate chain 部分。您的证书应出现在深度 0 处,中间证书应出现在深度 1 处。如果深度 1 缺失,说明中间证书未能被写入 PEM 文件,网站在某些浏览器中可以正常工作,而在另一些浏览器中会失败。在判断这一点时,请忽略 Verify return code 这一行:它仅报告证书链的验证结果,在与您所测试内容完全无关的情况下也可能显示为成功。
然后从外部进行确认,这样得到的结果才能反映真实访客所看到的情况。我们的 SSL 检测工具会报告证书、证书链以及到期日期。
续期与自动化
替换 PEM 文件就是整个续期流程:用新证书和相同或新的密钥重新构建该文件,然后重新加载。HAProxy 配置中没有任何内容引用到期日期,因此只要文件路径保持不变,就无需修改配置。
随着证书有效期不断缩短,现在就值得设置自动化流程。HAProxy 从 3.2 版本开始内置了 ACME 客户端,通过 acme 部分进行配置。目前请将其视为预览功能,而非生产环境基础设施:在 3.4 版本中它仍被标记为实验性功能,需要在全局部分启用 expose-experimental-directives,它仅支持 http-01、dns-01 和 dns-persist-01 这几种质询类型,并且它生成的证书必须从 stats socket 导出才能落盘。dns-persist-01 类型是在 3.4 版本中新增的,使用一条静态 TXT 记录,该记录一次设置后在各次续期之间都不会变化,因此每次续期都不需要对 DNS 提供商的 API 拥有写入权限。目前成熟稳妥的替代方案是运行一个外部 ACME 客户端,让其部署步骤重新构建 PEM 文件并重新加载 HAProxy,这与我们的 Apache 和 NGINX ACME 指南中描述的模式相同。
常见问题解答
先是您的证书,然后是任何中间证书,最后是私钥。HAProxy 的手册将该文件描述为通过拼接 PEM 文件构建而成,并说明中间证书可以拼接进去。如果您希望将密钥单独保存,可以完全省略它,并将其保存为同一路径附加 .key 后缀的文件,HAProxy 会自动加载它。
通常是因为后端服务器配置行没有指定地址,例如 server web1 :80 check。HAProxy 会将空地址解析为 0.0.0.0,而该地址意味着连接会被转发到与客户端连接的相同 IP 地址,也就是 HAProxy 自身。因此后端指向的是 HAProxy 自己的端口 80,也就是发出 HTTP 到 HTTPS 重定向的那个前端。请为每一条服务器配置行指定一个真实的 IP 地址或主机名。
可以,但这正是问题所在。HAProxy 既不会将它们合并,也不会给出警告,而 haproxy -c 会通过检查,因为它在尝试绑定之前就已退出。您最终会在同一端口上得到两个监听套接字,每条配置行携带着各自不同的 TLS 选项,因此仅存在于其中一条上的设置只会应用于部分流量。请为每个端口只保留一条 bind 行,并将共享的 TLS 设置放在 ssl-default-bind-options 中。
单靠它本身并不能。HAProxy 的文档说明 ssl-min-ver 的默认值本来就已经是 TLSv1.2,因此将其设置为相同的值只是记录了您的意图,并不会改变实际行为。真正的加固来自密码套件设置,请记住 TLS 1.3 需要 ssl-default-bind-ciphersuites,而 TLS 1.2 及以下版本使用的是 ssl-default-bind-ciphers。
在标准配置中不需要。HAProxy 在边缘终止 TLS 并转发纯 HTTP,这正是为什么只有负载均衡器持有证书。只有当您的策略要求内部环节也需要加密时,才需要在服务器配置行上添加 ssl verify required 及一个 CA 文件。
将 crt 指向一个目录,而不是单个文件。HAProxy 会加载该目录下的每一个证书,并根据请求使用 SNI 选择合适的证书,因此一条 bind 行就能覆盖所有证书。多域名(SAN)证书是另一种方案,适合一组一起续期的名称。
3.4 分支于 2026 年 6 月发布,是当前的长期支持版本,维护至 2031 年第二季度。HAProxy 的偶数编号分支是 LTS 分支,维护期约为五年,而像 3.3 这样的奇数编号分支则只有 12 到 18 个月的维护期。本指南中的所有内容都适用于 3.0 及更高版本;只有内置 ACME 客户端需要 3.2 或更新版本。
如果证书已安装,但浏览器仍然报错,我们关于常见 SSL 错误的指南涵盖了通常的原因,最常见的是证书链不完整,或证书未列出相应的名称。


