NAS 上的自架網站愈來愈多之後,真正麻煩的往往不是把服務架起來,而是每個後台都有自己的一套登入方式。這次我把原本分散在不同網站、不同程式裡的驗證逐步收斂到 Caddy + Authelia,讓多個後台共用同一個登入入口。實作本身並不複雜,反而是上線後遇到的兩次 404,讓我重新釐清「瀏覽器看到的網址」和「Caddy 實際去找的檔案」之間到底怎麼對應,也順便把 Synology NAS 權限與 Docker 重建的差別重新整理了一次。
一、為什麼開始做統一登入?
我的 Synology NAS 上陸續累積了好幾個自架網站:相簿、部落格、資訊站、桌遊站、小工具,以及可以直接操作 Codex CLI、Claude Code CLI 的瀏覽器終端。這些服務不是同一天架好的,每一套程式也不是用同一種方式設計,所以後台登入自然愈來愈零散。
有的網站使用自己的 Session 登入,有的只是共用密碼,有的使用 HTTP Basic Auth,有的則直接沿用應用程式本身的帳號系統。平常還能靠瀏覽器記住密碼,一旦要換電腦、清 Cookie,或從手機臨時進去維護,就會開始想:「這一個後台到底是哪一組密碼?」
更實際的問題是,驗證邏輯分散之後,密碼強度、更新習慣與安全性也很難一致。這次盤點時,就真的發現其中一個舊後台還在使用早期設定的預設密碼。
所以這次沒有打算把所有網站重寫成同一套會員系統,而是在最外層的反向代理先加一道共用驗證。使用者先在 Authelia 證明「我是誰」,通過之後,Caddy 才把請求交給真正的網站。

二、先盤點:原本每個網站怎麼驗證身分
正式改設定之前,我先把各網站既有的登入方式列成表格。這一步很值得做,因為不是每個服務都適合把原本驗證直接拆掉。
| 網站(示範名稱) | 原本的登入方式 | 盤點後的想法 |
|---|---|---|
wordpress |
WordPress 內建帳號系統 | 本身已經是成熟的帳號與權限系統,沒有必要為了 SSO 立刻拆掉。 |
photo |
自寫 Session 登入、單一共用密碼 | 功能簡單,但啟動 Log 顯示仍使用預設值,這次盤點才發現密碼從未真正更換。 |
heartteam |
另一套自寫 Session 登入 | 和 photo 是不同程式,各自有自己的密碼與登入邏輯。 |
tabletop |
後台自己驗證一組密碼 | 驗證邏輯直接寫在應用程式中,之後可以考慮改成信任反向代理傳來的身分。 |
dietcalc |
網址層級共用密碼,用 Cookie 記住 | 只是一個小工具,沒有真正的帳號概念,暫時不急著整合。 |
| 瀏覽器終端機 | Web Terminal 內建 HTTP Basic Auth | 可在外面再疊 Authelia,原本 Basic Auth 先保留。 |
三、為什麼選 Caddy + Authelia?
我的網站原本就全部先經過 Caddy,再由 Caddy 依網址路徑轉到不同 Container 或內部服務。既然所有流量本來就經過同一個入口,統一驗證最合理的位置自然也是這一層。
Authelia 可以和 Caddy 的 forward_auth 配合:瀏覽器要進入受保護路徑時,Caddy 先把驗證工作交給 Authelia。若尚未登入,就導向登入頁;驗證成功後,再把原本的請求繼續交給後端網站。
這樣做最大的好處,是不用先修改每一套網站的程式。即使某個舊網站只有最簡單的密碼保護,只要它的入口都經過 Caddy,就可以先在外層加 Authelia。
同時,我也沒有把「統一登入」理解成「所有網站原本的密碼都必須立刻刪掉」。對比較重要、但還沒完全確認驗證流程的網站,我反而先保留原本的登入,讓 Authelia 當第一層,原站密碼當第二層。
四、最後採用的驗證架構
整體流程可以簡化成下面這樣:
Internet
↓
HTTPS / 443
↓
Caddy Router
↓
受保護路徑? ── 否 ──→ 直接送到公開網站
│
是
↓
Authelia forward_auth
↓
尚未登入 ──→ Authelia 登入頁
│
已登入
↓
Caddy 把原請求送往真正的後端
↓
photo / heartteam / tabletop / Web Terminal ...
Authelia 處理的是「這個請求是否已經通過身分驗證」。各個後端網站則可以依需要決定:
- 保留原本自己的登入,形成兩層驗證。
- 移除重複的密碼驗證,改成信任 Caddy 傳來的使用者身分。
- 公開頁面完全不經過 Authelia,只保護
/admin、Web Terminal 或其他管理路徑。
這比「把整個網站都鎖起來」更符合我的需求,因為相簿、文章與一般查詢頁本來就是公開內容,我真正想保護的是管理入口。
五、哪些網站先納入、哪些保留原本登入
這次沒有追求一次把所有系統改完,而是先從風險比較高、實際維護時最常進去的後台開始。
| 網站 | 目前做法 | 說明 |
|---|---|---|
photo |
Authelia + 原登入 | 先過 Authelia,進入原本後台後仍需輸入既有密碼。新架構穩定前先保留第二層。 |
heartteam |
Authelia + 原登入 | 和 photo 相同,先疊加而不是直接拆除舊驗證。 |
tabletop |
只用 Authelia | 原本應用程式內的重複密碼驗證移除,改由 Caddy 傳遞已驗證的使用者身分。 |
| 瀏覽器終端機 | Authelia + Basic Auth | 終端機本身的 Basic Auth 先保留,外層再增加 Authelia。 |
wordpress |
暫不納入 | 已有完整帳號系統,這次先維持原狀。 |
dietcalc |
暫不納入 | 只是簡單工具,不需要為了這次改造一起動。 |
photo、heartteam、tabletop、dietcalc 等名稱說明,實際使用時請換成自己的專案與路徑。六、替所有後台做一個管理入口頁
把驗證統一之後,還剩下一個很日常的問題:每個後台網址還是不同。既然已經有一個共同登入,乾脆再做一個只有登入後才能看到的管理入口,把常用後台全部集中在同一頁。
這個頁面不需要資料庫,也不需要另外寫後端,只是一個純 HTML:
<a href="/photo/admin/">Photo 後台</a>
<a href="/heartteam/admin/">HeartTeam 後台</a>
<a href="/tabletop/admin.html">Tabletop 後台</a>
<a href="/codex/">Codex Web Terminal</a>
我把它放在 NAS 的一個獨立資料夾,再由 Caddy 的 root + file_server 提供。這個入口頁本身也經過 Authelia,所以沒有登入的人連管理選單都看不到。
原本以為這會是整個架構最沒有技術含量的一塊,結果真正花最多時間的問題,偏偏就出在這個純靜態頁面。
七、Caddy 的 forward_auth 設定方式
我先把 Authelia 驗證整理成一個共用片段。之後哪個後台要納入,只要在對應路由裡 import 即可:
# Caddyfile:共用驗證片段
(authelia_guard) {
forward_auth 127.0.0.1:19091 {
uri /api/authz/forward-auth
header_up X-Forwarded-Proto https
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
}
# 範例:保護 photo 後台
handle /photo/admin* {
import authelia_guard
reverse_proxy 127.0.0.1:PHOTO_PORT
}
Authelia 的帳號與密碼集中放在同一套設定,密碼使用 bcrypt 雜湊儲存。驗證成功後,Caddy 可以把 Remote-User、Remote-Groups 等標頭帶到後端;是否真的使用這些資料,則由後端程式自己決定。
對 photo、heartteam 這類還保留舊登入的網站來說,Authelia 只是第一層。對 tabletop 這種已經移除舊密碼驗證的後台來說,則必須確保外部請求不能自行偽造後端信任的身分標頭。
八、上線隔天:登入成功,管理入口卻 404
第一天把 Authelia、受保護後台與管理入口接好之後,測試看起來都正常。隔天重新從瀏覽器登入,卻遇到一個很奇怪的狀況:Authelia 可以正常顯示登入畫面,帳號密碼也驗證成功,但跳回管理入口後,瀏覽器直接顯示 HTTP 404。
更容易誤判的是,這時如果手動輸入其他受保護後台網址,又可以正常進去。也就是:
- Authelia 本身有運作。
- 登入 Cookie 有建立。
- 其他受保護的 reverse proxy 路由正常。
- 只有那個純 HTML 管理入口讀不到。
這種症狀看起來很像「登入後導向壞掉」,所以一開始花了不少時間在驗證與 Container 設定上,後來才發現 Authelia 從頭到尾都不是問題。
九、第一個誤判:以為是 Docker 沒有重建
管理入口的靜態檔案掛載,是在原本 Caddy Container 已經存在之後才加入 Compose。這立刻讓我想到之前踩過的 Docker 問題:restart 只是重啟既有 Container,不會重新套用後來新增的 bind mount。
# 只重啟既有 Container
sudo docker compose restart caddy-router
# Compose 偵測設定差異,必要時重新建立 Container
sudo docker compose up -d caddy-router
執行後,畫面也真的顯示 Caddy Container 被 Recreate。這讓人很容易產生「應該就是這個了」的錯覺。
但重新登入後,管理入口仍然 404。
這個知識點本身沒有錯:新增或修改掛載後,單純 restart 的確不等於重新建立 Container。錯的是把一個曾經遇過的問題,直接套到這次的症狀。
十、第二個誤判:以為是 Synology ACL
既然 Container 已經重建,下一個懷疑對象自然變成檔案權限。尤其 Synology NAS 的權限不只傳統 Linux 的 rwx,還有自己的進階 ACL;有時候你用 ls -l 看到的 Unix mode,並不能完整代表 DSM 最後實際套用的權限。
我剛好又看到另一個正常運作的靜態目錄,Linux 權限看起來是很嚴格的 700,線上卻照樣讀得到。於是注意力完全被帶往 ACL,接著陸續做了幾件事:
- 把出問題的資料夾改成
777。 - 從 Synology Container Manager 再重新建置整個服務。
- 到 DSM 檔案總管檢查進階 ACL,並重新套用到子目錄與檔案。
- 把原資料夾重新命名,再建立一個全新的資料夾,讓它重新繼承上層權限。
結果四個方法全部沒有作用,404 完全沒變。
十一、真正原因:網址被多帶了一層 /manage
重建 Container、檢查掛載、調整 Synology ACL 都沒有改變結果後,我改了一個方向:先不要管 Authelia,也不要管登入流程,只測「Caddy 到底能不能把這個 HTML 檔案送出來」。
我另外做了一條暫時的診斷路由,直接指向同一個資料夾,而且刻意不經過 Authelia:
# 暫時診斷用,不經過 Authelia
handle /manage-diag/* {
root * /srv/manage
file_server
}
結果連這條最單純的路由都一樣讀不到測試檔。這一步很重要,因為它把問題範圍縮得很小:帳號密碼、Cookie、Authelia 登入頁都不是原因,問題就在 Caddy 怎麼把網址轉成實際檔案路徑。
真正看懂問題,是把「瀏覽器要求的網址」和「NAS 裡真正的檔案位置」並排之後:
瀏覽器要求的網址:
/manage/index.html
真正的檔案位置:
/srv/manage/index.html
這裡的 /manage 只是網址上的入口名稱,不是實際資料夾名稱。也就是說,Caddy 收到 /manage/index.html 之後,真正去硬碟找檔案時,應該把前面的 /manage 拿掉,最後才會對到 /srv/manage/index.html。
但原本的設定是:
handle /manage/* {
import authelia_guard
root * /srv/manage
file_server
}
這裡的 handle 會負責比對「是不是 /manage/... 這條路徑」,卻不會自動把 /manage 從後面的路徑拿掉。因此 Caddy 最後等於把兩邊接在一起:
root: /srv/manage
網址路徑: /manage/index.html
最後去找:
/srv/manage/manage/index.html
真正存在的檔案明明是 /srv/manage/index.html,Caddy 卻多找了一層 manage,所以一定是 404。這也解釋了為什麼前面怎麼重建 Container、怎麼改 ACL 都沒有用:不是「沒有權限讀檔案」,而是「從一開始就找錯檔案」。
handle 與 handle_path 想成這樣就比較容易:handle 只是在門口認出「這是 /manage 的請求」,原本的網址還完整保留;handle_path 除了認出這條路徑,還會順手把前面的 /manage 拿掉。這次真正需要的,其實就是「在適當的時間把 /manage 拿掉」。十二、Authelia 驗證在前,移除 /manage 要放在後面
找到「多了一層 /manage」之後,最直覺的修法是把 handle 換成 handle_path。因為 handle_path 會自動移除前面的路徑,照理說就能讓:
/manage/index.html
↓
/index.html
↓
/srv/manage/index.html
如果這只是一個普通的靜態頁面,這樣改就很合理。但這個管理入口前面還有 Authelia,所以還多了一個「先後順序」的問題。
Authelia 在驗證時需要知道使用者原本想去的網址。若 Caddy 太早就把 /manage 拿掉,Authelia 看到的路徑可能已經不是瀏覽器原本要求的 /manage/...;登入完成後,返回位置就可能不如預期。
因此我最後沒有直接改成 handle_path,而是保留 handle,把處理順序寫清楚:
- 瀏覽器先要求完整的
/manage/...網址。 - Authelia 先用這個完整網址做驗證。
- 驗證通過後,Caddy 才把前面的
/manage拿掉。 - 最後
file_server再去/srv/manage裡找真正的 HTML 檔案。
設定最後變成:
handle /manage/* {
route {
import authelia_guard
uri strip_prefix /manage
root * /srv/manage
file_server
}
}
這裡的 uri strip_prefix /manage,意思就是「把網址開頭的 /manage 拿掉」。而 route 則是把處理順序固定下來,讓驗證先做、改路徑後做。
所以整個流程可以直接讀成:
瀏覽器:/manage/index.html
↓
Authelia:先看到完整的 /manage/index.html
↓ 驗證成功
Caddy:拿掉 /manage
↓
剩下:/index.html
↓
root /srv/manage
↓
真正讀取:/srv/manage/index.html
handle、handle_path、strip_prefix 這些名稱,而是先畫出「瀏覽器網址 → 驗證時看到的網址 → 去掉入口前綴 → 實際檔案位置」。只要每一步的路徑對得起來,Caddy 指令就會容易理解很多。十三、修完第一個 404,又遇到第二個 404
管理入口終於可以正常打開後,我開始逐一點選裡面的後台連結。結果 tabletop 又跳出 404。
這次有了前面的經驗,就沒有再把責任丟給 Authelia。檢查後發現驗證流程完全正常,錯的是入口頁連結本身:
<!-- 原本誤寫成資料夾 -->
<a href="/tabletop/admin/">Tabletop 後台</a>
<!-- 真正存在的是單一 HTML 檔 -->
<a href="/tabletop/admin.html">Tabletop 後台</a>
兩次都是 404,第一個是 Caddy 把檔案路徑組錯,第二個是 HTML 連結寫錯;瀏覽器畫面看起來幾乎一樣,實際卻是完全不同層級的問題。
因此現在遇到受保護頁面 404,我會先確認它到底發生在:
- 驗證以前:請求根本沒有進到正確路由。
- 驗證過程:Authelia 或導向位置有問題。
- 驗證以後、Caddy 這層:rewrite、strip prefix、file_server 或 reverse proxy 路徑錯誤。
- 後端應用程式:Caddy 已經送到了,但後端自己沒有該 URL。
十四、日常維護與安全邊界
目前我沒有把 Authelia 當成「裝完之後所有舊驗證都可以刪掉」的理由,而是依每個服務的重要性與成熟度分開處理。
photo、heartteam:Authelia 外層 + 原站登入,保留兩層。tabletop:移除重複密碼,改成信任反向代理提供的已驗證身分。- Web Terminal:Authelia 外層 + 原本 Basic Auth。
wordpress:已有自己的成熟帳號系統,暫時不動。- 管理入口本身也受 Authelia 保護,不公開列出後台網址。
- 盤點時發現的舊預設密碼已另外更換,不因為前面多一層 SSO 就放著不管。
如果後端要直接信任 Remote-User 這類標頭,前提必須是後端不能被外部繞過 Caddy 直接存取,而且外部進來的同名標頭不能原封不動被信任。這一層如果沒有封好,原本方便的 SSO 反而可能變成新的驗證弱點。
另外,Authelia 密碼設定、Session Secret、Storage Encryption Key 等敏感資料不應該貼進公開文章、Git repository 或除錯截圖。文章中的帳號、Port 與路徑也最好使用示範值。
十五、這次整理出的疑難排解表
| 症狀 | 優先檢查 |
|---|---|
| Authelia 登入成功,但受保護靜態頁仍然 404 | 先另外做一條不經 Authelia 的最小診斷路由。若同一檔案仍讀不到,就先查 Caddy 路徑、掛載與 file_server,不要繼續查登入。 |
| Compose 新增 bind mount 後,Container 看不到檔案 | docker compose restart 不會重新建立掛載;用 docker compose up -d 讓 Compose 依新設定 Recreate,再用 docker inspect 確認 Mount Source / Destination。 |
| Linux 權限看起來不合理,DSM 裡卻可正常存取 | Synology 還有 ACL 進階權限,不要只看 chmod 數字;但也不要一看到 NAS 檔案問題就直接認定是 ACL。 |
/manage/index.html 一直 404,但實際檔案明明在 /srv/manage/index.html |
先看 Caddy 是否把網址上的 /manage 又帶進實際檔案路徑,變成不存在的 /srv/manage/manage/index.html。handle 會保留這段前綴;真正要做的是在讀取檔案前把 /manage 拿掉。 |
拿掉 /manage 後,Authelia 登入完成卻回到錯誤位置 |
代表網址前綴可能移除得太早。讓 Authelia 先看到完整的原始網址,驗證成功後再用 uri strip_prefix /manage 移除前綴;需要時用 route 固定執行順序。 |
| 管理入口正常,但點某一個後台又 404 | 先直接確認該後台真正存在的 URL。不要因為前一個 404 是 Caddy,就假設下一個也是同一原因。 |
已驗證後端直接信任 Remote-User |
確認後端只能經過受控反向代理存取,並避免外部自行注入同名身分標頭。 |
這次把 Authelia 加進 NAS 後,最大的改變不是「所有網站終於共用同一組密碼」,而是多了一個明確的驗證邊界:外部請求先經過 Caddy,再由 Authelia 判斷身分,最後才進真正的管理介面。後端要保留第二層、還是完全信任這個邊界,可以逐站調整,不需要一次把舊系統全部推倒重做。
而兩次 404 反而比 SSO 本身更值得記錄。第一個問題提醒我,看到登入畫面正常,不代表錯誤一定在驗證;第二個問題則證明,同樣都是 404,也可能一個發生在反向代理、一個只是 HTML 連結寫錯。把問題拆成「驗證、路由、檔案、後端」四層逐一確認,比同時改十個設定有效得多。
參考資料
這篇文章已有 6 次瀏覽
發佈留言