FR-063.3 落地版部署 — 環境變數與掛載完整清單

落地版(on-premise 容器版)部署設定清單

產出於 2026-08-15(CM-1192 / FR-063.3)。適用對象:把 guidant-ai-be:<version> image 裝到客戶主機的人。

前提:正式 image 不帶任何設定檔。編譯進 binary 的只有產品常數,所有部署差異 一律由外部注入(--env-file 或 compose environment:)。缺關鍵項不會拖到執行期 才出錯,啟動期就會一次列出全部缺項後中止。


0. 三分鐘上手(compose 定式,CM-1238 起)

正式部署一律走 repo 內的 docker/production/docker-compose.yml——所有掛載/ 環境/重啟策略都已釘成定式(含 read_only 唯讀化與 tamper 標記 volume), 照抄即可,不必再手打長指令。

# ① 準備設定檔(照 §2 必填項填)
cp env.sample /srv/guidant-ai/guidant.env && vi /srv/guidant-ai/guidant.env

# ② 準備掛載目錄(owner 不必先喬,容器啟動時會自動 chown,見 §4)
mkdir -p /srv/guidant-ai/{static,log,home,pki}

# ③ 起兩個服務(api + socketio,同一個 image)
GUIDANT_VERSION=<version> docker compose -f docker/production/docker-compose.yml up -d
# 只動單一服務時必指定 service 名:... up -d guidant-api

# ④ 驗
curl -s localhost:8000/api/1.0/version    # {"data":{"commit":"...","version":"..."}}
curl -s localhost:8002/healthz            # {"mode":"socketio","status":"ok"}

可調變數(都有預設,詳見 compose 檔頭註解):GUIDANT_VERSION(image tag)、 GUIDANT_ENV_FILE(預設 /srv/guidant-ai/guidant.env)、GUIDANT_DATA_DIR (預設 /srv/guidant-ai)、AGENT_CERT_DIR(agent 憑證主機端目錄,須與 env 內 AGENT_* 路徑一致,見 §4)。

換版:改 GUIDANT_VERSION(或 pull 新 image)後重跑同一句 up -d, compose 會自動 recreate,資料都在 volume 不受影響。

Fallback:無 compose 環境的 docker run 寫法(點開)
# ③a 起 API 服務
docker run -d --name guidant-api --restart unless-stopped \
  --read-only --tmpfs /tmp:rw,size=2g --tmpfs /run:rw \
  --env-file /srv/guidant-ai/guidant.env \
  -p 8000:8000 \
  -v /srv/guidant-ai/static:/app/static \
  -v /srv/guidant-ai/log:/app/log \
  -v /srv/guidant-ai/home:/home/guidant \
  -v /srv/guidant-ai/pki:/opt/guidant/pki \
  -v /etc/machine-id:/etc/machine-id:ro \
  -v <憑證目錄>:<憑證目錄>:ro \
  guidant-ai-be:<version>
# ⚠️ <憑證目錄> 必須與 .env 內 AGENT_* 指的路徑一致(原因與驗證方式見 §4)
# 🔴 `/opt/guidant/pki` 這行不可省:那是 FR-064 tamper 標記落點,漏掛會落成
#    匿名 volume,`docker rm` 容器就把鎖定標記洗掉、防竄改鎖定失效(見 §4)

# ③b 起通知服務(**同一個 image、同一個 command**,只多一個環境變數)
docker run -d --name guidant-socketio --restart unless-stopped \
  --read-only --tmpfs /tmp:rw,size=2g --tmpfs /run:rw \
  --env-file /srv/guidant-ai/guidant.env \
  -e RUN_MODE=socketio \
  -p 8002:8002 \
  -v /srv/guidant-ai/static:/app/static \
  -v /srv/guidant-ai/log:/app/log \
  -v /srv/guidant-ai/home:/home/guidant \
  -v /srv/guidant-ai/pki:/opt/guidant/pki \
  -v /etc/machine-id:/etc/machine-id:ro \
  -v <憑證目錄>:<憑證目錄>:ro \
  guidant-ai-be:<version>

1. 🔴 env-file 的兩個寫法陷阱(實際撞過,先看這段)

docker --env-file 的格式與 shell 的 .env 慣例不相容

寫法 結果
export FOO=bar ❌ docker 把 export FOO 整串當變數名,報 variable 'export FOO' contains whitespaces拒絕啟動整個容器
FOO="bar" ⚠️ docker 不剝引號,值變成 "bar"(含引號),密碼會因此對不上
FOO=bar ✅ 正確

export 前綴是給 source 用的寫法,人不會覺得有錯——188 上的 .env 就有 7 行是這樣 (2026-08-15 實測撞到,容器直接起不來)。交付前務必檢查

grep -nE '^[[:space:]]*export ' <你的 env 檔>   # 應無輸出
grep -nE '=".*"$'              <你的 env 檔>   # 應無輸出(或確認該值真的要含引號)

2. 核心必填(缺任一項啟動即中止)

啟動期由 config/config_loader.py:validate_required_env() 檢查,缺項會一次列全 (不是缺一個報一個),訊息形如 缺少:DB_HOST, JWT_SECRET_KEY(或 JWT_SECRET)

變數 啟動期檢查 說明
DB_HOST ✅ 強制 PostgreSQL 主機
DB_USER ✅ 強制 DB 帳號
DB_PASSWORD ✅ 強制 DB 密碼
JWT_SECRET_KEY ✅ 強制 JWT 簽章金鑰。每個客戶獨立生成,不可跨部署點共用
REDIS_HOST ✅ 強制 Redis 主機
REDIS_USER ⚠️ 未強制 Redis 帳號。啟動期不檢查,但 Redis 有啟用帳密時漏填會在連線當下才失敗(症狀比缺項難查,故仍視為必填)
REDIS_PASSWORD ⚠️ 未強制 Redis 密碼,同上

舊 JSON 包裝格式仍完全相容且優先(既有部署零改動):填了 DB_SECRET / REDIS_SECRET / JWT_SECRET 就以它們為準。新裝機建議用上表的平鋪格式。

舊格式 新平鋪格式
DB_SECRET={"rds_master_username":"u","rds_master_password":"p"} DB_USER=u + DB_PASSWORD=p
REDIS_SECRET={"redis_user_name":"u","redis_user_password":"p"} REDIS_USER=u + REDIS_PASSWORD=p
JWT_SECRET={"jwt_secret":"s"} JWT_SECRET_KEY=s

3. 一般設定(有預設值,依部署點調整)

變數 預設 落地版建議
RUN_MODE api api 容器不設;通知容器設 socketio
PORT / SOCKET_PORT 8000 / 8002 容器各有獨立網路命名空間,通常不用改,只改對外 map 的 port
DB_PORT 5432 依客戶 DB
DB_NAME compliance_manager 依客戶命名
ENV DEV 純顯示標籤(只在啟動 log 印一行,不驅動任何行為)。落地版填 PRD
DEBUG false 落地版必須 false。true 會開 SQL 全量 log(洩漏查詢內容且拖慢)
CORS_ALLOWED_ORIGINS * 落地版必填實際站台網址(逗號分隔)。留 * 等於任何站台都能打這組 API
LOG_DIR ./log 維持預設即可(容器內 cwd 固定 /app,故落在 /app/log 這個掛載點)
USE_GUNICORN 未設=not DEBUG 落地版走 gunicorn(不設即可)
GUNICORN_WORKERS 4 依客戶機器核心數調整
GUNICORN_TIMEOUT 120 匯出/大檔解析比一般請求久,機器慢時調大
SYSTEM_NAME / SYSTEM_URL / FRONTEND_BASE_URL 通知信內容與連結會用到
TZ image 內已設 Asia/Taipei 🔴 必須與該部署的主機/DB/其他服務一致。跨時區客戶用 -e TZ=<該地時區> 覆寫,但不可各自為政——程式內有 naive datetime 比較(如 agent 在線判定),時區不一致會讓「東西是活的、判定卻壞掉」(見 §4 末的時區說明)
ENABLE_MULTI_TENANT true 單一客戶落地版通常仍維持 true
MFA_REQUIRED false DB RUNTIME_CONFIG 有值時以 DB 為準,本項是預設來源
LICENSE_ENFORCEMENT_ENABLED true 顯式寫出(曾因靠隱式預設而誤判)
LICENSE_READONLY_GATE_ENABLED true 同上
DEPLOYMENT_MODE saas 落地版填 host(啟用機器指紋核對)
TURNSTILE_SECRET_KEY 登入 captcha。落地版(內網無外網)需評估是否停用,否則登入會因連不到 Cloudflare 而失敗
DB_READ_HOST 目前未生效(讀寫分離從未實際運作,無消費者),保留定義供未來使用

4. 掛載點(七個,缺一不可)

容器內路徑 用途 不掛的後果
/app/static 使用者上傳檔落地處 重啟即全丟。問卷 Excel 上傳實測落點:/app/static/file/answer/upload/<user>/
/app/log 應用 log 重啟即丟,出事無從查起
/home/guidant $HOME:內含 .cm-jobs(背景 job 狀態)與 .config/libreoffice(LibreOffice profile,首次轉檔自建) job 狀態遺失;LibreOffice 每次重建 profile
/tmp LibreOffice 轉檔中繼(TMPDIR 用容器層,大檔轉檔可能吃爆容器可寫層。建議掛實體 volume,容量數 GB 以上
/opt/guidant/pki 🔴 FR-064 tamper 標記落點common/integrity/tamper_marker.py 預設 /opt/guidant/pki/.integrity-tamper Dockerfile 有 VOLUME 宣告,不顯式掛載會落成匿名 volume——docker rm 容器時鎖定標記跟著消失,防竄改鎖定形同虛設(重建容器即解鎖)。2026-08-16 實際踩到,CM-1238 補洞
/etc/machine-id唯讀,掛「檔」不是目錄 🔴 機器指紋錨點common/license/machine_fingerprint.py 第一級來源)——unlock token 綁機(FR-064.5)與 license 綁機(FR-062 D6)都以它為準 image 內的 /etc/machine-id0 bytes 空檔(Debian base image 標準行為,容器不跑 systemd 永遠填不上)→ 指紋退回 uuid.getnode()(容器 MAC)→ 每次 docker restart 就換一個指紋:原廠簽的解鎖憑證重啟即失效(永遠解不了鎖)、license 綁機漂移。2026-08-16 實測撞到,CM-1242 補洞
憑證目錄(路徑見下方 🔴 檢測 agent 的 mTLS/JWT 憑證檔 不用檢測 agent 功能可不掛;要用時憑證只能外部掛入(不打進 image)

🔴 /etc/machine-id 掛載的前置條件:宿主機必須有這個檔

-v /etc/machine-id:/etc/machine-id:ro 掛的是檔案。宿主機若沒有這個檔, docker 會自動把它建成目錄——容器內讀檔失敗、指紋悄悄退回 MAC,症狀與沒掛 完全一樣(服務全綠、只有解鎖與綁機壞掉)。裝機前先驗:

test -s /etc/machine-id && echo OK || echo "缺 machine-id,見下方補法"
# 缺的話(極精簡系統才會缺,systemd 系開機安裝即有):
sudo systemd-machine-id-setup    # 或 sudo sh -c 'dbus-uuidgen > /etc/machine-id'

⚠️ machine-id 一旦被重新生成,該機所有既有綁機憑證(unlock token/host 版 license)都會失效,需向原廠重新申請。故只在「本來就沒有」時補,不要為了整齊 重新生成。

🔴 憑證掛載:路徑必須與 AGENT_* 的值逐字一致

這是實際踩過的坑(2026-08-15,188 上容器化後 agent 全數連不上)。

AGENT_CA_CERT 等變數存的是檔案的絕對路徑字串,程式直接拿去開檔 (common/util/agent_auth/settings.py)。它不會去別的地方找、也沒有預設位置—— 所以容器內必須在那個路徑上真的有檔案,掛在別處等於沒掛。

失敗症狀:BE 起得正常、healthcheck 綠、API 都通,只有 agent 連不上 (讀不到 CA 憑證就無法完成 mTLS 驗證)。又是一個「服務看起來活著」的靜默失效。

兩種做法,擇一:

做法 A(推薦,.env 零改動) —— 把主機憑證目錄按原路徑掛進容器:

# .env 內若是 AGENT_CA_CERT=/opt/guidant-ai-be/scripts/pki/agent_dev/ca.crt
# 就照這個路徑掛,容器內外路徑相同:
-v /opt/guidant-ai-be/scripts/pki:/opt/guidant-ai-be/scripts/pki:ro

:ro 唯讀 —— BE 只需要讀憑證,唯讀可防止容器意外寫壞 CA 私鑰。

做法 B(路徑收斂,需同步改 .env —— 統一掛到 /opt/guidant/pki, 並把 .env 內六個 AGENT_* 路徑一起改成該目錄下的位置。新客戶裝機建議走這條, 既有部署轉換時兩邊要一起改,改一半就是連不上

🔴 憑證檔的 owner 必須是容器內的 uid 1000

光是「掛對路徑」還不夠——私鑰通常是 0600 rootca.key / cloud_client.key / jwt_private.pem),而容器內業務進程跑的是 uid 1000,讀不到。原生部署時 BE 以 root 或檔案擁有者執行,從來不會遇到;容器化刻意降權後就撞上。

主機端修正(只改 owner、不放寬權限模式,私鑰維持 0600):

sudo chown 1000:1000 <憑證目錄>/*.key <憑證目錄>/*.pem

失敗症狀極具誤導性(2026-08-15 實際踩到,繞了很久):

  • agent 心跳正常last_seen_at 持續更新 ← 這條只驗 JWT 公鑰,而公鑰是 0644 讀得到
  • 只有健康檢查失敗{"reachable": false, "detail": "[Errno 13] Permission denied"}
  • 而 FE 的「在線狀態」欄會以健康檢查結果覆蓋心跳判定(CM-928 設計), 於是畫面上兩欄一起顯示離線 → 看起來像「agent 根本沒連上」, 實際上是它連得好好的、只有一支私鑰讀不到

裝機驗證(別等 agent 心跳才發現):

# ⚠️ 必須帶 -u 1000:docker exec 預設以 root 進入,root 讀得到 0600 私鑰,
#    用 root 驗會全部 ✅ 卻漏掉這個坑(本案初次驗證即因此漏掉)。
#    要驗的是「業務進程的身分讀不讀得到」,不是「有沒有這個檔」。
docker exec -u 1000 <容器> sh -c 'for p in $AGENT_CA_CERT $AGENT_CA_KEY \
    $AGENT_CLOUD_CLIENT_CERT $AGENT_CLOUD_CLIENT_KEY \
    $AGENT_JWT_PRIVATE_KEY $AGENT_JWT_PUBLIC_KEY; do
  [ -r "$p" ] && echo "✅ $p" || echo "❌ $p"; done'

六個全 ✅ 才算好。有 ❌ 的兩種可能:掛載路徑與 AGENT_* 對不上(檔案根本不在), 或 owner 不是 1000(檔案在但讀不到)——ls -ln 一看便知。

端到端確認(最終判準,光看檔案可讀還不夠):

curl -sk -X POST -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
  -d '{"base_url":"<agent 位址>"}' https://<主機>/api/1.0/remote-agents/health-check
# 期望 {"reachable": true, "status_code": 200}

確認 agent 真的通了(心跳間隔預設 AGENT_HEARTBEAT_INTERVAL_SEC,等一輪):

docker logs --since 5m <容器> | grep 'agents/heartbeat'   # 應見 HTTP 200

AGENT_AUTH_MODE 不設=預設 none=agent 認證整條關閉,此時以上憑證不會被讀。 設為 full 才會走 mTLS + JWT。

🔴 時區:容器必須與主機/DB 一致

image 內已釘死 TZ=Asia/Taipei不要把它改成與該部署其他元件不同的值。

原因:程式內有 naive datetime 比較的既有寫法,最典型的是 agent 在線判定 (app/remote_agent/service/agent_enrollment_service.py:is_online()):

return (datetime.now() - last_seen_at).total_seconds() < offline_threshold_sec

兩邊都是不帶時區的時間值,一旦容器時區與寫入端(DB/其他服務)不同,差幾小時就 永遠算成「很久沒心跳」。

實際症狀(2026-08-15 於 188 踩到,容器初版未設 TZ 落到 UTC、主機是 CST):

  • agent 心跳持續 HTTP 200
  • DB last_seen_at 持續更新
  • 但畫面顯示離線 ← 唯一的異常

也就是說東西全都是活的,只有那個判定壞掉——查的時候很容易往「網路不通/憑證錯」 的方向鑽。先比對時間再說

date                        # 主機
docker exec <容器> date     # 容器,兩者應完全一致(含時區縮寫)

跨時區客戶部署時以 -e TZ=<該地時區> 覆寫,但主機、容器、DB 三者要一致。

owner 不必事先喬:容器 entrypoint 會在啟動時把上述掛載點(且僅限這幾個) chown1000:1000(已實測:root 擁有的 bind mount 掛進去後自動修正,上傳正常)。 若以 docker run --user 自行指定非 root 身分啟動,則 chown 不會執行,owner 需自備。

產物本身(/app 下除 staticlog 外的內容)是 root:root,業務進程(uid 1000) 只讀不寫——這是刻意的防竄改設計,見下一段。

STORAGE_CONFIG 是 DB 設定不是環境變數:儲存後端(local / minio / remote_agent) 存在 public.system_configs(group=STORAGE_CONFIG)。⚠️ 選 localbase_dir 必須明確設定——未設會 fallback 到 /tmp/upload/,重啟即遺失。

🔒 建議加上 --read-only(防竄改,FR-064 D4)

image 內產物已歸 root:root、業務進程跑 uid 1000,檔案系統層就寫不進產物。 再加 --read-only 把整個 rootfs 掛成唯讀,連 root 身分(例如有人 docker exec -u 0 進來)也改不動——這是成本近乎為零的一道防線,正式交付建議都加。

加了之後必須補兩個 tmpfs,否則啟動失敗:

docker run -d --name guidant-api --restart unless-stopped \
  --read-only \
  --tmpfs /tmp:rw,size=2g \
  --tmpfs /run:rw \
  --env-file /srv/guidant-ai/guidant.env \
  -p 8000:8000 \
  -v /srv/guidant-ai/static:/app/static \
  -v /srv/guidant-ai/log:/app/log \
  -v /srv/guidant-ai/home:/home/guidant \
  -v /srv/guidant-ai/pki:/opt/guidant/pki \
  -v /etc/machine-id:/etc/machine-id:ro \
  -v <憑證目錄>:<憑證目錄>:ro \
  guidant-ai-be:<version>
項目 為什麼
--tmpfs /tmp:rw,size=2g TMPDIR=/tmp,LibreOffice 轉檔中繼要寫。容量要給足——大檔轉檔吃掉 tmpfs 會轉檔失敗。若轉檔量大,改掛實體 volume(-v /srv/guidant-ai/tmp:/tmp)而非 tmpfs
--tmpfs /run:rw gunicorn 等常見的 pid/socket 落點
既有 volume(含 /opt/guidant/pki 照舊掛,唯讀化不影響它們(bind mount / volume 不受 --read-only 管)
/home/guidant 一定要掛 LibreOffice 首次轉檔會寫 $HOME/.config/libreoffice。沒掛時該路徑落在唯讀 rootfs 上 → 轉檔失敗

compose 寫法:直接用 repo 內 docker/production/docker-compose.yml(§0), read_only + tmpfs + 全部掛載(含 tamper 標記的 /opt/guidant/pki)都已內建。

socketio 容器同樣適用(RUN_MODE=socketio,實測兩模式皆 healthy)。

不加 --read-only 也能跑,不會有任何錯誤訊息——這正是要在文件寫明的原因: 它是「加了才有、不加也不吭聲」的防護,不會有人在排錯時發現自己漏了。


5. 功能模組(用到才填,整段可留白)

功能 變數
AI(Dashboard/翻譯/bot) OPENAI_API_KEYANTHROPIC_API_KEYGOOGLE_API_KEY
Google Drive 整合 GOOGLE_DRIVE_OAUTH_CLIENT_ID..._SECRET..._REDIRECT_URIDRIVE_TOKEN_ENCRYPTION_KEY(⚠️ 每環境獨立生成,換值會導致既有密文解不開需重新授權)、DRIVE_FILE_SIZE_LIMIT_MBDRIVE_SYNC_WORKER_POOL_SIZE
檢測 Agent AGENT_AUTH_MODEDETECTION_TOOL_ENCRYPTION_KEYAGENT_CA_CERT_KEYAGENT_CLOUD_CLIENT_CERT_KEYAGENT_JWT_PRIVATE_KEY_PUBLIC_KEY(值是憑證檔的絕對路徑,掛載點必須與這些值逐字一致,見 §4)、AGENT_CLOUD_ENDPOINT(agent 回連 BE 的位址,每部署點必改)、AGENT_HEARTBEAT_INTERVAL_SECAGENT_CERT_VALID_DAYS(預設 365)、AGENT_JWT_TTL_SEC(預設 60)、DETECTION_UPLOAD_* 五項與 DETECTION_PROFILE_* 四項上傳限制(都有預設,見 .env.sample
License Center 對接 LICENSE_ACTIVATION_SERVER_URL(非本機部署必填,預設 http://127.0.0.1:5062 僅 DEV 可用)、LICENSE_CENTER_API_TOKEN
排程報表 SCHEDULE_REPORT_DIR(若使用,該路徑需可寫且應在掛載點內)
SSP 匯出轉檔 LIBREOFFICE_CMD(LibreOffice 裝在非標準路徑才需填;容器版 image 內已裝於標準路徑)

6. jedi-* 套件內的環境變數(主專案 grep 不到,最容易漏)

這 16 個由內部共用套件直接讀取,不經主專案 config,因此在主專案原始碼裡搜不到—— 歷來漏設的常客(design 盤點#14):

類別 變數
執行環境 ENABLE_MULTI_TENANTRUN_ENVTZ
DB DB_USERNAMEDB_PASSWORDDB_SCHEMADEFAULT_SCHEMA
Redis REDIS_DBREDIS_SSLREDIS_SECRET(見下)
i18n DEFAULT_LOCALE
外部整合 GITLAB_*GITHUB_PRIVATE_TOKEN
Auth OTP_COOLDOWN_SECONDSAUTH_CHANGE_SECRET_COOLING_HOURS

⚠️ DB_USERNAME(jedi 讀)與 DB_USER(主專案讀)是兩個不同的變數名。 兩邊都用到 DB 帳號時,兩個都要填成同一個值。

⚠️ jedi-mfa(Email OTP)只認舊 JSON 格式 REDIS_SECRETjedi_mfa/common/util/redis_client_util.py:18 直接 json.loads(os.getenv("REDIS_SECRET"))), 不吃主專案的平鋪 REDIS_USER / REDIS_PASSWORD。有啟用 Email OTP 的部署, REDIS_SECRET 舊格式必須保留;且主專案「兩種都填時以舊 JSON 為準」, 所以填了就要填真值。


6.5 build 期簽章(產 release image 時才需要,跑服務不用)

scripts/build/build_release.sh 產完 dist 會產 integrity manifest 並送 License Center 簽章。用的環境變數與產品 BE 既有參數同名config/config.py,FR-062),不另創新名:

變數 用途 預設
LICENSE_ACTIVATION_SERVER_URL LC 簽發站 base URL(送簽打 /api/internal/sign-manifest http://127.0.0.1:5062(build 機 188 上 LC 就在本機)
LICENSE_CENTER_API_TOKEN LC api_tokens 白名單內的明文 token,至 LC 後台「API Token 管理」頁建立;不入版控、不進 build log(腳本只印前 4 碼) —(必填才會送簽)
  • 無 token 時只產不簽build_release.sh 印明確警告後繼續,但沒簽的 dist 打不出 imagebuild_image.sh 會擋),所以「忘了簽」不會靜靜流到出貨。
  • 手動補簽:
    LICENSE_CENTER_API_TOKEN=scripts/build/sign_manifest.sh \
        --manifest <dist>/integrity-manifest.json \
        --output   <dist>/integrity-manifest.sig
  • air-gapped/簽發站不可達時的 CLI 保底(在 LC 主機上跑,私鑰不離開該機): license-center sign-manifest <manifest 檔> --output <sig 檔>。 兩條路徑走同一個 sign_payload 與同一道結構守門,產物等價。

7. 落地版停用的功能

功能 原因 影響面
證據自動分類(AI 分類器) 需以 docker run 起分類器容器;BE 自己容器化後變成 DinD 問題(要掛 docker.sock,等同給容器宿主機 root 權限)。D4 定案降級停用 該模組端點仍在,觸發時止於 EC_500001「分類容器執行失敗」。其他模組不受影響
Swagger UI 從未啟用(docs.init_app 一直被註解),且落地版不應對客戶暴露 API 文件

9. 客戶端主機層留證建議(FR-064 D11,2026-08-16)

產品的防竄改機制(FR-064)能偵測到「檔案 hash 對不上」並留下 machine_fingerprinttamper_event_id/不符檔案的 mtime/ctime/owner/size 等檔案證據,但**「是誰在主機上 動的手」不在產品應用層能力範圍內**——竄改動作(SSH 登入改檔、docker exec 進容器、 直接改 volume 掛載來源)完全不經過產品的業務邏輯,產品拿不到操作者身分。

要溯源到「哪個帳號、哪個 session 做的」,需要客戶主機層自行建立審計機制, 這是客戶責任範疇,不是本產品的責任範疇。以下是建議客戶部署時一併規劃的項目:

9.1 auditd 監控產物目錄

Linux 主機上用 auditd 對容器產物與掛載路徑加寫入監控規則,範例:

# 監控 /app 底下(產物目錄)的寫入/屬性變更(含 chmod/chown)
auditctl -w /app -p wa -k guidant_integrity

# 監控憑證與 tamper 落點目錄(見 §4)
auditctl -w /opt/guidant/pki -p wa -k guidant_pki_write

# 若採 bind mount,監控主機端實際來源路徑(容器內 /app 對應到的主機路徑)
auditctl -w /srv/guidant-ai/app-image-root -p wa -k guidant_host_write

-p wa 表示監控 write/attribute-change 事件;-k 是自訂 tag,方便後續用 ausearch -k guidant_integrity 篩選。規則要設為開機常駐(寫入 /etc/audit/rules.d/),否則重開機即失效。

查詢範例(配合 tamper 事件的 detected_at 時間窗,往回抓對應的稽核紀錄):

ausearch -k guidant_integrity -ts <detected_at 前後時間窗>

9.2 log 外送建議

主機層審計紀錄(auditd log、docker exec 使用紀錄、SSH 登入 log)建議外送到 客戶自有的集中式 log 平台(SIEM/syslog server),而非只留在被懷疑遭竄改的 同一台主機上——本地留存的稽核紀錄與本地 tamper 標記檔面臨同一個弱點: 主機若被完全控制,本地留存的一切都可能被清除或竄改,外送才能保留不受單機 狀態影響的獨立證據鏈。

9.3 明文邊界

產品應用層無法識別檔案竄改行為人,主機層審計是客戶責任範疇。 產品偵測與存證止於「檔案證據+(若有業務 session)旁證」,不含操作者身分溯源; 若客戶場景需要「查明是誰做的」這一層,須自行部署並維運上述主機層審計機制。


10. 交付前檢查清單