CDN 回源鉴权实战:自定义回源头、时间戳签名与源站 IP 防直连,Nginx 444 默认 server 收口

套了 CDN 之后,你的源站其实还裸露在公网

很多个人站长上了 CDN 之后就松了一口气,觉得「源站 IP 藏起来了,攻击打不到我」。但真实情况是:源站服务器的 IP 往往并没有真正被隐藏——历史 DNS 记录、证书透明日志(Certificate Transparency)、子域名解析、邮件头、甚至第三方扫描服务的数据库里,都可能留着你的真实 IP。

攻击者一旦拿到源站 IP,就可以完全绕过 CDN,直接把流量砸到你的机器上:CDN 的防护、限流、缓存全部失效,你为 CDN 付的钱也白花了。这就是所谓的源站直连(origin bypass)问题。

本文讲的是应对这个问题最有效的一层技术手段:CDN 回源鉴权(也叫回源签名、源站鉴权、origin authentication)。核心思想一句话就能概括:只有携带正确签名的请求,源站才响应;没有签名或签名错误的请求,直接拒绝。

回源鉴权的三种主流实现方式

在动手配置之前,先理解你的 CDN 支持哪一种,这决定了后面所有配置怎么写。

方式一:自定义回源请求头(最简单,通用性最好)

CDN 在回源时自动加上一个只有你和 CDN 知道的请求头,比如:

X-Origin-Auth: 7f3a9c2e5b1d8f4a6e0c3b9d2a5f7e1c

源站 Nginx 检查这个头,值不对就直接返回 403。这种方式实现最简单,缺点是静态密钥:一旦泄露就得全量轮换。

方式二:基于时间戳的签名(推荐,抗重放)

CDN 拼接「时间戳 + 密钥」做 HMAC 或 MD5 计算,把签名和时间戳一起带给源站。源站收到后重新计算一遍,并检查时间戳是否在允许窗口内(比如 5 分钟)。这样即使请求被中间人抓到,过了时间窗也无法重放。

方式三:回源 mTLS / 客户端证书

CDN 和源站之间建立双向 TLS 认证,源站只接受持有合法客户端证书的连接。这是最安全的方案,但配置成本最高,且不是所有 CDN 都支持。自建 CDN 或用 Cloudflare Tunnel 时比较常见。

Nginx 侧实现自定义头鉴权

先看最简单的方案一。在源站的 Nginx 配置里加一段判断:

server {
    listen 443 ssl http2;
    server_name www.example.com;

    # 回源鉴权密钥(存在独立文件里,不要写进版本库)
    # 文件内容: set $origin_key "7f3a9c2e5b1d8f4a6e0c3b9d2a5f7e1c";
    include /etc/nginx/conf.d/origin_key.conf;

    # 默认拒绝:所有未通过鉴权的请求一律 403
    if ($http_x_origin_auth != $origin_key) {
        return 403;
    }

    # 以下正常业务配置
    root /var/www/html;
    index index.php index.html;
}

几个关键点必须注意:

  • 密钥不要硬编码在主配置里,用 include 引入独立文件,并设置权限 chmod 600,避免被误提交到 Git 或备份泄露。
  • 先做灰度再全量。直接把 return 403 上线,如果 CDN 那边还没配好回源头,你的网站会瞬间全站打不开。稳妥做法是先只记录不拦截:
if ($http_x_origin_auth != $origin_key) {
    # 先只打日志观察,确认 CDN 回源都带上了正确的头
    access_log /var/log/nginx/origin_auth_fail.log authfail;
    # return 403;   # 观察 24 小时无异常后再打开这一行
}

更稳妥:基于时间戳的 HMAC 签名方案

静态密钥的致命弱点是长期有效。下面给出一个时间戳签名方案,源站用 Nginx 原生能力校验。

CDN 回源时携带两个头:X-Auth-Ts(Unix 时间戳)和 X-Auth-Sign(签名值),签名算法约定为:

sign = md5(secret + timestamp)

Nginx 侧可以这样校验:

server {
    listen 443 ssl http2;
    server_name www.example.com;

    include /etc/nginx/conf.d/origin_key.conf;   # set $origin_secret "..."

    set $now $msec;                               # 当前时间(秒.毫秒)

    # 计算期望签名(需要 ngx_http_secure_link_module 或 Lua)
    # 方案 A:使用 secure_link 模块
    location / {
        secure_link $arg_sign,$arg_ts;
        secure_link_md5 "$origin_secret$secure_link_expires";

        if ($secure_link = "") { return 403; }
        if ($secure_link = "0") { return 410; }

        proxy_pass http://backend;
    }
}

secure_link 模块原本设计用于下载防盗链,用来做回源鉴权也很合适,因为它的核心逻辑就是「校验签名 + 校验过期时间」。缺点是签名算法固定为 MD5,签名值的拼接格式有约束(md5(secret + expires),注意 secret 在前)。

如果你的 CDN 支持自定义签名算法,更推荐用 OpenResty / njs 实现任意 HMAC:

# 使用 ngx_http_js_module(njs)示例,需编译进 njs 模块
js_import auth from /etc/nginx/js/origin_auth.js;

server {
    listen 443 ssl http2;
    js_content auth.checkOrigin;   # 在 location 里调用
}

# /etc/nginx/js/origin_auth.js
# export default { checkOrigin }
# function checkOrigin(r) {
#     const ts = r.headersIn['X-Auth-Ts'];
#     const sign = r.headersIn['X-Auth-Sign'];
#     const now = Math.floor(Date.now() / 1000);
#     if (!ts || Math.abs(now - Number(ts)) > 300) {   // 5 分钟窗口
#         r.return(403); return;
#     }
#     const crypto = require('crypto');
#     const expect = crypto.createHmac('sha256', '你的密钥')
#                          .update(ts).digest('hex');
#     r.return(sign === expect ? 204 : 403);
# }

上面这段 JS 注释里的代码是 njs 的写法,实际部署时把注释去掉即可。用 HMAC-SHA256 替代裸 MD5,安全性明显更高,而且时间窗限制让重放攻击失去意义。

别忘了这些会绕过鉴权的口子

鉴权配置做好了,不代表源站就安全了。以下几个位置最容易被遗漏,是实战中源站被直连的主要入口。

1. 直接暴露的 IP 访问

攻击者用 IP 直连你的服务器,Host 头可以是任意值。如果你的 Nginx 有默认 server 块返回了正常内容,就等于给了一条绕过路径。正确做法是配置一个 catch-all 的默认 server,直接拒绝:

# 放在所有 server 块的最前面,作为默认 server
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate     /etc/nginx/ssl/default.crt;
    ssl_certificate_key /etc/nginx/ssl/default.key;
    return 444;   # 444 = 直接断开连接,不回任何内容
}

return 444 比 return 403 更彻底——连响应都不给,扫描器拿不到任何信息。

2. 未加鉴权的其他端口和子服务

常见漏洞:主站 443 加了鉴权,但 8080(Tomcat)、3000(Node)、9000(PHP-FPM/管理后台)还裸奔在公网。攻击者不需要攻破你的 Nginx,直接访问这些端口就行。用防火墙统一收口:

# 只允许 CDN 节点 IP 段访问源站的 80/443,其余端口一律拒绝
# 以 ufw 为例(先确认 CDN 官方公布的回源 IP 段)
ufw allow from 203.0.113.0/24 to any port 80,443 proto tcp
ufw allow from 203.0.113.0/24 to any port 80,443 proto tcp
ufw deny 80/tcp
ufw deny 443/tcp
ufw enable

这是比应用层鉴权更根本的一层防护。应用层签名做得再好,端口还开着就总有被绕过的可能。IP 白名单 + 应用层签名,两层都做才叫闭环。

需要注意的是:CDN 回源 IP 段会变动,必须定期从 CDN 官方渠道同步更新,否则某天 CDN 换节点,你的网站就全站 502 了。建议把 IP 段维护成脚本自动更新。

3. 泄露真实 IP 的间接渠道

IP 白名单的前提是别人不知道你的源站 IP。以下渠道会泄露:

  • DNS 历史记录——上线 CDN 之前的 A 记录会被各种 DNS 历史查询站点永久记录。换 CDN 时最好同时换源站 IP。
  • 证书透明日志(CT Log)——你为源站申请的每一张证书都会公开在 CT 日志里,包含域名(不含 IP,但可以配合其他信息)。
  • 邮件头——从源站发出的邮件,Received 头里会带上源站 IP。自建 Postfix 的站长尤其要注意。
  • 子域名——dev.example.com、mail.example.com 等未走 CDN 的子域名,解析出来的常常就是同一台源站。
  • SSRF / 图片外链——网站上引用了外部的图片或接口,请求会从源站发出,对方在日志里就拿到了你的 IP。

所以回源鉴权要配合「源站 IP 保密」一起做,单靠其中一项都不够。

验证鉴权是否真的生效

配置完必须验证,而且要从源站外部验证,不能只在服务器上 curl 127.0.0.1。

# 1. 带正确密钥访问 -> 应当返回正常内容
curl -sI "https://源站IP/" -H "Host: www.example.com" \
     -H "X-Origin-Auth: 正确的密钥" | head -1

# 2. 不带密钥访问 -> 应当 403 或 444(连接被拒)
curl -sI "https://源站IP/" -H "Host: www.example.com" | head -1

# 3. 带错误密钥 -> 应当 403
curl -sI "https://源站IP/" -H "Host: www.example.com" \
     -H "X-Origin-Auth: wrong" | head -1

# 4. 走正常域名(经 CDN)-> 应当正常 200
curl -sI "https://www.example.com/" | head -1

第 2、3 条如果不返回 403/444 而是 200,说明鉴权没生效,重点检查:Nginx 配置有没有 reload、if 判断的变量名对不对($http_ 前缀 + 头名转小写并把 - 换成 _)、CDN 那边有没有真的把自定义头透传到源站。

还有一种情况要警惕:CDN 回源时把客户端传来的同名头也一起转发了。如果攻击者自己伪造 X-Origin-Auth 请求头,而 CDN 不做覆盖只是追加,某些情况下源站可能读到攻击者提供的值。解决办法是在 CDN 侧配置覆盖(override)而非追加,或者干脆改用带时间戳的签名,让伪造无从下手。

常见问题解答

Q:回源鉴权会导致 CDN 缓存失效吗?

A:不会。鉴权头只在「CDN 回源」这一段存在,不会出现在返回给用户的响应里,也不参与缓存键计算(除非你特意把它加进缓存键,那就要避免)。

Q:用了 Cloudflare 还需要做回源鉴权吗?

A:需要。Cloudflare 免费版不提供源站 IP 白名单能力(企业版有 Authenticated Origin Pulls)。最实用的做法是:源站只接受 Cloudflare IP 段(官方公布且可定期同步),再加上一个自定义回源头或 Cloudflare Tunnel。注意 Cloudflare 的 IP 段列表较大且会更新,维护成本要提前考虑。

Q:源站只做 IP 白名单,不做签名,够不够?

A:如果攻击者不知道你的源站 IP,够用。但一旦 IP 泄露(通过上文任一渠道),白名单就成了唯一屏障,而攻击者可以伪造源 IP 吗?不能伪造 TCP 源 IP(三次握手做不到了),所以 IP 白名单本身是可靠的前提是 CDN 的 IP 段绝对可信。风险在于:CDN 是共享基础设施,如果你的 CDN 上有别人的站点,理论上存在从同一 IP 段发起攻击的可能。所以「IP 白名单 + 应用层签名」双保险才是最佳实践。

Q:密钥轮换怎么做才不中断服务?

A:支持双密钥并存。源站的校验逻辑改成「匹配新密钥或旧密钥任一即可」,先在源站上线这个逻辑,再更新 CDN 的签名密钥,观察日志确认新密钥的请求量 100% 后,最后从源站移除旧密钥。

小结

回源鉴权的本质是把「谁可以访问源站」这件事从网络层提升到应用层的明确判断。它不能替代「隐藏源站 IP」,也不能替代「防火墙收口」,三者是互补的:

  • 隐藏源站 IP 提高攻击者的发现成本
  • 防火墙 IP 白名单从网络层挡住大部分直连
  • 应用层签名确保即使流量走到了源站,没有凭证也拿不到任何内容

对个人站长来说,最低成本的起步方案是:Nginx 默认 server 返回 444 + 一个自定义回源头 + 源站只放行 CDN IP 段。这三步加起来配置不超过二十行,却能把「CDN 形同虚设」的风险基本堵死。

Last modification:September 29th, 2026 at 10:23 pm

Leave a Comment