Web 服务器
HAProxy 配置 SSL 卸载并使用 Let's Encrypt 证书
HAProxy 卸载 SSL 要用一个合并了证书链和私钥的 PEM 文件:Certbot 在 HAProxy 后面的本地端口申请 Let's Encrypt 证书,部署钩子把证书合并成 PEM 并重载 HAProxy,443 前端加载这个文件,80 前端负责跳转。
适用于 HAProxy 2.8 及以上(含 3.x LTS),Certbot 4.1 及以上。
前提条件
HAProxy 占用 80 和 443,Certbot 在它后面的本地端口上应答验证。
- 已安装 HAProxy 的 Linux 服务器,有 sudo 权限;已用 snap 安装 Certbot(
sudo snap install --classic certbot)。 example.com、www.example.com的 A/AAAA 记录指向 HAProxy,例如203.0.113.10。- 服务器在中国大陆的,域名需已 ICP 备案。
- 防火墙和安全组对所有来源放行 TCP 80、443;8888 端口只监听 127.0.0.1,不要对外开放。
- 后端应用可以访问,例如
127.0.0.1:8080。
第一步:在 80 端口转发 ACME 验证请求
先用只有 HTTP 的配置,把验证请求交给 Certbot,其他请求交给应用:
# /etc/haproxy/haproxy.cfg(节选)
frontend web
bind :80
acl acme path_beg /.well-known/acme-challenge/
use_backend certbot if acme
default_backend app
backend certbot
server certbot 127.0.0.1:8888
backend app
server app1 127.0.0.1:8080 check
sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
第二步:用 Certbot standalone 申请证书
Certbot 只在验证期间临时监听 127.0.0.1:8888:
sudo certbot certonly --standalone \
--http-01-address 127.0.0.1 --http-01-port 8888 \
-d example.com -d www.example.com
证书保存在 /etc/letsencrypt/live/example.com/。
第三步:用部署钩子生成合并 PEM
写一个部署钩子,合并证书链和私钥后重载 HAProxy。Certbot 每次续期成功后会执行这个目录里的脚本:
sudo mkdir -p /etc/haproxy/certs
sudo chmod 700 /etc/haproxy/certs
sudo tee /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
name=$(basename "$RENEWED_LINEAGE")
cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" > "/etc/haproxy/certs/$name.pem.tmp"
chmod 600 "/etc/haproxy/certs/$name.pem.tmp"
mv "/etc/haproxy/certs/$name.pem.tmp" "/etc/haproxy/certs/$name.pem"
systemctl reload haproxy
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
对刚申请的证书手动执行一次:
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
第四步:开启 443 监听和跳转
/etc/haproxy/certs/example.com.pem 生成后,再加 443 监听、TLS 基线和跳转:
global
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
frontend web
bind :80
bind :443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
acl acme path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 unless { ssl_fc } || acme
http-request set-header X-Forwarded-Proto https if { ssl_fc }
use_backend certbot if acme
default_backend app
sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
不要给 Let's Encrypt 证书配置 OCSP 相关选项,证书里已经没有 OCSP 地址。
验证
配置正确时,example.com 返回 Let's Encrypt 证书,HTTP 请求跳转到 HTTPS。用 SSL 证书检查 从外部检查,或在终端运行:
curl ssl.sh/example.com
再确认 HAProxy 发送的证书链和跳转:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9] s:'
curl -sI http://example.com | head -n 3
证书链输出里应该有两张证书(深度 0 和 1)。
自动续期
snap 版 Certbot 的定时器每天运行两次 certbot renew,4.1 及以上按 ARI 建议时间续期。证书是用 8888 端口 standalone 申请的,续期会沿用这些参数,第三步的钩子负责重建 PEM 并重载 HAProxy。测试整个流程:
sudo certbot renew --dry-run
systemctl list-timers | grep certbot
--dry-run 不会执行部署钩子,第一次续期后要到线上确认新的到期时间。2027 年 2 月 Let's Encrypt 默认证书改为 64 天后这套配置照常工作。建议加到 ssl.sh 监控(开发中) 兜底。
常见问题排查
- unable to load SSL private key 或 No Private Key found:PEM 里没有私钥,用钩子重新生成。
- unable to stat SSL certificate from file:
crt路径不对或目录为空,证书目录为空时 HAProxy 无法启动。 - Certbot 连接超时:ACL 没匹配上或 80 端口不通,见 certbot 连接超时。
- 客户端提示证书链不完整:PEM 用的是
cert.pem而不是fullchain.pem,见 缺少中间证书。 - 套了 CDN 后无限跳转:CDN 回源走 HTTP,
ssl_fc为假,改在 CDN 上做跳转,见 HTTP 跳转 HTTPS。
常见问题
为什么 HAProxy 要把证书和私钥放在一个文件里?
crt 参数要求的 PEM 文件里依次放站点证书、中间证书和私钥。HAProxy 也能自动加载同名的 .key 文件,但每个站点一个合并文件最容易用脚本维护。
HAProxy 到后端也能走 HTTPS 吗?
可以,在 server 行加 ssl verify required ca-file /etc/ssl/certs/ca-certificates.crt,需要时再加 sni str(backend.example.com)。这里的配置在 HAProxy 上卸载 TLS,到本机或内网后端走明文 HTTP。
80 端口不通时怎么给 HAProxy 签证书?
改用 DNS-01:用 acme.sh 调用阿里云 DNS 或 DNSPod API 签发,再用 --install-cert --reloadcmd 把合并后的 PEM 写到 /etc/haproxy/certs/ 并重载 HAProxy。