Caddy 反向代理报 first record does not look like a TLS handshake
Caddy 返回 502,日志提示 first record does not look like a TLS handshake。核对上游端口的 HTTP 与 HTTPS 协议,修正错误启用的 TLS 传输,并检查重载、负载均衡后端和配置回退。
浏览器访问站点时收到 502 Bad Gateway,Caddy 日志同时出现:
tls: first record does not look like a TLS handshake
如果这条错误出现在 http.log.error 的反向代理请求中,说明 Caddy 在与上游建立 TLS 连接时,收到的第一段响应不符合 TLS 格式。一种常见原因是后端端口提供普通 HTTP,reverse_proxy 却启用了 TLS 传输。
本文适用于 Caddy 2 的常规 HTTP 反向代理,命令示例使用 Linux 环境。先确认错误来自上游连接,再判断端口提供的协议;只有确认后端是 HTTP,才移除上游 TLS 设置。单凭这条日志,还不能排除端口指错或中间设备返回了非 TLS 内容。
站点 HTTPS 和上游 HTTPS 是两段连接
用户通过 HTTPS 访问 Caddy,并不要求 Caddy 也通过 HTTPS 访问后端。这两段连接可以分别配置:
浏览器 -- HTTPS --> Caddy -- HTTP --> 后端
站点块中的 tls 和自动 HTTPS 管理面向用户的连接;reverse_proxy 内的 transport http { tls } 管理连接上游时的 TLS。出现此问题时,要查看反向代理内部的传输配置:
cloud.example.com {
reverse_proxy 127.0.0.1:8081 127.0.0.1:8082 {
lb_policy round_robin
transport http {
tls
}
}
}
域名、地址和端口均为示例。如果两个后端只提供 HTTP,这段配置会让 Caddy 向 HTTP 端口发起 TLS 握手。增加重试次数或更换负载均衡策略,不能改变端口的协议。
先核对正在运行的实例实际加载了哪个配置文件、哪些导入文件,以及是否由面板、容器挂载或部署工具生成。修改本地另一份同名 Caddyfile,不会影响当前实例。
用同一个上游地址分别检查 HTTP 和 HTTPS
检查应从 Caddy 能访问后端的网络环境进行。如果 Caddy 在容器里,宿主机能连通后端还不足以证明容器能连通;容器中的 127.0.0.1 指向容器自身。同一 Docker 网络里的后端通常应使用服务名和容器内端口,具体以现有网络配置为准。
下面假设 Caddy 与后端处于同一主机网络,后端地址为 127.0.0.1:8081,并且 /health 是已确认无副作用的 GET 接口。把地址和路径替换成实际值;在现有诊断环境中使用可用的 curl,不要为这一检查向生产容器临时安装工具。
curl --noproxy '*' --connect-timeout 5 --max-time 10 \
-v --output /dev/null http://127.0.0.1:8081/health
curl --noproxy '*' --connect-timeout 5 --max-time 10 \
-v --output /dev/null https://127.0.0.1:8081/health
--noproxy '*' 避免 curl 被环境里的代理变量转发到别处。两个请求使用相同地址、端口和路径,只改变协议;这里也没有跟随跳转,便于看清这个端口本身提供什么。
根据两次请求的结果判断:
- HTTP 请求收到完整 HTTP 响应,HTTPS 请求出现 TLS 格式错误:结合应用监听配置,可以确认这个端口提供 HTTP。HTTP 返回 401、404 或跳转,仍可证明收到了 HTTP 响应,但不代表业务检查成功。
- HTTPS 请求进入证书验证阶段,报未知签发机构、过期或名称不匹配:端点能够进行 TLS 握手,应处理证书信任与名称问题,不能直接改用 HTTP。
- 两种请求都连接拒绝或超时:先确认地址、监听端口和网络路径。此时尚无证据把问题归因于上游 TLS 配置。
负载均衡配置要逐个检查后端,不能只测一个。若某个端口返回了网关或其他服务的页面,也要继续核对地址是否指向预期应用。
注意区分后端的健康接口与 Caddy 自己的响应。例如配置了 respond /health 200 时,访问站点的 /health 只证明 Caddy 能返回响应,这个请求没有经过上游。
后端确认使用 HTTP 后,移除上游 TLS
先备份当前有效配置。下面的路径是占位值,适用于普通配置文件;如果配置由工具生成,备份并修改其管理源。遇到符号链接、只读挂载或多个导入文件,先确定实际文件和修改范围,再做备份。
caddyfile=/path/to/active/Caddyfile
backup_dir=$(sudo mktemp -d /var/backups/caddy-upstream.XXXXXX) || exit 1
sudo cp -p -- "$caddyfile" "$backup_dir/Caddyfile" || exit 1
printf 'Backup: %s\n' "$backup_dir/Caddyfile"
记录输出的备份路径,确认文件已复制。备份目录不应对普通用户开放读取,配置中可能含有凭据;修改涉及导入文件时也分别保存原文件。
对已确认提供 HTTP 的上游,删除 transport http 内启用 TLS 的设置。若这个块只有 tls,可以删除整个块。修正后的最小配置如下:
cloud.example.com {
reverse_proxy http://127.0.0.1:8081 http://127.0.0.1:8082 {
lb_policy round_robin
}
}
这是配置结构示例,不能直接覆盖现有站点的鉴权、访问控制、路由和证书设置。未指定协议的普通上游地址默认使用 HTTP,显式写 http:// 便于阅读。但如果仍保留 transport http { tls },仅把地址改成 http:// 并不能消除显式的 TLS 传输设置。块中其他必需的传输参数应保留。
修改上游传输设置不会关闭用户访问站点时的 HTTPS。若后端跨越不受信任的网络,不应为了消除错误而降低链路保护;应让后端真正提供 HTTPS,或使用已有的受保护内网通道,再配置对应协议。
后端本来就应提供 HTTPS 的情况
如果应用设计要求上游 HTTPS,确认它的 TLS 监听端口确实已启用,再使用 HTTPS 地址。例如:
cloud.example.com {
reverse_proxy https://app-a.example.internal:8443 https://app-b.example.internal:8443
}
域名和端口需替换为实际值。上游证书须能被 Caddy 信任,证书名称也须匹配实际连接所用的名称。连接 IP 而证书只包含域名时,还要按应用部署情况设置正确的 tls_server_name;私有 CA 则需要配置对应的信任来源。
tls_insecure_skip_verify 只会跳过证书验证,连接仍然使用 TLS。它不能把普通 HTTP 端口变成 HTTPS,也不应作为这里的修复办法。
同一个 reverse_proxy 的上游共用传输配置,不要把 HTTP 和 HTTPS 地址混在同一组里。需要代理两种协议时,按实际路由或服务边界分成不同的代理处理器。
校验、重载和请求检查
先在本机或测试环境,使用与运行实例相同的 Caddy 版本、插件、环境变量和文件路径校验配置:
caddy validate --config /path/to/active/Caddyfile --adapter caddyfile
validate 会加载并预备模块,可能访问模块依赖的资源。包含独占资源的模块时,应按其部署要求校验,避免与运行实例争用资源。校验通过只证明配置能被加载,不能证明后端连接正常。
确认修改范围和校验结果后,通过现有管理流程重载当前实例。管理 API 已启用、CLI 能访问其地址的常规实例可以使用:
caddy reload --config /path/to/active/Caddyfile --adapter caddyfile
管理地址经过定制时,按现有配置指定 --address。面板或容器部署应使用对应的管理入口、实例内路径和持久化配置,不要为了重载把管理 API 暴露到公网。如果管理 API 已关闭,使用既有的受控部署或重启流程。正常重载提交失败时,Caddy 会保留或恢复原来的工作配置;仍需检查返回结果和日志,不能只看命令已经执行。
重载后,访问一个确实经过 reverse_proxy、且没有副作用的现有测试接口,确认前端 HTTPS 正常、业务响应符合预期,并检查同一请求时间附近的日志不再出现该 TLS 错误和 502。使用负载均衡时,还需结合后端访问日志或已有实例标识,确认每个后端均可处理请求;重复刷新几次不能保证所有后端都被验证。
若配置写成了 reverse_proxy / ...,其中 / 只匹配根路径。需要代理整个站点时应省略这个路径匹配器,并检查一个实际的非根路径。它是独立的路由问题,不会改变后端提供 HTTP 还是 HTTPS。
需要回退时
若修改后业务异常,恢复本轮保存的有效配置,再通过同一管理入口重载。以下命令假设只修改了这一个普通文件,backup_dir 仍指向本轮备份:
sudo cp -p -- "$backup_dir/Caddyfile" "$caddyfile" &&
caddy reload --config "$caddyfile" --adapter caddyfile
涉及导入文件或部署工具时,同时恢复本轮修改的管理源,避免下一次部署把配置再次覆盖。恢复前确认期间没有其他必要的新修改。回退可能重新带回原来的 502,应继续核对端口协议和网络路径,保留已获取的日志与连接结果。


