🔑 关键洞察
✦ AI·GEN这篇记录了作者跟另一位自架站长交叉监控的完整过程,并整理成一份可照做的教学。起点是自架服务的一个尴尬处境:监控自己的那台机器,自己挂了就没人通知得了。解法是互相盯,但一开始两人都卡在同一个错误前提上——以为要走 HTTPS 就得交换凭证。真正的答案是不用交换:对方把子网域 A 纪录指向你的 IP,TLS 就在你这里终结,Let's Encrypt 的 HTTP-01 验证会直接打到你的 nginx,所以你自己就能签那张凭证、还自动续签;共享 wildcard 反而踩中私钥暴露与手动续签两个雷。文中也整理了反代 Kuma 必备的 WebSocket 设定(少了 Upgrade 标头,即时图表会完全不动)、给后端加公开 /health 而非用 401 取巧、用 push 型监控盯 cron 脚本这种沉默故障,以及 v1 升 v2 的资料库迁移该当作不可逆来处理。最后是全篇最值得记的一个坑:Trust Proxy 这个看起来明显该开的选项,在 nginx 用 $proxy_add_x_forwarded_for「附加」而非「覆写」的架构下,开了之后任何人都能伪造 IP(作者实测伪造成功),而真正的风险不是登入爆破(2FA 已挡),是 CrowdSec 会因此去封 Cloudflare 的整段网段。
自架服务久了会遇到一个尴尬问题:监控自己的那台机器,自己挂了谁告诉你?
我跑 Uptime Kuma 监控自己的所有服务,但它就跑在被监控的那台机器上。停电、断网、整台当掉,监控跟着一起死,通知一封都发不出来。
要解决这个,监控就得放在别的地方。方法其实不少:
| 做法 | 成本 | 适合谁 |
|---|---|---|
| 一台便宜 VPS 跑自己的 Kuma | 每月几美金 | 想完全自己掌控、不想欠人情 |
| 用现成的第三方监控服务 | 免费方案通常够用 | 只想要「挂了通知我」,不想再多养一个服务 |
| 跟另一个自架的人交叉监控 | 零成本 | 刚好认识也在自架的人 |
我选第三个纯粹是因为刚好有:另一个也在自架的站长,两边互相盯,他的 Kuma 盯我、我的 Kuma 盯他。附带好处是两边的机房、线路、电力完全独立,而且对方是活人,真的挂掉时会直接敲你。
这篇记录整个过程,包括一开始那个把我们卡住的凭证误解、v1 升到 v2 的资料库迁移,以及最后查出来的一个坑:Trust Proxy 这个选项,在我的架构下开了之后反而比不开更危险。
(状态页的外观可以用自订 CSS 换掉,上面这个是我后来调成的像素风。Kuma 的状态页跟后台是分开的:后台要登入,状态页可以单独公开,只露你想露的服务。)
凭证不用交换#
一开始的设定是这样:对方开了一个子网域 status.<朋友网域>,把 A 纪录指向我的固定 IP,Kuma 跑在我这台。
然后就卡住了。我的 nginx 用 Let's Encrypt,而那是他的网域。当时我们两个的想法都是:要走 HTTPS,他就得把凭证给我。他有一张 *.<朋友网域> 的,我以为那就是唯一的路。
这个前提整个是错的。
IMPORTANT
互相监控不需要交换任何凭证
关键在于:status.<朋友网域> 虽然是他的网域,但 A 纪录已经指向我的 IP,所以 TLS 是在我的伺服器上终结的。Let's Encrypt 的 会去打 http://status.<朋友网域>/.well-known/acme-challenge/...,而这个请求会直接进到我的 nginx。所以我自己就能签这张凭证,他什么都不用给我。
certbot --nginx -d status.<朋友网域> 一行,就签好了。续签也一并解决:certbot.timer 自动跑,跟我自己的网域完全一样。
wildcard 只绑「签发那一刻」#
我当时的误解是「wildcard 绑死 DNS,所以只能用他的」。这句话对了一半:wildcard 的「绑 DNS」指的是签发那一刻的验证方式,它一定要走 DNS-01。但那跟「凭证之后绑在哪」是两回事。
凭证签好之后就只是一个档案,放在负责终结 TLS 的那台机器上。它不绑 DNS 商、也不绑某台伺服器。而且同一个主机名可以同时存在好几张不同的凭证,互不冲突,CA 不会因为他有 wildcard 就不准我再签一张单域名的。凭证签发不是独占的,只要你能证明你控制这个主机名,你就能拿到自己的那张。
反过来说,「用他的 wildcard」刚好踩中两个雷:
| 共享 wildcard | 自己签单域名 | |
|---|---|---|
| 私钥暴露面 | 涵盖他所有子网域,等于全站钥匙给我 | 只有这一个主机名 |
| 续签 | 每 ~90 天他要重发、我要重贴重启,纯手动易断 | certbot.timer 自动,零参与 |
三个角色分清楚就不会乱:签发的永远是 CA(Let's Encrypt);申请人是我(我跑 certbot、我证明控制权);网域拥有者是他,他唯一做的事就是把 A 纪录指过来,而那早就做完了。没有任何人需要「帮对方签」。
WARNING
这招的唯一前提:对方的子网域必须是纯 A 纪录直连,不能挂 Cloudflare 橘云 挂了橘云的话 TLS 会在 Cloudflare 终结,HTTP-01 验证会打到 Cloudflare 而不是你,这条路就不通(那要改用 Cloudflare Origin Cert)。这个前提在文章后半会再回来咬我一次。
反代:别漏了 WebSocket#
Kuma 的后台大量用 推即时心跳跟图表。反向代理如果只写一行裸的 proxy_pass,WebSocket 升级不了,症状是即时图表完全不动,或浏览器判定「不安全」(HTTPS 页面里混进 ws:// 的)。
反代区块该有的几行:
location / {
proxy_pass http://127.0.0.1:3011;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s; # 24 小时,长连线不会被 nginx 掐断
}有个很容易误判的地方:直接对 /socket.io/ 发 raw upgrade 请求会拿到 400,那是 Socket.IO 自己因为缺 sid 参数拒绝的,不是 nginx 挡的。看到 400 别急着改 nginx。
该监控什么#
服务接上来之后,有两个做法值得写下来。
顺带一提,新增监测器时记得勾凭证到期通知。交叉监控本来就会定期打对方的 HTTPS,顺便让它在凭证剩没几天时叫你一声,等于免费多一层保险。
我一开始监控 NAS 后端,是打一个需要认证的端点、然后把「可接受状态码」设成 401,反正回 401 就代表它活着。这能动,但语意是歪的:哪天后端坏掉但反代还在,可能照样回 401,监控却显示绿灯。
正解是在后端加一条公开、免认证的 /health 回 200。活着=200、挂了=502,可接受状态码设回预设的 200-299,语意就对了。
HTTP 监控只能确认「服务现在活着」,但像每日备份、更新脚本这种排程任务,真正该监控的是「它有没有按时跑完」。
Kuma 的 就是为这个设计的:它给你一个网址,脚本跑完在最后打一下:
curl -fsS "https://status.<你的网域>/api/push/<TOKEN>?status=up&msg=OK"超过设定的间隔没收到,就亮红灯。下面这张是它真的没收到心跳时的样子,讯息写得很直白:No heartbeat in the time window。
这样「脚本悄悄死掉」这种最难发现的故障就有人盯了。
所有东西接上之后,后台看起来就是这样,左边一整排心跳条:
v1 → v2:不可逆的迁移#
后来我把 Kuma 从 1.23.17 升到 2.4.0。想升的原因是 2.x 那些进阶功能,而更早之前我就吃过苦头:Kuma 的登入页是公开在网际网路上的,所以帐号一建好就该开 2FA。
这个升级不是换个 tag 那么单纯,几个关键点值得先知道:
CAUTION
1.x → 2.x 有资料库迁移,而且当作不可逆
- 迁移在首次启动 2.x 时自动跑(把逐笔 heartbeat 聚合成新格式),预设维持 SQLite,不强制换 MariaDB。
- 迁移中途不能打断,断了只能从备份还原重来。官方估 20 monitors / 90 天约 7 分钟,慢的硬体更久。
- 官方没写能不能降回 1.x,所以当作不可逆,备份是唯一的退路。
- 迁移期间服务会短暂下线(对交叉监控来说,就是对方那侧会看到你红一下)。
顺序就是:备份 → 改 image 到 2.4.0 → up → 盯着迁移 log → 验证。
实际跑起来比官方估计快很多。我这边 8 个 monitor、12278 笔 heartbeat,迁移 4 秒就跑完:
Uptime Kuma Version: 2.4.0
Aggregate Table Migration Completed
Listening on: ... ✓有一个数字第一眼会吓到:heartbeat 从 12278 变成 10190,少了两千笔。那是正常的:2.x 的迁移会把旧的逐笔 heartbeat 聚合压缩,monitor 一个都没少。
Trust Proxy:开了更危险#
升完 v2 之后,我在设定里看到 Trust Proxy 是关的,而我的 nginx 有送 X-Forwarded-For。关着的话 Kuma 会把所有访客都当成 127.0.0.1,登入速率限制就变成全域的:任何人爆破登入页,会连我自己都一起被锁在外面。
看起来该开。而且开它「应该」是安全的,因为 Kuma 只绑在 loopback、外面连不到,一定得经过 nginx。所以我开了。
然后实测打脸。
$ # 测试 1:不带 X-Forwarded-For
Kuma 记录 → 我的真实 IP ✅
$ # 测试 2:自己伪造一个 X-Forwarded-For
$ curl -H 'X-Forwarded-For: 203.0.113.99' https://status.example.com/...
Kuma 记录 → 203.0.113.99 ❌ 照单全收(203.0.113.99 是 RFC 5737 保留给文件用的网段,不会打到真人。)
Kuma 完全相信了我伪造的标头。 病灶不在 Kuma,在我的 nginx 那一行:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# ↑ 这是「附加」,不是「覆写」 的意思是「把客户端送来的 XFF 保留,再把 $remote_addr 接在后面」。而 Kuma(Express 的 trust proxy)取的是最左边那一个,也就是客户端自己说的那个。于是谁都能宣称自己是任何 IP。
真正的风险:CrowdSec 会封掉 Cloudflare#
我第一时间的判断是「登入速率限制形同虚设,可以无限试密码」。但我已经开了 2FA,试对了也进不去,所以那不是主要问题。
真正的问题在另一个服务身上。我的机器上跑着 ,简单说就是现代版的 fail2ban:它读 nginx 的 access log,发现有人在扫描或爆破就自动把那个 IP 封掉。
NOTE
CrowdSec 本身的架设已经另外写成一篇 它的安装、场景选择、bouncer 怎么接,还有我自己踩过的坑(最经典的是测试自家网站时把自己的 IP 封掉,人在外面连不回家),都在那篇里。这里只需要知道一件事:它的判断完全依赖 access log 里的来源 IP。
CrowdSec 取代 fail2ban:六天挡下 1619 次扫描,和我把自己封锁的三次
而这正是问题所在。追下去才发现拓朴跟我想的不一样:我自己的网域没开 Cloudflare 橘云,但对方的网域开了,而它指向我家:
也就是说,进到我 nginx 的请求有一部分是先经过 Cloudflare 的(实测近期日志约 10% 来自 CF 网段)。这些请求写进 access log 时,$remote_addr 是 Cloudflare 节点的 IP,不是真实访客,而 CrowdSec 就是照这个栏位在判断谁该封。
后果有两个方向,都不好:
- 有人透过那条路径对我扫描/爆破 → CrowdSec 判定攻击来源是 CF 节点 → 把 Cloudflare 的网段封掉。后果是对方的状态页对全世界都连不上,而且极难诊断,因为封的是 CF 不是攻击者。
- 反过来,真正的攻击者躲在 CF 后面,CrowdSec 根本抓不到他,只抓得到 CF。
修法:XFF 改成覆写#
问题出在 nginx 不在 Kuma,两件事一起做:
# 1. 让 nginx 知道 Cloudflare 后面的真实客户端 IP
set_real_ip_from 173.245.48.0/20; # 从 CF 官方清单产生全部网段,定期更新
real_ip_header CF-Connecting-IP;
# 2. XFF 改成覆写,而不是附加
proxy_set_header X-Forwarded-For $remote_addr;第一条让 $remote_addr 变成真正的访客 IP;第二条让客户端送什么进来都被整个丢掉,换成 nginx 自己认定的来源,伪造就失效了。改完之后拿同一组伪造测试再验一次,确认 Kuma 不再吃假 IP。
副作用是好的:CrowdSec 开始看到真实的攻击者 IP,防护实际变强;Kuma 的日志也显示真实 IP,而不是对方那端某个装置注入的 192.168.0.1。
NOTE
$proxy_add_x_forwarded_for 是很多人的预设写法
它本身没错,在「你信任上游、而且要保留完整转发链」时是对的。错的是把它跟「下游应用开 trust proxy 并取最左值」组合在一起,又没有 set_real_ip_from 界定谁可信。我主站的四个 server block 也都是这种写法,只是主站是灰云、CrowdSec 读的是 $remote_addr 没被影响,同一颗地雷躺在那里而已。
一页式清单#
交叉监控 + Kuma 自架检查清单
- 互相监控不用换凭证:对方把子网域 A 纪录指向你,你就自己
certbot --nginx -d <那个子网域>,HTTP-01 会打到你、CA 就发证给你,续签自动。 - 别共享 wildcard:私钥涵盖对方全站,而且每 90 天手动重发一次,两个雷。
- 前提是纯 A 纪录直连,对方挂了 Cloudflare 橘云的话 HTTP-01 打不到你,得改用 Origin Cert。
- 反代 Kuma 一定要有
proxy_http_version 1.1+Upgrade+Connection "upgrade",否则即时图表不动。/socket.io/回 400 是正常的,别误判。 - 后端加公开
/health,别用「接受 401」取巧。 - cron 脚本用 push 型监控,盯的是「没按时跑完」这种沉默故障。
- v1→v2 当作不可逆:先备份资料卷,迁移中途不能断;heartbeat 数量变少是聚合,不是掉资料。
- 登入页公开就开 2FA。
- 开 Trust Proxy 前先确认 XFF 是覆写不是附加,并用
set_real_ip_from界定可信上游;否则开了比不开更危险。
- Uptime Kuma —— 专案本体与 2.x 迁移说明GitHub
- Let's Encrypt —— HTTP-01 / DNS-01 验证方式说明letsencrypt.org
- CrowdSec —— 官方文件docs.crowdsec.net
- Cloudflare —— 还原访客真实 IP(CF-Connecting-IP 与网段清单)Cloudflare Docs
- nginx —— realip 模组nginx.org
还没有留言
✨ 成为第一个留言的人吧