跳轉到

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 重建:

# cron 重建(以 www-data 執行)
drwxr-xr-x. 2 www-data www-data   104 Apr 10 17:40 2026-04-10  ← 正確!

EFS 持久化假說排除

kubectl volumeMount 確認:

[
  {"mountPath": "/config/GlobalSettings.json", "name": "global-settings", "readOnly": true},
  {"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", ...}
]
func-web container 完全沒有 EFS/PVC 掛載,root:root 日期子目錄是每次 init-all.sh 執行後當場建出來的。

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

  1. Layer 1(必要):在 BuildPSWebImage-MakeWish.groovy inline Dockerfile 中,chown -R 1000:1000 之前加入 RUN mkdir -p /var/www/html/Gamania/logs/batch /var/www/html/Gamania/logs/syncdb
  2. Layer 2(必要):在 docker-entrypoint.sh 的 supervisord 啟動前加入 chown -R 1000:1000 /var/www/html/Gamania/logs,並確保此 entrypoint 被 COPY 進 image
  3. Layer 3A(必要,已驗證):在 BuildPSWebImage-MakeWish.groovyRestart Web Server stage 中,改用 su -s /bin/bash www-data -c 'bash init-all.sh <site>'——Build #133 完整 log 確認此修正有效,4/4 腳本成功
  4. Layer 3B(必要,v4 確認):在 AutoBuildSiteDeploy pipeline 中,同樣將 init-all.sh 改為以 www-data 執行——Build #622 log 確認這是問題在 BuildPSWebImage 修正後仍持續重現的根本原因
  5. 嚴禁:不要使用 chmod -R 777 作為永久解,違反最小權限原則
  6. 注意版號:每次修改 Dockerfile 或 entrypoint.sh 後,webImageVersion 必須遞增;同版號 ECR image tag 不會被覆蓋
  7. 排查順序:懷疑重建沒用時,先確認 (a) 版號是否遞增、(b) volumeMount 是否有 EFS、© 是哪條 pipeline 觸發的 init-all.sh(BuildPSWebImage or AutoBuildSiteDeploy)

本文檔由 Semi-Brain 自動生成

Session ID: 2915f12c-2e40-49a6-9734-a50edf42bc3a

分析信心度: 97%