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。

资料来源

  1. HAProxy 3.2 Configuration Manual
  2. HAProxy documentation: SSL/TLS
  3. Certbot User Guide
  4. Let's Encrypt: Challenge Types