K8s Web Container logs/batch Permission Denied:三層修正完整方案 + AutoBuildSiteDeploy 第二攻擊向量(融合版 v4)¶
文檔資訊
- 分類: debugging
- 難度: intermediate
- 預估閱讀時間: 40 分鐘
- 標籤:
kubernetes,docker,permissions,jenkins,php,monolog,init-container,www-data,chown,ecr,groovy-pipeline,efs,volume-mount,docker-entrypoint,supervisord,permission-denied,autobuildsite-deploy
摘要¶
PHP web container 的 logs/batch/<date>/ 目錄因 init-all.sh 以 root 執行觸發 Monolog 自動建立日期子目錄,owner 為 root:root,導致後續 cron(www-data)寫不進去。
對話歷經三個調查階段(qa-test 初始診斷、rowie 驗證、EFS 假說排除)並最終確認三層修正方案有效。v4 融合重點:整合了完整 Jenkins Build #133 log(逐步確認 Layer ½/3 生效、4/4 腳本成功)以及 AutoBuildSiteDeploy Build #622 log(job path 為 AutoBuildSiteDeploy/builds/622,確認這條獨立 pipeline 在 BuildPSWebImage 修正後仍以 root 觸發 init-all.sh,是問題持續重現的第二攻擊向量)。
關鍵學習¶
-
Docker image build 時應預建 logs/ 子目錄並 chown 給 runtime user,避免執行期 Monolog 自動 mkdir 造成 owner 錯誤
-
Dockerfile 預建目錄只能保護目錄本身,無法阻止 init-all.sh 執行後 Monolog 自動建立的日期子目錄繼承 root:root owner
-
docker-entrypoint.sh 是最佳的第二層防護:在 supervisord 啟動前執行
chown -R 1000:1000 /var/www/html/Gamania/logs,但只對容器啟動時有效,對容器啟動後的 root 操作無效 -
init-all.sh 應以 www-data 執行而非以 root 執行後補 chown:
su -s /bin/bash www-data -c 'bash init-all.sh',從根源防止問題 -
AutoBuildSiteDeploy 是第二個攻擊向量。它在容器啟動後(Layer 2 chown 已執行完)再次以 root 執行 init-all.sh,導致 Monolog 重新建出 root:root 日期子目錄
-
所有會觸發 init-all.sh 的 pipeline(BuildPSWebImage-MakeWish 和 AutoBuildSiteDeploy)都必須套用
su -s /bin/bash www-data修正——這不是一次性修正 -
chmod 777 是暫時救火但錯誤的做法,正確方式是 chown -R 1000:1000(www-data uid),遵循最小權限原則
-
EFS 持久化假說已排除:kubectl volumeMount 確認 func-web container 無 EFS/PVC 掛載,logs/ 不跨 pod 持久化
-
同版號 ECR image tag 不會被自動覆蓋更新,需確保 webImageVersion 遞增
-
手動刪除日期目錄後讓 cron 重建,可快速佐證「cron 以 www-data 建目錄是正確的」這個假說
技術細節¶
問題根因¶
init-all.sh 在 Jenkins pipeline 中以 root 身分執行(container 內 uid=0(root)),其中呼叫 PHP(透過 Monolog)寫 log 時,Monolog 會自動 mkdir 建立當日日期子目錄 logs/batch/2026-04-10/。
由於此時 process owner 是 root,建出來的目錄 owner = root:root,mode = 755,導致之後以 www-data(uid=1000)執行的 cron job 嘗試寫入時拿到 Permission denied。
三層修正架構¶
Layer 1 — Dockerfile 預建目錄
# 預建 logs/batch 避免 init-all.sh root 執行時 Monolog 自動 mkdir 出 root:root 的目錄
RUN mkdir -p /var/www/html/Gamania/logs/syncdb /var/www/html/Gamania/logs/batch
RUN chown -R 1000:1000 /var/www/html/
保護目錄本身的 owner,但無法阻止 Monolog 在目錄內建立的日期子目錄。Build #133 log 中可見 #10 [4/8] RUN mkdir -p ... CACHED 確認此層已生效。
Layer 2 — docker-entrypoint.sh chown(容器啟動時)
# 啟動前將 logs/ 擁有者統一為 www-data (uid=1000)
chown -R 1000:1000 /var/www/html/Gamania/logs
supervisord -n -c /etc/supervisord.conf
有效對象:容器啟動時。對容器啟動後由外部 pipeline 觸發的 root 操作無效。Build #133 後 debug tool 確認此行存在於 /docker-entrypoint.sh 最後 5 行。
Layer 3 — pipeline 以 www-data 執行 init-all.sh(根治)
// 正確:以 www-data 執行,從根源防止 root:root 目錄問題
kubectl exec --context ${ctx} -i $POD_NAME -n ps -- \
su -s /bin/bash www-data -c 'bash /var/www/html/env/scripts/init-all.sh rowie'
Build #133 log 確認:su -s /bin/bash www-data -c 'bash ...init-all.sh rowie' 成功執行,4/4 腳本(01-pg_db_init, 02-pg_db_init_share, 03-migrateDB, 04-batchOnce)全部成功。
三階段調查佐證¶
Phase 1 — qa-test 初始狀態:
drwxr-xr-x. 3 root root 24 Apr 8 11:14 batch ← root:root!
drwxr-xr-x. 1 www-data www-data 22 Apr 2 10:40 cron
Phase 2 — rowie(只有 Layer 1 Dockerfile 修正):
# batch 本身
drwxr-xr-x. 1 www-data www-data 24 Apr 10 12:01 batch ← www-data ✓
# batch 內容:日期子目錄仍是 root:root!
drwxr-xr-x. 2 root root 16384 Apr 10 12:01 2026-04-10
Phase 3 — 手動刪除後 cron 重建:
EFS 持久化假說排除¶
kubectl volumeMount 確認:
[
{"mountPath": "/config/GlobalSettings.json", "name": "global-settings", "readOnly": true},
{"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", ...}
]
What Changed¶
Build #133 完整 Jenkins log 確認三層修正全部生效¶
新對話提供了完整的 Jenkins Build #133 執行紀錄。Docker build 階段可見:#10 [4/8] RUN mkdir -p .../logs/batch CACHED(Layer 1)、#11 [7/8] COPY ./env/scripts/docker-entrypoint.sh CACHED(Layer 2)。Restart Web Server 階段改為 su -s /bin/bash www-data -c 'bash ...init-all.sh rowie'(Layer 3),執行結果 4/4 腳本全部成功,包含 DB init、Migration(68 個表 all up)、Seeding、batchOnce 批次任務。
AutoBuildSiteDeploy Build #622 提供完整 log 確認第二攻擊向量¶
Build #133 成功後,用戶發現 2026-04-10 目錄仍為 root:root(drwxr-xr-x. 2 root root 16384 Apr 10 20:16)。用戶提供了 AutoBuildSiteDeploy Build #622 的 Jenkins log,log 路徑明確顯示 AutoBuildSiteDeploy/builds/622,確認這條完全獨立的 pipeline 在 BuildPSWebImage-MakeWish 修正後仍在執行,並以 root 觸發 init-all.sh,繞過了 Layer 2 的防護。
v3 vs v4 的差異:佐證細節強化¶
v3 已正確識別兩個攻擊向量並提出理論架構;v4 的新增價值在於整合了完整的 Jenkins log 原始輸出作為證據鏈——Build #133 log 中每一個 Docker build step 的 CACHED 確認、Layer 3 su www-data 執行結果、以及 AutoBuildSiteDeploy Build #622 的 job path 都成為鐵證,讓「BuildPSWebImage 已修好,AutoBuildSiteDeploy 未改」這個診斷在 log 層面完全可重現。
So What¶
這個問題揭示了一個 Docker + K8s 環境中的常見隱藏地雷:多條 pipeline 共用同一套 init script,若只修正其中一條,問題在另一條執行時仍會重現。
更深層的教訓是架構層面的:docker-entrypoint.sh 的 chown 只能保護「容器啟動時」的狀態,Layer 2 是啟動時的保護,Layer 3(su www-data)才是執行時的根治。任何在容器啟動後以 root 身份 exec 進容器的操作,都可以繞過 Layer 2 的防護。
這個模式在有多條 CI/CD pipeline 都會進入容器執行初始化腳本的架構中非常普遍。修正不能只在一條 pipeline 套用,而是必須在所有入口點一致套用——這是一個必須在組織層面建立規範的架構原則,不是一次性的 bug fix。
Trade-offs¶
- Dockerfile 預建目錄 vs initContainer:Dockerfile RUN mkdir 簡單直接,但每次 image rebuild 才能更新;改用 initContainer 可以更動態,但增加 K8s manifest 複雜度
- docker-entrypoint.sh chown vs 改 init-all.sh 本身:entrypoint chown 是全面性防護,不管誰建的目錄都能修正;缺點是每次容器啟動都要跑 chown,若 logs/ 子目錄很多會稍微拖慢啟動時間;且對容器啟動後的 root 操作無效
- pipeline 改以 www-data 執行 vs 繼續以 root 執行後補 chown:以 www-data 執行是更乾淨的根治方案,不依賴事後補救;Build #133 log 確認 init-all.sh 的所有操作(DB 連線、Migration、Seeding)在 www-data 權限下都能正常執行
- chown 1000:1000 vs chmod 777:chown 是正確的最小權限做法;chmod 777 雖然快速救火,但開放所有 process 寫入,違反最小權限原則,不應用於 production
- 修正覆蓋範圍:只修正 BuildPSWebImage-MakeWish 是不夠的,所有呼叫 init-all.sh 的 pipeline(含 AutoBuildSiteDeploy)都需要套用
Try It Fast¶
# 驗證當日日期子目錄 owner(最常出問題的位置)
kubectl -n ps exec <pod-name> -c func-web -- ls -la /var/www/html/Gamania/logs/batch/
# 期待:2026-04-10/ 目錄 owner 為 www-data,而非 root
# 確認 container 是否有 EFS/PVC volumeMount(排除持久化假說)
kubectl -n ps get pod <pod-name> -o jsonpath='{.spec.containers[0].volumeMounts}' | python3 -m json.tool
# 確認 docker-entrypoint.sh 是否有 chown 那行(Layer 2)
kubectl -n ps exec <pod-name> -c func-web -- grep -n 'chown.*1000.*logs' /docker-entrypoint.sh
# 期待輸出:chown -R 1000:1000 /var/www/html/Gamania/logs
# 手動以 www-data 執行 init-all.sh(驗證 Layer 3 修正)
kubectl exec --context gamania-ps-dev -i <pod-name> -n ps -- \
su -s /bin/bash www-data -c 'bash /var/www/html/env/scripts/init-all.sh rowie'
# 臨時救火:手動 chown(pod 重啟後失效)
kubectl --context gamania-ps-dev -n ps exec <pod-name> -c func-web -- \
chown -R 1000:1000 /var/www/html/Gamania/logs
# 佐證 cron 建目錄是正確的:刪除日期目錄讓 cron 重建
kubectl -n ps exec <pod-name> -c func-web -- \
rm -rf /var/www/html/Gamania/logs/batch/$(date +%Y-%m-%d)
# 等待 cron 重建後再次 ls,應為 www-data
Recommendation¶
- Layer 1(必要):在
BuildPSWebImage-MakeWish.groovyinline Dockerfile 中,chown -R 1000:1000之前加入RUN mkdir -p /var/www/html/Gamania/logs/batch /var/www/html/Gamania/logs/syncdb - Layer 2(必要):在
docker-entrypoint.sh的 supervisord 啟動前加入chown -R 1000:1000 /var/www/html/Gamania/logs,並確保此 entrypoint 被 COPY 進 image - Layer 3A(必要,已驗證):在
BuildPSWebImage-MakeWish.groovy的Restart Web Serverstage 中,改用su -s /bin/bash www-data -c 'bash init-all.sh <site>'——Build #133 完整 log 確認此修正有效,4/4 腳本成功 - Layer 3B(必要,v4 確認):在
AutoBuildSiteDeploypipeline 中,同樣將 init-all.sh 改為以 www-data 執行——Build #622 log 確認這是問題在 BuildPSWebImage 修正後仍持續重現的根本原因 - 嚴禁:不要使用
chmod -R 777作為永久解,違反最小權限原則 - 注意版號:每次修改 Dockerfile 或 entrypoint.sh 後,
webImageVersion必須遞增;同版號 ECR image tag 不會被覆蓋 - 排查順序:懷疑重建沒用時,先確認 (a) 版號是否遞增、(b) volumeMount 是否有 EFS、© 是哪條 pipeline 觸發的 init-all.sh(BuildPSWebImage or AutoBuildSiteDeploy)