Chips

用 Cloudflare Access 替代应用的弱认证

· 约 7 分钟 · 2316 字

隧道只解决了网络暴露,认证责任仍默认落在应用自身,而自托管应用的门锁往往很弱。本文比较应用层、反代层与边缘层三种认证位置的能力差异,给出用 curl 首跳 302 判断 Access 是否生效的方法,并划清它不能替代应用授权的边界。

问题的形状

服务经隧道发布之后,网络暴露这一层是干净的:源站不监听对外端口,公网 IP 不出现在 DNS 里,边缘终结 TLS。但认证这一层没有跟着变。

通道修好了,门锁还是原来那把。而自托管应用自带的门锁通常很弱:

这些门锁在内网里不算严重问题;一旦挂上 example.com 的子域、经边缘发布到全网可路由,性质就变了。所有人都在同一个入口排队试口令,而应用自身通常没有速率限制、没有封禁、没有二次验证。

先厘清一件事:隧道的收益是 网络层 的。它回答「谁能连到源站」,不回答「谁能登录应用」。访问控制要单独设计,而设计的第一件事是选它落在哪一层。

三层可以把关的位置

认证层 典型实现 二次验证 审计 设备策略 策略统一 源站是否运行认证逻辑
应用自身 应用内置用户体系、Basic 口令、默认凭据 多数没有 多数只有零散的登录日志 没有 逐个应用配置
反代层 nginx / Caddy 的 Basic Auth、htpasswd 没有 访问日志(需自己脱敏) 没有 一份口令文件可共享,但仍要按 vhost 挂 是(反向代理仍在源站侧)
边缘层 Cloudflare Access:邮箱 OTP、SSO、服务令牌 有(取决于身份源) 有(边缘侧统一记录) 有(配合 WARP 的客户端姿态) 有(策略对象可复用)

三行的差别不是「好一点 / 差一点」,是能力上的有无:Basic 这一类口令认证,无论放在应用里还是放在反代里,都天然没有二次验证、没有身份、没有审计、没有设备信息。它只知道「口令对 / 不对」。而边缘层接入的是身份源,验证的是「这个人是谁、他的设备是什么状态、这条策略允许不允许」。

反代层仍有它的位置:在没有身份源、也不想引入第三方的情况下,「反代加 Basic 加 fail2ban」明显强过应用自带弱口令,至少把尝试次数管住了。但它有天花板,别把它当成终点。

为什么倾向放在边缘

请求在到达源站之前就被拦下。 未认证的请求根本不会出现在应用日志里,也不会进入应用的任何代码路径。认证前的攻击面被抬到了边缘:认证绕过、未认证远程执行、解析器漏洞,都需要先过 Access 这一关才谈得上够到。这一点是「认证逻辑不部署在源站」带来的,而不是靠更严的口令换来的。

策略可以统一。 邮箱域、身份源里的组、来源国家、会话时长,都是一次定义、挂到多个应用上。加一个新服务时认证是复用已有的策略对象,而不是又去建一套用户。对比之下,应用层意味着每加一个服务就多一套独立账号体系(以及多一处可能忘记加固的登录页)。

会话与凭据分开管理。 浏览器用户走 OTP 或 SSO,机器客户端走服务令牌(service token),两者的生命周期、轮换方式、吊销路径都不一样,在边缘层可以各管各的。应用层往往只有一种方式,于是「给 CI 用的账号」就是「人手一个的账号」。

怎么判断一个域名套没套 Access

一条命令就够。关键是看首跳的状态码与 Location,不要跟随重定向:

curl -s -o /dev/null -D - https://<主机>/ | grep -Ei '^(HTTP/|location:)'

套了 Access 的输出形如:

HTTP/2 302
location: https://<团队名>.cloudflareaccess.com/cdn-cgi/access/login/<主机>?kid=<十六进制>&redirect_url=%2F

判据:

几个容易误判的点:

别跟随重定向。 curl -sIL 的最终状态码是登录页的状态码(也是 200),看起来和「裸奔」一模一样。判断依据是首跳的 Location 主机,不是最后一个状态码。

用无 cookie 的会话测。 浏览器里已经登录的会话会带着 Access 会话 cookie 直接拿到 200,因此浏览器地址栏看不出策略是否生效。curl 天然没有 cookie,这正是它适合这个用途的原因。

探测只能证伪。 一次 200 说明「这条路径上没有强制认证」,但也可能来自 Access 里刻意配置的按路径 / 按来源放行策略。要结论准确,得连策略一起看。

Access 只作用于经过边缘的流量。 如果某个服务的 DNS 是直连源站(灰云)而不是走隧道,那条路径完全不经过 Access。隧道场景下 CNAME 必须处于代理状态,这一点通常自动满足;但同一台机器上「有的服务走隧道、有的服务直接挂 A 记录」时,后者就没有这层保护。

它解决不了什么

它回答「你是谁」,不回答「你能做什么」。 应用内部的多用户权限、只读分享、按目录的可见性,仍然必须由应用实现。把 Access 当授权用只有两个结果:要么所有通过认证的人看到全部内容,要么你在边缘重新实现一套权限模型——那是在错误的层做事。

它不修复有漏洞的服务。 认证通过之后的攻击面完全不变:越权、注入、文件读取漏洞都在应用里。Access 消除的是认证前的暴露面,让一个「未认证就能打」的漏洞够不着;但只要有一个合法身份能进来,漏洞就还在。单人自托管尤其要意识到这点:唯一那个合法身份就是你自己,你的一次误点仍然可以触发漏洞。

对公开内容用它属于过度设计。 给博客、公开文档加 Access,会挡掉访客、搜索引擎与订阅器,每次访问还多一跳登录页。判断依据很简单:内容是「谁看都行」还是「只给特定人」。前者不需要认证层,加了只是给自己找麻烦。

它有运维成本。 新加的 hostname 忘了建 Access 应用就等于裸奔(隧道通了、看着很正常);服务令牌是长期有效的密钥,要和隧道 token 一样当机密文件管,并留轮换方案;身份源出问题时,你自己的管理入口也会一起进不去,所以运维通道(例如 SSH 密钥登录)应当独立于 Access 存在。

决策表

服务类型 建议的认证层 说明
公开内容站(博客、公开文档) 不需要 公开即公开;加 Access 会挡掉访客与搜索引擎
个人笔记 / 知识库 边缘层 访问者有明确范围;OTP 一次配置多设备可用,且应用本身通常只有单口令
管理面板(容器面板、路由器、监控、机器人 WebUI) 边缘层(必要) 这类面板的认证最弱、漏洞代价最高;同时不该把认证逻辑留在源站
文件分享站 边缘层 + 应用内授权 Access 控制「谁能进」,应用控制「能看哪个目录」,两层各司其职
对外 API 服务 边缘层 + 服务令牌,或应用自身令牌 机器客户端做不了浏览器 OTP;服务令牌要按密钥管理并定期轮换

落到配置上的第 4 步

「加一条共享网络、加一条 ingress、加一条 DNS」是发布一个服务的三步。用 Access 的话还有第 4 步:为这个 hostname 建 Access 应用并挂上策略。这一步漏掉不会报错,服务照常工作——只是所有人都在敲应用那把门锁。

配套的几点:

← 全部文章