🔑 關鍵洞察
✦ 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。
而這正是問題所在。追下去才發現拓樸跟我想的不一樣:我自己的網域沒開 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
還沒有留言
✨ 成為第一個留言的人吧