Cloudflare Tunnel 受管入口与 Access 治理手册
Tunnel、connector 与访问策略
Cloudflare Tunnel 让 cloudflared 从内网主动建立到 Cloudflare 边缘的连接,不要求给开发机或源站开放公网入站端口。这个模型减少了直接暴露源站的网络面,却没有自动回答谁能访问、哪些 hostname 与 path 可以到达哪个 upstream、connector 拿到了什么权限,以及关闭时需要删除哪些对象。
受管入口包含 Tunnel 对象、connector 凭证、DNS route、ingress 规则和 Cloudflare Access 策略。下面的本机实验面向 Linux amd64 普通用户,先运行 uname -m,预期 x86_64;其他架构从官方安装下载页选对应发行资产,不运行下面的 amd64 下载命令。Linux amd64 官方 latest binary 是滚动地址,下载后应记录实际版本与摘要,不能把某次输出当作永久版本。在负责 agent 与管理命令的终端二安装,已有 ~/.local/bin/cloudflared 时先核对来源,不覆盖它:
CF_DL_DIR="$(mktemp -d /tmp/cloudflared-download.XXXXXX)" || exit 1
CF_BINARY="$CF_DL_DIR/cloudflared-linux-amd64"
if test -e "$HOME/.local/bin/cloudflared" || test -L "$HOME/.local/bin/cloudflared"; then
printf '已有 cloudflared,先核对实际路径与版本,不覆盖\n' >&2
elif curl -q --fail-with-body --location --connect-timeout 10 --max-time 300 \
--output "$CF_BINARY" \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64; then
install -d -m 0700 "$HOME/.local/bin"
install -m 0755 "$CF_BINARY" "$HOME/.local/bin/cloudflared"
export PATH="$HOME/.local/bin:$PATH"
cloudflared --version
sha256sum "$CF_BINARY"
else
printf '下载失败,停止安装;改用官方仓库或获准的离线包\n' >&2
ficloudflared --version 应显示实际发行版本;若命令找不到或输出来自已有安装,先用 command -v cloudflared 核对路径。下载中断时不要安装不完整文件。离线分发要核对来源与发行方提供的校验信息。以下服务只返回合成文字;在终端一创建独占目录并保持 HTTP 服务前台运行,后续无需在另一个终端猜测相对目录:
CF_WEB_DIR="$(mktemp -d /tmp/cloudflared-web.XXXXXX)" || exit 1
mkdir -p "$CF_WEB_DIR/sandbox"
printf 'tunnel-ok\n' >"$CF_WEB_DIR/health"
printf 'webhook-ok\n' >"$CF_WEB_DIR/sandbox/webhook"
printf 'CF_WEB_DIR=%s\n' "$CF_WEB_DIR"
python3 -m http.server 8080 --bind 127.0.0.1 --directory "$CF_WEB_DIR"在终端二检查本地服务和二进制:
curl -q --noproxy '*' --fail-with-body -i http://127.0.0.1:8080/health
curl -q --noproxy '*' --fail-with-body -i http://127.0.0.1:8080/sandbox/webhook
cloudflared --version两条路径分别应返回 tunnel-ok 与 webhook-ok。本地失败时先查 8080 是否被占用、终端一是否仍运行;Tunnel 只会把 origin 故障带到公网。目标 upstream 也必须是可公开的测试服务,而不是数据库、容器管理口、集群管理面、Actuator 或带真实数据的后台。
Quick Tunnel 只用于隔离变量
Quick Tunnel 可以在不先配置稳定 DNS 与受管对象的情况下,验证 cloudflared 到本地 upstream 的路径。官方说明默认目录已有 ~/.cloudflared/config.yaml 时 Quick Tunnel 不受支持;遇到这类启动错误,应先识别既有配置及用途,在独立开发环境中处理,不要直接删除团队配置。终端二在本地服务仍运行时执行:
cloudflared tunnel --url http://127.0.0.1:8080命令输出随机 trycloudflare.com 地址。在另一条获准的网络路径上,把终端输出的完整 HTTPS 地址填入 CF_QUICK_URL,从外部请求,不用浏览器缓存作为通过证据:
CF_QUICK_URL='https://实际输出的随机域名.trycloudflare.com'
curl -q --noproxy '*' --fail-with-body --max-time 15 -i "$CF_QUICK_URL/health"预期 HTTP 200、响应体 tunnel-ok,终端一出现对应 /health 记录。随后只停止本地 HTTP 服务并重复请求,connector 仍在线时应得到 origin 故障而非 tunnel-ok;具体边缘状态码随故障情形变化,不写死一个值。在终端一重新执行 python3 -m http.server 8080 --bind 127.0.0.1 --directory "$CF_WEB_DIR",应再次读回 tunnel-ok。受限网络如果只能走批准的代理,应记录代理路径,不用此直连命令推断代理下行为。
Quick Tunnel 有功能与使用限制,不提供适合团队长期治理的稳定入口。随机地址一旦被写进第三方 Webhook、CI、客户演示或共享脚本,就形成了没有明确生命周期的依赖。需要固定 hostname、组织身份、审计、冗余 connector 或长期运维时,应转入受管 Tunnel,而不是把 Quick Tunnel 当成免费的生产入口。
Quick Tunnel 的并发、SSE 等限制见 Quick Tunnels。随机 URL 默认是公开入口,只用于上述无敏感文件的测试服务;结束后在 connector 终端按 Ctrl+C,并从外部再次确认旧地址不能到达该服务。之后继续受管实验时先恢复终端一的本地服务。
创建受管 Tunnel 时分开管理权与运行权
以下是“本地管理的 Tunnel”,不是 Zero Trust 控制台生成 connector token 的“远程管理”路径;两种凭证和配置来源不能混用。Cloudflare 对多数场景推荐远程管理 Tunnel,本地管理更适于这里需要直接检查 ingress 文件的开发实验或已有本地管理环境。操作者需要获准的 Cloudflare 账号、已接入该账号 DNS 的测试域名、创建 Tunnel/DNS/Access 对象的权限。本机实验可由同一人在一台开发机上完成管理和运行,长期部署则把管理证书留在管理机,只把单 Tunnel credential 交给 connector 主机。先查同名对象,确定 webhook-dev 尚未被他人使用;已存在时先给 CF_TUNNEL_NAME 换用经批准的唯一名称,不直接接管:
cloudflared tunnel login
CF_TUNNEL_NAME='webhook-dev'
cloudflared tunnel list确认清单中没有同名对象后再创建;若已有,修改 CF_TUNNEL_NAME 并再次查清单。创建输出的 UUID 与凭证路径随后会用于同一条操作链:
cloudflared tunnel create "$CF_TUNNEL_NAME"
cloudflared tunnel list
cloudflared tunnel info "$CF_TUNNEL_NAME"tunnel login 取得的 cert.pem 可以创建、路由和删除 Tunnel,权限面较大;tunnel create 生成的 <UUID>.json 只允许运行对应 Tunnel。本地创建步骤说明创建输出会给出 UUID 和 credential 文件路径,先记录这两个实际值。运行主机只应得到目标 Tunnel 的 credential,不应长期保留账号证书。把两者复制到同一个共享目录,会让“运行 connector”意外升级为“管理账号中的 Tunnel”。
凭证文件应进入只对服务账号可读的 secret 路径,不放在仓库、镜像层、工单附件或普通备份中。发生泄露时,处置对象是对应 Tunnel connector 权限;若账号证书泄露,则需要按更高等级处理,因为它的管理范围更广。
ingress 要用顺序与兜底表达拒绝
在管理终端中,把 CF_TUNNEL_UUID 填成刚创建的 UUID,CF_HOST 填成自己账号已接入 Cloudflare DNS 的专用测试子域名;示例占位域名 webhook.example.test 不能用于真实 DNS 发布。核对默认凭证文件属于该 UUID,再复制到本次独占目录,配置与其同目录且仅本人可读。此实验的 connector 与 upstream 在同一主机,所以 127.0.0.1:8080 指向上文的静态服务;跨机部署时应换成 connector 主机实际可达的 origin 地址。
CF_TUNNEL_UUID='替换为创建输出的 UUID'
CF_HOST='替换为本账号实际拥有的专用测试域名'
CF_DEFAULT_CRED='替换为 tunnel create 输出的凭证绝对路径'
if test -s "$CF_DEFAULT_CRED"; then
CF_CONFIG_DIR="$(mktemp -d /tmp/cloudflared-managed.XXXXXX)" || exit 1
install -m 0600 "$CF_DEFAULT_CRED" "$CF_CONFIG_DIR/$CF_TUNNEL_UUID.json"
umask 077
else
printf '找不到本次 Tunnel 凭证,停止;先核对 create 输出\n' >&2
fi一份本地管理配置把 hostname、path 和 upstream 写清楚。确认 CF_CONFIG_DIR 已创建,用 ${EDITOR:-vi} "$CF_CONFIG_DIR/cloudflared.yml" 将以下内容保存为该路径,把两个变量在文件中替换成上面的实际值;credentials-file 必须是展开后的绝对路径,YAML 不会替 shell 展开 $CF_CONFIG_DIR:
tunnel: <本次 CF_TUNNEL_UUID 的实际值>
credentials-file: /tmp/cloudflared-managed.<本次目录后缀>/<本次 CF_TUNNEL_UUID 的实际值>.json
ingress:
- hostname: <本次 CF_HOST 的实际域名>
path: '^/sandbox/webhook$'
service: http://127.0.0.1:8080
- service: http_status:404tunnel 选择受管对象,credentials-file 允许当前 connector 运行它。ingress 按顺序匹配,明确的 hostname 与 path 应放在前面,末尾以 http_status:404 作为 catch-all。这个兜底不是装饰:它让未预期的 Host 与路径得到本地可解释的拒绝,而不是被过宽规则带到应用。
path 是 Go 正则而非字面路径;^ 与 $ 防止 /sandbox/webhook-extra 或 /other/sandbox/webhook 意外进入应用。保存后确认没有占位符,并检查实际命中规则;下面的 URL 必须随本次 CF_HOST 一起变化:
: "${CF_CONFIG_DIR:?凭证目录未创建,停止}"
chmod 600 "$CF_CONFIG_DIR/cloudflared.yml"
if grep -q '<' "$CF_CONFIG_DIR/cloudflared.yml" ||
! test -r "$CF_CONFIG_DIR/$CF_TUNNEL_UUID.json"; then
printf '配置仍有占位符或凭证不可读,停止发布\n' >&2
else
cloudflared tunnel --config "$CF_CONFIG_DIR/cloudflared.yml" ingress validate &&
cloudflared tunnel --config "$CF_CONFIG_DIR/cloudflared.yml" ingress rule "https://$CF_HOST/sandbox/webhook" &&
cloudflared tunnel --config "$CF_CONFIG_DIR/cloudflared.yml" ingress rule "https://$CF_HOST/random" &&
cloudflared tunnel --config "$CF_CONFIG_DIR/cloudflared.yml" ingress rule "https://$CF_HOST/sandbox/webhook-extra"
fi第一条 URL 应命中业务 upstream,另外两条应命中 http_status:404。配置中若有多个通配 hostname、path 或服务,应给每类匹配边界准备一个反向样本,不能只验证合法 URL。
ingress 顺序、验证命令与 catch-all 约束可对照官方本地管理配置文件;账号证书和 Tunnel credential 的权限区别可对照 Tunnel permissions。
DNS route 与 Access 的发布顺序决定暴露窗口
DNS route 让 hostname 指向 Tunnel,但不会自动增加身份保护。先在已授权账号的 Zero Trust 控制台进入 Access → Applications,添加 Self-hosted application,Public hostname 填写实际域名并保护整个主机名,避免只保护一条路径。创建 Service Token,再为该 application 添加动作 Service Auth 的策略,Include 仅选择这枚 token;不要增加 Bypass 或 Everyone 允许规则。未命中允许策略的请求被拒绝,不能额外创建覆盖所有身份的 Block 规则来代替默认拒绝。具体界面及令牌说明见 Access service tokens。
记录 Client ID 和只显示一次的 Client Secret 到调用方的 secret store。带 CF-Access-Client-Id、CF-Access-Client-Secret 两个请求头访问允许路径,应进入 upstream;不要把真实头值打印到调试日志。普通浏览器不携带这两项,应被拒绝。确认 Access application 已覆盖本次 CF_HOST、前面的 ingress 三条正反匹配符合预期,且本地 /sandbox/webhook 仍返回 webhook-ok,才发布入口。route dns 需要前面登录取得的账号管理证书;它创建指向 <UUID>.cfargotunnel.com 的 CNAME,尚未运行 connector 时 DNS 已存在但请求不会被代理到本地。发布前先核对该 hostname 没有现存、属于他人的 DNS 记录:
cloudflared tunnel route dns "$CF_TUNNEL_NAME" "$CF_HOST" &&
cloudflared tunnel --config "$CF_CONFIG_DIR/cloudflared.yml" run "$CF_TUNNEL_NAME"创建 DNS route 失败时不要运行 connector,更不要加 --overwrite-dns 强行接管记录;核对域名归属、账号证书和原记录所有者。connector 前台运行后,在另一个具有管理证书的终端用 cloudflared tunnel info <本次名称> 核对连接,外部读写再按下一节验证。DNS route 与 Tunnel 是两个独立对象,停止 connector 不会自动删除 CNAME;官方 DNS records说明 Tunnel 停止后访问者可能得到 1016,不能以此当成 DNS 已回收。
浏览器访问适合组织登录策略;能主动设置自定义请求头的 CI、监控或自有 Webhook 客户端适合 Service Auth 与 service token。service token 应按调用方和环境拆分,设置最小权限与到期时间,不能让一个长期 token 覆盖所有开发入口。接入第三方 Webhook 前要先核对提供方能否发送这两个 Access 请求头;不能发送时,本节的 Service Auth 策略会在边缘拒绝它,不能靠应用里的签名验证让请求越过 Access。
Cloudflare Access 回答“调用方是否有权穿过边缘”,业务签名回答“消息是否来自约定系统且未被篡改”。能携带 Access 凭据的调用方可以叠加两层;无法携带凭据的第三方回调需要另做入口设计,例如专用 hostname 或只匹配回调 path 的 Access application,并按 Cloudflare 的公开端点策略为该窄范围配置 Bypass,再在应用用提供方签名、时间窗与事件 ID 处理伪造和重放。Bypass 会关闭该范围的 Access 鉴权与请求日志,应结合边缘限流、应用日志和测试正反例评估,绝不能把整个管理后台或同域其他路径顺手放开。另一种做法是由可控中转层验签后携带 service token 转发;中转层本身也需明确身份、密钥与失败语义。本次静态服务实验只验证能发送 token 的客户端,不启用 Bypass,也不把它当成第三方回调的生产验收。
用四个结果验证真实边界
入口上线后的证据应覆盖正常、越权、错路由和断源四种结果。
| 实验 | 预期位置 | upstream 日志 |
|---|---|---|
| 正确身份请求允许路径 | Access 放行,ingress 转发,应用成功 | 有且可关联 |
| 缺少或使用错误 service token | Access 边缘拒绝 | 无 |
| 正确身份请求随机路径 | http_status:404 兜底 | 无 |
| 停止本地服务请求允许路径 | connector 在线但 origin 失败 | 无成功请求 |
在另一台获准的测试机上,将 CF_HOST 设置为本次 DNS hostname,交互输入只显示一次的 service token;不要在共享 shell 的命令参数里直接写真实密钥。下面的函数通过标准输入把临时 curl 配置交给 curl,-q 禁用用户级 .curlrc,--noproxy '*' 验证直连路径;若测试网络必须经过获准代理,应改用该路径并单独记录,不能把两种路径的观察混用。
read -rp '测试 hostname: ' CF_HOST
read -rp 'Access Client ID: ' CF_ACCESS_ID
read -rsp 'Access Client Secret: ' CF_ACCESS_SECRET
printf '\n'
cf_authorized() {
printf 'header = "CF-Access-Client-Id: %s"\nheader = "CF-Access-Client-Secret: %s"\n' \
"$CF_ACCESS_ID" "$CF_ACCESS_SECRET" |
curl -q --config - --noproxy '*' --silent --show-error --max-time 15 "$@"
}
CF_RESPONSE="$(mktemp /tmp/cloudflared-response.XXXXXX)" || exit 1
allowed=$(cf_authorized --output "$CF_RESPONSE" --write-out '%{http_code}' \
"https://$CF_HOST/sandbox/webhook") &&
test "$allowed" = 200 && test "$(cat "$CF_RESPONSE")" = webhook-ok退出码为零、allowed=200、正文为 webhook-ok,且终端一记录了 /sandbox/webhook,才证明 Access、ingress 和 origin 同时放行。再在同一测试机上做拒绝对照;这几次不使用 --fail-with-body,否则预期 4xx 会使 curl 自身返回非零。不能把传输失败的 000 误认为 Access 拒绝:
anonymous=$(curl -q --noproxy '*' --silent --show-error --max-time 15 \
--output /dev/null --write-out '%{http_code}' "https://$CF_HOST/sandbox/webhook") &&
test "$anonymous" != 000 && test "$anonymous" != 200
wrong_token=$(printf 'header = "CF-Access-Client-Id: %s"\nheader = "CF-Access-Client-Secret: invalid-test-secret"\n' \
"$CF_ACCESS_ID" |
curl -q --config - --noproxy '*' --silent --show-error --max-time 15 \
--output /dev/null --write-out '%{http_code}' "https://$CF_HOST/sandbox/webhook") &&
test "$wrong_token" != 000 && test "$wrong_token" != 200
wrong_path=$(cf_authorized --output /dev/null --write-out '%{http_code}' \
"https://$CF_HOST/random") && test "$wrong_path" = 404匿名和错密钥响应可能是 Access 的拒绝页或登录重定向,不把某个状态码硬写成唯一产品行为;两者都不得在 upstream 留下对应请求。正确凭据访问随机路径应为 ingress 的 404,也不应进入 upstream;仅看 404 不能区分它是否由应用发出,还需核对本地访问日志与先前的 ingress rule 命中结果。最后保持 connector 在线,停止终端一的本地服务,用同一 cf_authorized 请求允许路径;应出现 origin 故障,cloudflared tunnel info 仍能看到 connector,且不再得到 webhook-ok。恢复 origin 后再读回 webhook-ok,最后执行 rm -f -- "$CF_RESPONSE"; unset CF_ACCESS_SECRET CF_ACCESS_ID。若 cf_authorized 返回传输错误或状态 000,先查客户端 DNS/TLS/代理,不把它归为 origin 故障。
如果未授权请求出现在应用日志,应先检查 Access application 是否覆盖实际 hostname,策略动作是否为允许,以及是否存在绕过 Access 的另一个 DNS route。如果随机路径进入应用,则检查 ingress 顺序与通配范围。合法请求被拒绝时,先看 Access 身份与策略事件,再看 Tunnel、connector 和 origin;删除全部策略虽然可能暂时“通了”,却同时抹掉了故障边界。
connector 应被当作生产进程管理
长期运行时,cloudflared 需要固定版本策略、服务账号、只读配置、受控 credential、结构化日志和明确的重启方式。可以由系统服务或容器管理,但不能同时保留多个无人知晓的启动入口。进程存在不等于服务健康,监控至少要区分 connector 是否连接、Tunnel 是否有健康连接、ingress 是否命中、origin 是否成功。
多个 connector 可以为同一 Tunnel 提供冗余,但这也改变了关闭语义:停止当前机器并不代表 Tunnel 下线。恢复记录必须列出 connector 实例与运行位置,避免一个遗留副本继续承载流量。滚动升级时应先确认新版本连接健康,再停止旧实例,并观察边缘状态码与 origin 错误,而不是只看服务管理器返回成功。
日志中可能出现 hostname、path、源站错误和调用方信息。默认保留结构化元数据即可,完整 header 与 body 只在授权的短窗口中捕获。Service token、Cookie、Access JWT 和 Webhook 签名不得进入普通日志、截图或共享聊天。
稳定入口需要容量与故障预算
Cloudflare 边缘、connector 和 origin 是三段独立的容量边界。长连接、文件上传、流式响应、突发 Webhook 与慢 origin 会占用不同资源。团队应观察活跃连接、connector 重连、边缘状态码、origin 状态码、端到端延迟分位数、传输字节和 Access 拒绝次数。
Tunnel 隐藏源站地址并减少入站防火墙配置,但不能修复慢应用、错误缓存、无限请求体或缺失限流。需要稳定对外服务时,必须为 origin 配置并发、超时、请求大小、幂等和降级策略。边缘成功接收请求不等于业务已经处理成功,Webhook 仍应返回可重试语义,并保存业务事件的幂等状态。
关闭时删除的是一组对象
临时任务结束后,先让第三方停止向 hostname 发送请求,或把回调切到安全占位。若 service token 只供本次实验使用,先在 Access 控制台禁用它,阻止继续通过身份检查;这不等于删除 Tunnel。随后停止全部 connector,检查 cloudflared tunnel info <本次名称> 不再列出本次连接。清理 DNS 时到该域名的 Cloudflare DNS Records,核对本次 hostname 的 CNAME 目标确为 <本次 UUID>.cfargotunnel.com、无其他服务依赖,再删除这一条记录;停止 connector 本身不会删除 CNAME。
若 Tunnel 确由本次实验独占且没有其他 connector 或 route,管理员可在确认归属后运行 cloudflared tunnel delete "$CF_TUNNEL_NAME" 并复查 cloudflared tunnel list;共享 Tunnel 不执行此命令。删除本地 <UUID>.json 副本不等于撤销已经泄露的凭证:本地管理的 tunnel credential 不自动过期,疑似泄露时应隔离该 Tunnel 并按所有者批准的替换/删除路线处理。Access application 与 service token 只有在确认无其他调用方依赖时才删除;账号 cert.pem 可能管理同一账号的其他 Tunnel,不能为了清理本实验直接撤销整张管理证书。具体证书撤销与权限边界见 Tunnel permissions;service token 的删除与单纯撤销既有会话不同,见 Service tokens。
最后只清理本次创建的本机文件:先确认打印的 CF_WEB_DIR、CF_CONFIG_DIR、CF_DL_DIR 对应本实验且服务均已停止;目录里若有额外文件则停止清理并核对,不对父目录递归删除。用户安装的 ~/.local/bin/cloudflared 和可能被其他 Tunnel 使用的默认 cert.pem、默认 credential 不自动删除。
# 在保有对应变量的终端执行;任一目录出现额外文件时 rmdir 会拒绝
if [[ "${CF_CONFIG_DIR:-}" == /tmp/cloudflared-managed.* ]] && test -O "$CF_CONFIG_DIR"; then
rm -f -- "$CF_CONFIG_DIR/cloudflared.yml" "$CF_CONFIG_DIR/$CF_TUNNEL_UUID.json" &&
rmdir -- "$CF_CONFIG_DIR"
fi
if [[ "${CF_DL_DIR:-}" == /tmp/cloudflared-download.* ]] && test -O "$CF_DL_DIR"; then
rm -f -- "$CF_DL_DIR/cloudflared-linux-amd64" && rmdir -- "$CF_DL_DIR"
fi# upstream 所在终端,确认 Python 服务已停止后执行
if [[ "${CF_WEB_DIR:-}" == /tmp/cloudflared-web.* ]] && test -O "$CF_WEB_DIR"; then
rm -f -- "$CF_WEB_DIR/health" "$CF_WEB_DIR/sandbox/webhook" &&
rmdir -- "$CF_WEB_DIR/sandbox" "$CF_WEB_DIR"
fi独占 Tunnel 已删除且凭证原件确属本实验时,管理员再按创建输出的 CF_DEFAULT_CRED 核对路径和所有权后处理该原件;如果只是停止 connector,保留凭证以便获准恢复,不把删文件写成远程吊销。
外部验证是最后一道门:从未登录环境请求旧 hostname,DNS 可能因缓存暂时仍解析、边缘也可能返回错误页或 Access 拒绝,不预设固定失败码;关键是旧请求不再进入本次 origin,账号中不再有未授权的在线 connector,且本次 CNAME 与入口对象已按归属清理。若仍可访问,应检查其他 connector、重复 DNS 记录、通配 Access application、负载均衡 route 和缓存配置。
恢复记录至少要能回答 Tunnel UUID 与名称、DNS hostname、Access application、service token owner、connector 运行位置、凭证撤销状态和外部失败证据。Cloudflare Tunnel 的价值不只是把端口转出去,而是让入口、身份、路由和运行权成为可以分别审计与回收的对象。
