前言:聖誕節沒等到禮物,卻等來了 VPS CPU 的 100%
2025 年 12 月,為了強化連線品質,把一部分原本放在德國伺服器託管商的站台,搬到有台灣機房的託管商。
27 日,那本來是個平靜的週末,我還在想要不要幫公司的吉祥物『鍛碼匠』換個新年造型。結果在 push 一個小更新時,部署的容器卡了一個小時才起來,VPS 的 CPU 與 RAM 使用率直接飆到 100%。我以為是某個專案突然爆紅,滿懷期待打開監控——沒有流量,只有一個在 Docker 容器深處悄悄跑的 miner 執行檔。
這是當時的真實截圖與 log 紀錄(為了保護其他客戶資訊做了去識別化處理):
> [圖:Coolify Cloud 後台的 CPU 監控曲線,12/27 14:00 起從 5% 瞬間拉到 100% 並持續 6 小時以上]
駭客用的是教科書級的 RCE 攻擊鏈。kill 掉行程,幾秒後又復活;rebuild 容器,過一會兒又長出來。那個週末我學到一件事:容器不是保險箱,它只是薄薄一層塑膠袋;要變成防彈背心,得靠「最小權限」跟「唯讀檔案系統」。
事件復盤:駭客是怎麼溜進來的?
事後我做了數位鑑識。這次入侵由三個弱點串成:
> RCE(Remote Code Execution)= 駭客在遠端機器執行任意程式碼的能力,與本地端權限無關。OWASP 在 2021 年發布的 Top 10 Web Application Security Risks 中,「Broken Access Control」與「Injection」類別多次與 RCE 風險相關(OWASP Top 10 2021)
-
軟體漏洞:被入侵的專案用的是 Next.js 15.5.4,這個版本含有 Critical 等級的 RCE 漏洞(GitHub Advisory GHSA-9qr9-h5gf-34mp / CVE-2025-55182,CVSS 10.0,2025-12-03 發布)。該漏洞源自 React 19.0.0-19.2.0 的 Server Components 協定,連帶影響 Next.js 15.x 與 16.x(Next.js 官方安全公告 CVE-2025-66478)。要查某個 Next.js 版本是否受影響,可以直接看 Next.js 官方的 Security Advisories 列表。,導致資料外洩或系統控制。
-
Root 執行:容器預設以 root 跑。駭客進來後在容器裡就是 root,能在
/app下載挖礦程式、改啟動腳本,甚至安家落戶。Docker 官方文件在 Docker security non-root user 指南 明確建議正式環境一律以非 root 身份執行。這樣的設定使得駭客可以無限制地執行任何命令,增加了系統被攻擊的風險。 -
/tmp可執行:駭客把腳本藏在/tmp,因為 Linux 預設允許/tmp執行二進位檔。Linux Foundation 與 NIST 的容器安全文件 NIST SP 800-190(Application Container Security Guide) 第 4.4 節直接把「以 root 執行 + 可寫入的暫存目錄」列為容器隔離失效的高風險組合。這使得駭客能夠在不被發現的情況下執行惡意程式碼。
症狀紀錄(我那天看 log 看到的):
- VPS 的 CPU / RAM 長期 100%。
/app出現不明檔案miner。/tmp出現惡意 shell script(檔名是隨機 12 碼英數)。crontab -l多了一筆每分鐘跑的排程。
防禦哲學:洋蔥式縱深防禦
痛定思痛後,單靠升級版本是不夠的——明天又會有新的 0-day。我們需要的是縱深防禦(Defense in Depth),這是資安領域的經典框架,NIST 在 SP 800-190 與 SP 800-53 Rev. 5 都有完整論述。
策略分四層:
- 修補漏洞:關上大門,定期更新所有依賴,確保使用最新的安全版本。具體步驟包括使用
npm outdated檢查過期的依賴,然後使用npm audit掃描已知漏洞,最後用npm audit fix自動修補(npm audit 官方文件,2026)。注意:npm update只更新 semver 範圍內的版本,不會自動升級 major 版本,因此對 critical RCE 漏洞不一定有效。。 - 最小權限:即使進來了,也做不了什麼,確保容器以非 root 身份運行,限制其權限。可以在 Dockerfile 中使用
USER指令來指定非 root 使用者。 - 唯讀系統:想做壞事也寫不進去,設置檔案系統為唯讀,防止未經授權的寫入。這可以通過在 Dockerfile 中使用
VOLUME指令來實現。 - 執行限制:寫進去了也跑不起來,使用
noexec參數限制可執行檔的執行。這可以在docker run命令中指定。
實戰一:Dockerfile 的斷捨離(最小權限原則)
很多 Dockerfile 是直接 FROM node 然後 CMD ["npm", "start"],開發環境沒問題,正式環境就是裸奔。OWASP 在 Docker Top 10 裡就把「Image 中的 Root 預設」列為 D01(Root User 風險)。
正解:建立低權限專用使用者 + 限制檔案歸屬。
> ... (前面的 Build Stages 省略) ...
FROM node:20-bullseye-slim AS production
> 1. 建立受限使用者:明確指定 UID 1001,避免與宿主機權限混淆
RUN groupadd --gid 1001 nodejs && \
useradd --uid 1001 --gid nodejs --shell /bin/bash --create-home nextjs
> ... (複製編譯好的檔案) ...
> 2. 權限加固:Root 擁有,nextjs 唯讀
> 例外:只開放「必須寫入」的目錄
RUN mkdir -p .next/cache && chown nextjs:nodejs .next/cache
> 如果有用 Strapi 或其他 CMS,上傳目錄也需要開放
> 3. 切換使用者:放棄 Root 權杖
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
關鍵三行:
USER nextjs:應用程式不再以 root 身份運行,降低了潛在的攻擊面。- 程式碼由 root 擁有,執行者是 nextjs,形成「唯讀」的物理基礎。
- Docker 官方在 Dockerfile USER 指令 明確建議「正式環境必須指定 USER」。
實戰二:Next.js Standalone 瘦身與加固
Next.js 有 Standalone 輸出模式,會自動追蹤依賴,只打包必要的檔案。檔案越少,攻擊面越小。設定只要在 next.config.ts 加一行:
// next.config.ts
const nextConfig: NextConfig = {
output: "standalone",
};
Image 更小、部署更快、攻擊面更窄。根據 Next.js 官方文件,這樣的配置能有效減少不必要的依賴,進一步提升安全性。
實戰三:終極部署參數(唯讀與 Noexec)
這是整套防禦裡最關鍵的一環。我們用 docker run 參數強制上鎖(如果用 Coolify Cloud,可以在 Docker Options 加入同樣的參數):
> 1. 鎖死 /tmp,禁止執行 (noexec)
--tmpfs /tmp:rw,noexec,nosuid,size=64m
> 2. Next.js Cache 掛在記憶體中,重啟即焚
--tmpfs /app/.next/cache:rw,noexec,nosuid,mode=1777
> 3. 其他暫存與上傳區
--tmpfs /app/cms-admin/.tmp:rw,noexec,nosuid,mode=1777
--tmpfs /app/cms-admin/public/uploads:rw,noexec,nosuid,mode=1777
這三個參數的意義:
noexec:就算駭客成功把 miner 丟進/tmp,執行時系統只會冷冷回一句Permission denied。noexec是 Linux kernel 的 mount option(man mount 8),跟 seccomp 是不同層級的安全機制 — seccomp 是 syscall 過濾,noexec是檔案系統層級的執行權限。Docker 官方在 tmpfs mounts 文件 把它跟nosuidnodev並列為容器安全 baseline。nosuid:防止提權攻擊,確保即使駭客獲得了執行權限,也無法提升權限。tmpfs:這些目錄掛在記憶體。容器一重啟,潛在的病毒殘留就會瞬間被清掉,降低了持久性威脅。
緊急應變 SOP:當災難再次降臨時
資安沒有 100% 的安全。如果監控警報又響了,按這五步走,順序很重要:
- 止血(切斷連線):立刻
docker stop該 container,不要猶豫。 - 保全證據:不要急著刪!用
docker commit把中毒現場存成 Image,之後開隔離環境分析。NIST SP 800-61 Rev. 2(電腦事件處理指南)強調「證據保全先於系統還原」。 - 溯源(看 Log):檢查 access log,找出駭客從哪個端點打進來的,這可以幫助未來的防範措施。使用
grep指令來過濾特定的 IP 或 URL。 - 焦土政策(Rotate Secrets):假設所有環境變數(DB 密碼、AWS Key、API Secret)都已經外洩,全部重設。不要心存僥倖。
- 重生(Clean Build):跑
npm audit修依賴漏洞,用--no-cache強制重新構建映像檔,確保所有依賴都是最新的安全版本。
結語:付費學費後的領悟
那個週末損失了一些電費與假期,但對容器安全有了具體理解。容器預設的 root + 可寫入的 /tmp,是 NIST 與 OWASP 文件反覆點名的兩個高風險組合。修補這兩項不需要昂貴工具,只要改 Dockerfile 跟 docker run 參數。
VPS 後來恢復平靜,鍛碼匠也回到吉祥物的正職,而不是被迫當礦工。
如果你也在用 Next.js + Docker,趁駭客敲門之前,先把鎖換好。也記得定期看 Next.js Security Advisories 跟 GitHub Dependabot alerts,該升級就升級。








