FR-066 · Agent Native Installer — 需求討論稿 · 2026-08-18(v1 定案版)
檢測 Agent(evidence-agent)目前唯一交付形式是 docker image+三容器 compose。但 agent 的本職是量測宿主機——機器指紋要讀 /sys/class/dmi/id/product_uuid、/etc/machine-id、主網卡 MAC,這些在容器裡讀不到或會漂移,已經靠 collect-host-id.sh workaround 硬撐。FR-065 期間決策者裁定:agent 安裝包另起新 FR、非 docker。本案把 agent 做成 Nuitka 編譯的 tarball 安裝包,install.sh 一鍵裝成 systemd 服務,先做 Linux;Windows 只出評估文件。九項關鍵決策(D1–D9)已全數拍板,本稿記錄定案理由與被排除方案,供 design.md 深化。
整案一句話:把檢測 Agent 從「docker 三容器出貨」改成「Nuitka 編譯+外部工具全包的一顆 tarball,install.sh 一鍵裝成 Linux systemd 服務」,宿主指紋直讀、SQLite 取代 PG、agent 自起 TLS 取代 nginx sidecar、build 期 manifest 簽章+啟動驗章。
| 階段 | 一行說明 |
|---|---|
| FR-066.1 Nuitka build 管線 | agent 版 build_release.sh、dependency-injector 私房 wheel、version bake、resource_path() 改造、SQLite 化 |
| FR-066.2 tarball 打包+工具 bundle | 全包一顆(CINC / sonar-scanner / LibreOffice / xsltproc / git)+ manifest 簽章 |
| FR-066.3 install.sh+systemd | 七步安裝、互動問答、upgrade/rollback(symlink 切換)、維運子命令、自起 TLS |
| FR-066.4 驗收與文件 | 乾淨機真機驗收、封閉網路驗證、客戶手冊、Windows 評估文件 |
依賴一句:.1 → .2 → .3 串行(build 產物是打包的輸入、打包產物是安裝的輸入),.4 收口。
檢測 Agent(evidence-agent,獨立 repo ~/Projects/Billows/Audit-Manager/evidence-agent,v0.2.27,Flask + dependency-injector)目前唯一的交付形式是:
python:3.11-slim base)agent 本體 + agent-db(postgres:16)+ nginx mTLS sidecardocker save / docker loadFR-065(產品落地 installer)期間,決策者裁定:agent 安裝包另起新 FR、非 docker。核心理由是 agent 的本職與容器化根本衝突:
指紋來源是 /sys/class/dmi/id/product_uuid、/etc/machine-id、主網卡 MAC——容器內讀不到或會漂移。已經發生過 machine-id 空檔導致指紋漂移的事故(Debian base image 的 /etc/machine-id 是空檔,退回隨機 MAC),現行靠 deploy/collect-host-id.sh workaround 從宿主抽值塞進容器硬撐。
Nessus Agent、Qualys Cloud Agent、Wazuh Agent、Elastic Agent、Datadog Agent——全部以原生套件(deb/rpm/tarball)+ systemd 服務交付,沒有一家把主機檢測 agent 包成 docker 出貨。貼宿主量測的產品,原生安裝就是行規。
install.sh 一鍵安裝為 Linux systemd 服務本案不是從零開始——三個前作已把大部分地基打好:
| 前作 | 可直接複用 |
|---|---|
| FR-063 Nuitka 落地版打包 | scripts/build/build_release.sh build 管線、dependency-injector 私房 wheel rebuild_wheels.sh、resource_path() 慣例、DI 靜態清單處理手法 |
| FR-064 防竄改偵測 | 簽章泛用層 sign_payload / verify_payload(帶 type 欄)、gen_integrity_manifest.py、sign_manifest.sh、License Center 簽發 |
| FR-065 產品落地 installer | install.sh 七步結構、--check-only、安裝落檔慣例、digest 驗證、維運子命令(status / logs / uninstall / fingerprint) |
以下九項全部已由決策者拍板,每項記定案內容、理由與被排除方案。
✅ 定案:Nuitka standalone 編譯,打成 tarball,裝成 systemd 服務
standalone 模式同樣是機器碼編譯——自家 code 不可見,保護強度與 FR-063 的 BE 打包相同。第三方套件以原始 .py 附帶(standalone 的 EXCLUDE 名單機制),但那些是公開開源套件、非自家 code,不構成洩漏。
排除:onefile 模式——①啟動要先解壓到暫存目錄,有啟動延遲;②FR-064 逐檔 manifest 驗章在單一自解壓檔上難做;③外部工具(CINC / LibreOffice 等)反正要目錄形式存在,onefile 省不了目錄。
✅ 定案:agent-db(postgres:16)退役,換 SQLite
現況的 agent-db 只服務 jedi-file-upload 的 upload_files 一張表(core/app_factory.py:37 註解明寫;create(checkfirst=True) 自建表、無 migration 體系)。單表、單進程寫入、無並發、無 RLS——正是 SQLite 的甜蜜點。init_db() 吃 SQLAlchemy URL,換成 sqlite:/// 即可。
實作前置查證項(Q2):掃 jedi-file-upload 的 model 與查詢確認無 PG 方言依賴(jsonb / ON CONFLICT 等)。
排除:①要求宿主裝 PG——加重客戶前置需求,違背一鍵安裝目標;②bundle PG 進 tarball——包更肥、多一個服務要維運(起停/備份/密碼),為一張表不值得。
✅ 定案:所有外部檢測工具 bundle 進同一顆 tarball,install.sh 自動安裝,離線完全自足
決策者裁定理由:封閉網路情境(FR-059.3 已驗證為既定支援情境)下,全包才不存在漏帶/版本錯配的現場事故。體積 ~1GB+ 已明示接受。
bundle 清單:
| 工具 | 體積 | 備註 |
|---|---|---|
| CINC Auditor(Ruby omnibus) | ~275MB | ⚖️ 授權紅線:絕不可換成官方 InSpec 商業 binary |
| sonar-scanner(含 JRE) | ~147MB | amd64 專屬 |
| LibreOffice + fonts-noto-cjk | ~400MB+ | 檔案預覽轉 PDF;⚠️ 字型家族名是 Noto Sans CJK TC 不是 Noto Sans TC |
xsltproc + assets/nmap.xsl |
小 | nmap 報告轉換 |
| git | 小 |
不 bundle:ZAP / OpenVAS(客戶自備 daemon)、OpenSCAP / Nmap 引擎(裝在目標主機、agent SSH 過去跑,不在 agent 機上)。
排除:方案 B「核心包+工具附加包分層」——升級只傳 core 較省流量,但多檔交付有漏帶風險,封閉網路現場出事沒有網路可補救;方案 C「宿主前置需求清單」——把安裝品質丟給客戶,且客戶自裝 InSpec 會踩 CINC 授權紅線。
✅ 定案:agent 以 gunicorn/ssl 自行承接 8443 資料面 TLS,nginx sidecar 退役
mTLS 的驗證邏輯本來就在 agent code 內(core/data_plane_auth.py),nginx 只是轉發層——砍掉它不損失任何安全能力。8443 的 server 憑證由 enrollment 時 CA 簽出(CSR SAN=base_url host),客戶不需自備憑證。
✅ 定案:/etc/guidant-agent/agent.env 作為 systemd EnvironmentFile
沿用現有全環境變數設計(config/config.py,agent 沒有 agent.json 這種設定檔)——零 code 改動。install.sh 互動問答三題(雲端位址/註冊 token/本機對外位址)生成該檔。
客戶側 mTLS 全自動:agent 自我註冊流程已存在(core/enroll.py:產 keypair+CSR → 拿 token 註冊 → 憑證簽回存 cert_dir → 心跳走 mTLS → 404 自動重註冊),客戶零 openssl 操作。雲端側 PKI 由 FR-065 產品 install.sh 已涵蓋(install.sh:1936 AGENT_CERT_DIR)。
✅ 定案:build 期 manifest 簽章+啟動時驗章;不做 FR-064 runtime 全家桶
複用 FR-064 泛用層:簽章加一個 agent_manifest type、gen_integrity_manifest.py 改常數、sign_manifest.sh 近零改——複用成本僅 1–2 張小卡。
為什麼值得做:agent 以 root 服務跑在客戶機、握 mTLS 憑證,是整條信任鏈最容易被摸的一端。
不做:FR-064 的 runtime 抽查/lockdown/unlock token 全家桶——對 agent 過重。
✅ 定案:/opt/guidant-agent/versions/<ver>/ + current symlink
升級=解新版本目錄、切 symlink、重啟服務;回滾=切回舊 symlink。SQLite 檔與 mTLS 憑證放資料目錄、不隨版本目錄走。agent 無 DB migration 負擔(D2 單表自建),所以不需要 FR-065 那套 pg_dump 備份模型——這條升級模型比產品端輕得多。
✅ 定案:本 FR 對 Windows 只產出評估文件,比照 FR-065 D9
(FR-065 的對應工作 T-4.3 於 2026-08-18 裁定跳過、評估文件未產出——agent 版評估文件從零寫,不假設有前稿可抄。)
評估三題:①指紋來源(WMI / registry);②服務形式(Windows Service / NSSM);③工具鏈可用性(CINC / sonar-scanner 等在 Windows 的形態)。
註:脫離 docker 後,Docker Desktop 商業授權疑慮消失;但 machine-id 等指紋問題在 Windows 依然存在,是評估文件的重點。
✅ 定案:原生化後直讀系統路徑,廢除 collect-host-id.sh;不為 Windows 預留抽象層
直讀 /sys/class/dmi/id/product_uuid、/etc/machine-id、主網卡 MAC——core/fingerprint.py 已有伏筆註記(「原生封裝後直接讀系統路徑,模型不變」)。deploy/collect-host-id.sh workaround 隨之廢除。
排除:先抽 OS 抽象層——Windows 需求未定案(D8 只出評估文件),為未定需求預先抽象是過度設計;等 Windows 真要做再抽,重構成本可控。
evidence-agent repo 內每個會被本案觸動的元件,逐項列現況與本案動作:
| 元件 | 現況 | 本案動作 |
|---|---|---|
Dockerfile / rebuild.sh |
docker image build 入口 | 退役,換成 agent 版 Nuitka build 腳本(.1) |
deploy/docker-compose.yml |
agent + agent-db + nginx 三服務編排 | 退役,換 systemd unit(.3) |
| agent-db(postgres:16) | 只服務 jedi-file-upload upload_files 一張表 |
→ SQLite(D2,.1) |
| nginx mTLS sidecar | 8443 TLS 終結+轉發 | 退役,agent 自起 TLS(D4,.3) |
deploy/collect-host-id.sh |
容器讀不到宿主指紋的 workaround | 廢除(D9) |
config/config.py 版本上報 |
讀 pyproject.toml 取版號 |
Nuitka 後檔案不存在 → 改 version bake(同 FR-063 D6 手法,.1) |
| connector 硬編工具路徑 | /usr/bin/* 寫死(image 內裝好) |
配置化或 install.sh 建 symlink 指向 bundle 工具(.2/.3) |
__file__ 定位三處 |
inspec.py:145 / nmap.py:101 / config.py:85 |
改 resource_path() 慣例(FR-063 同款,.1) |
assets/ 資料檔 |
nmap.xsl 等隨 repo | Nuitka --include-data-dir 帶入(.1) |
| dependency-injector | C extension,Nuitka 需特製 wheel | 複用 FR-063 私房 wheel(rebuild_wheels.sh,.1) |
| jedi-common / jedi-file-upload | 私服(Nexus)套件 | 編進 binary;build 機需可達 Nexus(.1) |
| DI wiring | 本來就是硬編靜態清單(無動態掃描) | 免改——FR-063 的 DI 掃描雷不會踩 |
| eventlet / WeasyPrint | agent 沒有這兩個依賴 | 免處理——FR-063 另兩大雷也不會踩 |
| 心跳排程 | threading.Thread(非 APScheduler) |
免改,Nuitka 相容 |
好消息:FR-063 的三大 Nuitka 雷(eventlet monkey patch / WeasyPrint 動態載入 / DI 動態掃描)agent 一個都不會踩
agent 沒有 eventlet、沒有 WeasyPrint,DI wiring 本來就是硬編靜態清單。.1 的風險面遠小於當初 BE 打包。
Q1 LibreOffice 離線化形式
自帶 deb 組 dpkg -i vs portable / AppImage——兩條路都存在,實作期(.2)實測選定。判準:離線可裝、字型(fonts-noto-cjk)能一起落、體積可接受。
Q2 jedi-file-upload PG 方言依賴掃描(D2 前置)
掃 model 定義與所有查詢,確認無 jsonb / ON CONFLICT 等 PG 方言。有的話評估改寫成本再定 SQLite 落地細節。.1 開工先做。
Q3 Nuitka 編 agent 的 EXCLUDE_COMPILE_PKGS 名單重評
agent 依賴集合與 BE 不同(paramiko / python-gvm / zaproxy / cryptography 等),FR-063 的名單不能照抄。agent 沒有 pandas 級的編譯大戶,冷 build 預期遠低於 BE 的 56 分鐘。
Q4 image 內 openscap-utils / sshpass 是否實際已無用
OpenSCAP connector 已改 paramiko 直連、不用官方 oscap-ssh 腳本——這兩個套件可能是殘留。查證結果決定要不要跟著搬進 tarball(不搬=包更瘦)。
供後續 design.md 深化。建議拆 4 個子需求:
| 子需求 | 內容 | 依賴 |
|---|---|---|
| FR-066.1 Nuitka build 管線 | agent 版 build_release.sh、dependency-injector 私房 wheel 複用、version bake、resource_path() 改造三處 __file__、assets/ include、SQLite 化(含 Q2 查證) |
起點 |
| FR-066.2 tarball 打包+工具 bundle | 全包一顆(D3 清單)、Q1/Q4 查證落地、manifest 簽章(D6:agent_manifest type、gen/sign 腳本改造) |
依賴 .1(build 產物是輸入) |
| FR-066.3 install.sh+systemd | 七步安裝結構、互動問答三題生成 agent.env(D5)、自起 TLS(D4)、upgrade/rollback symlink 模型(D7)、維運子命令(status / logs / uninstall / fingerprint) | 依賴 .2(tarball 是輸入) |
| FR-066.4 驗收與文件 | 乾淨機真機驗收、封閉網路(離線)驗證、客戶安裝手冊、Windows 評估文件(D8) | 收口,依賴 .1–.3 |
依賴鏈:.1 → .2 → .3 串行,.4 收口。與 FR-063/064/065 不同,本案各階段產物互為輸入,無平行空間。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
T["guidant-agent-<ver>.tar.gz<br/>~1GB+(D3 已接受)"]
T --> A["agent/<br/>Nuitka standalone 產物<br/>自家 code 已編機器碼"]
T --> B["tools/<br/>外部工具 bundle"]
T --> C["install.sh<br/>七步安裝+互動問答<br/>upgrade / rollback / 維運子命令"]
T --> D["manifest.json + manifest.sig<br/>FR-064 泛用層簽章<br/>type=agent_manifest"]
T --> E["systemd/<br/>guidant-agent.service<br/>EnvironmentFile=/etc/guidant-agent/agent.env"]
B --> B1["cinc-auditor(~275MB)<br/>⚖️ 不可換官方 InSpec"]
B --> B2["sonar-scanner+JRE(~147MB)"]
B --> B3["LibreOffice+fonts-noto-cjk(~400MB+)<br/>形式待 Q1 實測"]
B --> B4["xsltproc+nmap.xsl / git"]
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
participant OP as 客戶操作者
participant IS as install.sh
participant SD as systemd
participant AG as guidant-agent 服務
participant CL as 雲端產品(FR-065 已含 PKI)
OP->>IS: 解 tarball、執行 install.sh
IS->>IS: 七步:前置檢查→驗 manifest 簽章→落檔 versions/<ver>→裝 bundle 工具→建 symlink→裝 systemd unit→啟動
IS->>OP: 互動問答三題(雲端位址/註冊 token/本機對外位址)
IS->>IS: 生成 /etc/guidant-agent/agent.env
IS->>SD: systemctl enable --now guidant-agent
SD->>AG: 啟動(啟動時驗 manifest 簽章)
AG->>AG: 直讀宿主指紋(product_uuid / machine-id / MAC)
AG->>CL: enroll:產 keypair+CSR(SAN=本機位址)+token 註冊
CL-->>AG: CA 簽回 client 憑證+8443 server 憑證
AG->>AG: 憑證存 cert_dir,gunicorn/ssl 自起 8443 資料面
loop 心跳
AG->>CL: mTLS 心跳(404 → 自動重註冊)
end
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
S["evidence-agent 源碼<br/>+ jedi-common / jedi-file-upload<br/>(build 機需可達 Nexus)"] --> W["dependency-injector<br/>私房 wheel<br/>(複用 FR-063 rebuild_wheels.sh)"]
W --> N["Nuitka standalone 編譯<br/>version bake + resource_path()<br/>--include-data-dir assets/<br/>EXCLUDE 名單重評(Q3)"]
N --> P["組 tarball 骨架<br/>agent/ + install.sh + systemd/"]
TB["工具 bundle 下載/整備<br/>CINC / sonar-scanner /<br/>LibreOffice(Q1)/ xsltproc / git"] --> P
P --> M["gen_integrity_manifest.py<br/>(FR-064 改常數)逐檔 hash"]
M --> SG["sign_manifest.sh → License Center 簽章<br/>type=agent_manifest(D6)"]
SG --> OUT["guidant-agent-<ver>.tar.gz<br/>出貨"]