FR-193 · design v1.3 · 2026-11-11
主機準備層、docker 呼叫切身分、uid 換算、升級重驗;主系統後續另拆
Agent 安裝包改成以專用帳號 guidant 跑 rootless Docker。程式碼與 image 不動,改動全在 deploy/install.sh 與 scripts/install/lib/。十張卡七批,第一棒乾淨機手動踩坑,第二批起改安裝程式。
本版已被 design-v1.4.md 取代(A4/A5/A6 合併成一張卡,設計內容不變)。此檔凍結不改。
整案一句話:Agent 被掃主機上的 Docker 引擎改成普通帳號 guidant 執行,三顆容器全由它開;客戶照常 sudo ./install.sh,多出來的主機設定全由安裝程式做。
| 階段 | 做什麼 | 產出 | 完成怎麼判定(決策者檢查法) |
|---|---|---|---|
| P0 探路 | 乾淨機手動裝 rootless,列壞掉清單、量吞吐 | 證據頁 evidence/agent-rootless-probe.md |
打開證據頁,看得到十二項驗證清單每項的實際輸出、上傳 100 MB 的秒數、traceroute 兩種模式的結果 |
| P1 改安裝程式 | 主機準備層、Docker 三段式安裝、docker 呼叫切身分、uid 換算、升級回滾重驗、收尾小項 | install.sh 與 lib/ 四支新檔 |
乾淨機 sudo ./install.sh 一路到底不手動介入;裝完 ps -eo user,comm | grep dockerd 沒有 root;重開機 Agent 自起;主系統發更新後 Agent 換版成功 |
| P2 收口 | 手冊、資安報告、出貨包總驗 | README 兩節、SR-09.5 改寫、兩個 OS 的驗收證據頁 | 拿出貨包在 Ubuntu 24.04 與 Debian 12 各裝一次,audit_nonroot_stack.sh --agent 全 ✔,主系統派掃描收結果正常 |
依賴:決策者功課只有一件——A0 需要一台乾淨 Ubuntu 24.04 VM(有 TPM 2.0、能連 DEV 主系統)。主系統那套之後開 FR-193.1,照這份的做法抄。
| 編號 | 定案 | 理由 | 被排除 |
|---|---|---|---|
| D1 | 對外一律 8443,不貼能力、不改 sysctl;80 的 http 轉址拿掉 | PM 2026-11-10 裁:還沒出給客戶過,改預設沒有相容成本;省掉 capability 維護與主機掃描解釋 | 預設 443 貼 rootlesskit 能力(v1.0 路三);全機 sysctl。只影響主系統,本份不做,列入 FR-193.1 |
| D2 | ping sysctl 全開 0 2147483647 |
與 Ubuntu/RHEL 預設相同;放寬的只有核心代填的 ICMP echo | 只開 guidant 子 gid 區段(每台數字不同、客戶會蓋掉) |
| D3 | 網路驅動用預設 slirp4netns |
Agent 流量單向上傳、對吞吐不敏感;A0 留數字 | pasta(實驗性,吞吐不夠才換);bypass4netns(要自訂 seccomp) |
| D4 | 出貨包含 Docker Engine 完整一組,install.sh 三段式 | 客戶拿到包就能裝比省 500 MB 重要 | 只包 rootless 附加三支 |
| D5 | 帳號名預設 guidant,設定檔 GUIDANT_RUN_USER 可改;已存在就沿用並檢查三項 |
有些客戶帳號命名有規範;撞名不能死 | 固定名稱直接 useradd |
| D6 | 三支升級 systemd 單元仍以 root 跑,docker 操作經 DOCKER_HOST 指到 guidant 的 daemon;Wants/After 都改等 user@<uid>.service |
staging 目錄只有 root 進得去、簽章驗證用的是 root 持有的現行版執行檔,整支改 user unit 會失去這層隔離 | 整支改成 guidant 的 user unit |
| D7 | 不留 root Docker 相容路徑 | 沒有舊客戶(FR-180 同一判準) | 偵測模式雙路徑 |
| D9 | docker 呼叫一律是 root 的 CLI 加 DOCKER_HOST=unix:///run/user/<uid>/docker.sock,install.sh 開頭 export 一次;不用 sudo -u guidant |
實測 sudo -u 會讓三個基本動作失敗(從映像取檔到 root 的暫存目錄、讀 staging 目錄的映像檔、讀 /run/guidant-agent/.env);daemon 仍是 guidant,容器拿到的權限沒有變多。/run/guidant-agent 因此維持 root 700 不開放 |
sudo -u guidant 包一層(v1.2) |
| D10 | 維運程式不從 guidant 的映像庫取來執行:安裝與升級時把 install-lib 存一份到 /usr/local/lib/guidant-agent/install-lib(root 755),維運子命令與升級單元讀這份;升級時新版的 lib 在驗章通過後才換上 |
rootless 後映像庫屬 guidant,root 去 source 映像裡的腳本等於執行 guidant 改得到的東西。主機副本的做法 secrets_cmds.sh 與 upgrade_order.sh 已在用 |
維持從映像讀(root Docker 時代的做法) |
| D11 | 容器內 ping 靠 compose 的容器 sysctl net.ipv4.ping_group_range: "1000 1000",cap_add: [NET_RAW] 保留;主機 sysctl 照 D2 全開 |
rootless 的容器網路不帶主機的值,容器內預設是「沒有人」;映像裡的 ping 帶檔案能力,拿掉 NET_RAW 連執行都失敗 | 拿掉 NET_RAW 只靠主機 sysctl(v1.2) |
| D8 | 終態一律官方 docker-ce,docker.io/podman-docker 不視為合格的 Docker。發行版自帶的:沒有任何容器與 volume 就移除、換裝官方組;有就停下列清單請客戶處理 |
docker.io 沒有對齊版本的 rootless 附加套件、compose plugin 另裝且版本凍結、與出貨包那組不一致出問題無從重現;判準「有沒有人在用」,有就不代客戶決定 |
一律停下(乾淨機裝了 docker.io 就卡住);一律移除(可能砍掉客戶在用的) |
| 層 | 現況 | 目標 |
|---|---|---|
| 容器裡的程式 | uid 1000(FR-180 做完) | 不動 |
| Docker 引擎 | 主機 root | 主機帳號 guidant |
| 容器 uid 在主機上的實際身分 | 1000 就是 1000 | 子 uid 起點 + 1000(如 101000),每台不同 |
| 元件 | 現況 | 本案動作 |
|---|---|---|
deploy/install.sh check_environment() |
只查 docker 在不在、版本、compose plugin | 加 cgroup v2/systemd/核心版本;Docker 三段式(沒有→從包裝;夠新→沿用補附加;太舊→停) |
deploy/install.sh require_root() |
全程 root | 不變(安裝程式整支仍 sudo 跑),但 docker 操作內部切身分 |
deploy/install.sh 全部 docker 呼叫(60 處,另 lib/ 21 處) |
root CLI 連 /var/run/docker.sock |
install.sh 開頭 export DOCKER_HOST(D9),既有呼叫不逐行改 |
deploy/install.sh:2286 AGENT_UID=1000、:2333 _storage_key_owner、:2358 prepare_agent_host_paths(product_uuid/content/cache/factory-defaults.env) |
主機側 chown 給 1000 | 全部走換算;重跑 install.sh 不得蓋回 1000 |
lib/upgrade_order.sh _UO_APP_ID=1000、:91 chown |
升級單目錄 | 走換算 |
lib/local_admin.sh:28 AGENT_UID:-1000 |
volume 修 owner | 走換算 |
lib/secrets.sh SEC_RUN_DIR 700 |
/run/guidant-agent root 700 |
不動(D9:讀它的是 root 的 CLI) |
lib/systemd/*.service 三支+secrets 單元 |
root、Wants/After/Before=docker.service |
仍 root;三個欄位都改成 user@<uid>.service;Environment=DOCKER_HOST=… |
維運子命令與升級單元取 install-lib |
source <(image_read_file …) 從映像讀 |
讀主機副本(D10) |
check_environment 的錯誤訊息與磁碟檢查 |
叫客戶 systemctl start docker、查 /var |
訊息改掉;磁碟查 guidant 的資料目錄 |
deploy/docker-compose.yml:162 cap_add: [NET_RAW] |
給 ping | 保留,另加容器 sysctl(D11) |
scripts/build/build_agent_bundle.sh |
帶 tpm2 debs | 多帶 Docker 一組(fetch_docker_debs.sh) |
| 主機 OS | 無 | 六項設定(§3.1) |
lib/rootless_host.sh(A1)install.sh 第①步環境檢查之後、寫設定之前呼叫 rootless_host_prepare,冪等,重跑無害。
stat -fc %T /sys/fs/cgroup 要 cgroup2fs;systemctl --version 有輸出;uname -r ≥ 5.11。任一不符 check_fail。dpkg -s docker-ce/rpm -q docker-ce 有=官方;dpkg -s docker.io、podman-docker 有=發行版自帶。①沒有 docker → 從包 debs/docker/<目標>/ 裝(deb 用 dpkg -i 加 apt-get -f,el-9 把目錄當本機 repo 給 dnf)。②官方且 Server.Version ≥ MIN_DOCKER_MAJOR → 只補 docker-ce-rootless-extras、uidmap、slirp4netns、dbus-user-session。③官方但太舊 → fail 印「請自行升級或移除後重跑」。④發行版自帶 → docker ps -aq 與 docker volume ls -q 都空 → 印一行「移除發行版自帶 docker.io,改裝官方 Docker Engine」後 apt-get remove -y docker.io docker-compose docker-doc(el:podman-docker),接①;任一非空 → fail 印出容器與 volume 清單,請客戶確認後自行移除再重跑。裝完 systemctl disable --now docker.service docker.socket containerd.service(containerd 是獨立單元,不停會留一支 root 行程)。GUIDANT_RUN_USER 預設 guidant。不存在 → useradd --system --create-home --shell nologin。不論新建或既有都查 subuid:Ubuntu 24.04 的 useradd --system 不配,且 usermod 不檢查重疊。沒有就補,起點=/etc/subuid 與 /etc/subgid 所有區段 start+count 的最大值(存在 → 同樣檢查、shell 是否 nologin(否則 warn 不改)、是否在 sudo/wheel 群組(是則 fail)。loginctl enable-linger。/etc/sysctl.d/90-guidant-rootless.conf 一行 net.ipv4.ping_group_range = 0 2147483647,sysctl --system。/etc/systemd/system/user@.service.d/delegate.conf(Delegate=cpu cpuset io memory pids),daemon-reload;user manager 已在跑的話 systemctl restart user@<uid>.service,等 socket 回應(最多約 30 秒)。不能只 terminate-user,linger 不會把它拉回來。machinectl shell guidant@ /bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u) dockerd-rootless-setuptool.sh install && systemctl --user enable docker'。沒有 machinectl 就裝 systemd-container。machinectl shell 不回傳內層結束碼,成敗只看這一步。以 docker info --format '{{.SecurityOptions}} {{.CgroupDriver}}' 要含 rootless 與 systemd,否則 fail。install.sh 開頭(讀到 GUIDANT_RUN_USER 之後):
RUN_UID="$(id -u "$GUIDANT_RUN_USER")"
export DOCKER_HOST="unix:///run/user/${RUN_UID}/docker.sock"之後所有 docker、dc() 原樣可用,不必逐行改。維運入口與三支升級單元是另外的行程,各自要拿到同一個值:維運入口在同一段讀安裝紀錄後 export;單元檔加 Environment=DOCKER_HOST=…(安裝時代入 uid)。守衛 scripts/install/check_docker_host.sh:每支會被獨立執行的入口都要有這一行 export。
主機副本(D10):/usr/local/lib/guidant-agent/install-lib/,root 755。首裝在映像驗完完整性後存一份;升級在 verify_new_image_tree 通過後換上新版。所有 source <(image_read_file … install-lib/…) 改讀這份。既有的 /usr/local/lib/guidant-agent-secrets 與 guidant-agent-upgrade 併進同一個目錄。
lib/subuid_map.sh(A4)subuid_base() { awk -F: -v u="$GUIDANT_RUN_USER" '$1==u{print $2; exit}' /etc/subuid; }
host_uid_for() { echo $(( $(subuid_base) + $1 )); } # host_uid_for 1000 → 101000rootless 的對映是 0 → guidant 本人、N → 起點 + N − 1(N≥1)。主機側六處走換算:_storage_key_owner、prepare_agent_host_paths 的 product_uuid/content/cache/factory-defaults.env、upgrade/agent-order、AGENT_UID 常數本身。product_uuid 設 guidant:<容器 1000 的主機 uid> 0440,服務與 fingerprint 子命令(容器內 uid 0)才都讀得到;只設群組的話兩邊算出不同指紋,主系統會多一台陌生 Agent 且沒有任何錯誤。容器內以 -u 0 跑的 chown 1000 不改(容器裡看到的仍是 1000)。重跑 install.sh 不得把擁有者蓋回 1000。
三支單元維持 User=root。Wants=docker.service 與 After=docker.service 都改成 user@<uid>.service(留著 Wants=docker.service 會在升級單觸發時把 root Docker 拉起來);secrets 單元的 Before=docker.service 同樣改。單元檔帶 Environment=DOCKER_HOST=…。staging 目錄仍 root 700,解包、驗簽在 root 做完才 docker load。升級腳本偵測到容器屬於 root Docker 時停下,印「本版起改 rootless,請 uninstall 後重新安裝」(D7)。
fetch_docker_debs.sh 照 fetch_tpm2_debs.sh:同一組目標 ubuntu-22.04 ubuntu-24.04 debian-12 el-9,在對應版本最精簡容器加 Docker 官方 repo 後 apt-get install --download-only/dnf download --resolve,輸出 debs/docker/<目標>/。釘版寫在腳本頂部一個變數,bump 走 commit。平台固定 linux/amd64。
| # | 設定 | 落在哪 | 全機生效 | uninstall 還原 |
|---|---|---|---|---|
| 1 | guidant 帳號+子 uid 65536 |
/etc/passwd、/etc/subuid、/etc/subgid |
多一個帳號 | userdel -r |
| 2 | linger | loginctl |
否 | disable-linger |
| 3 | Docker Engine+rootless 附加套件;系統層 docker/containerd 三支單元停用 | 套件、systemd | 是 | 套件不移除(客戶可能另有用途),印提示;單元恢復原狀 |
| 4 | ping_group_range |
/etc/sysctl.d/90-guidant-rootless.conf |
是 | 刪檔、sysctl --system |
| 5 | cgroup 委派 | /etc/systemd/system/user@.service.d/delegate.conf |
是 | 刪檔、daemon-reload |
| 6 | 資料目錄 owner(六處換算) | /srv/guidant-ai-agent |
否 | 隨資料目錄 |
pasta 驅動切換(A0 量出吞吐不夠才開卡)。批=可同時開 session 的一組;階段=給 PM 看的分組。A1 到 A6 都在同一台 VM(192)上實測、都改 deploy/install.sh,所以除了 A1/A2、A5/A6 兩組之外一張一批。A0 已完成。由 handoff/cards.py 產生並驗過依賴與撞檔。A8 在 BE repo(資安報告),其餘在 Agent repo。
| 卡 | 階段 | 批 | Repo | 做什麼 | reuse/extend/new+理由 | 改哪些既有檔 | 新開檔 | 依賴 | 驗收 | Model/Effort |
|---|---|---|---|---|---|---|---|---|---|---|
| A0 | P0 | 1 | agent | 乾淨機手動跑 rootless:列壞掉清單 | new:不寫程式;後面的卡按實測清單校正 | — | docs/features/FR-193-2611-docker-rootless/evidence/agent-rootless-probe.md(BE repo,證據) |
— | 證據頁有壞掉清單(訊息→原因→修法)、十二項輸出、traceroute 兩種模式結果 | sonnet/medium |
| A1 | P1 | 2 | agent | rootless 主機準備層:四段式 Docker 安裝、建帳號與子 uid、linger、sysctl、cgroup 委派、啟用 rootless | new:現有 install.sh 沒有這層,獨立一支 lib;離線裝法照 lib/tpm.sh | deploy/install.sh(check_environment 段)、scripts/build/build_agent_bundle.sh(lib 複製清單) |
scripts/install/lib/rootless_host.sh |
A0 | 把 192 還原成沒有 Docker、沒有 guidant 帳號後,單獨執行 rootless_host_prepare:六項主機設定到位、docker info 含 rootless 與 systemd、ps 無 root 的 dockerd/containerd;重跑一次無變化 | opus/high |
| A2 | P1 | 2 | agent | 出貨包帶 Docker Engine 完整一組(每個 OS 版本一份) | extend:照 BE fetch_tpm2_debs.sh;打包點是 build_compose_bundle.sh 第 396 行 tpm2 那段旁邊 | scripts/build/build_compose_bundle.sh |
scripts/build/fetch_docker_debs.sh |
A0 | 四個目標目錄各有完整套件;el-9 含 repodata;各起一個對應版本的乾淨容器離線裝得起來;image-manifest.sha256 有列 | sonnet/medium |
| A3 | P1 | 3 | agent | docker 指到 guidant 的 daemon:DOCKER_HOST 一次 export、主機準備層接進安裝流程、維運程式改讀主機副本 | extend:D9:export 一次,既有 81 處呼叫不逐行改;D10:主機副本的做法 secrets_cmds.sh 與 upgrade_order.sh 已在用,併成一個目錄 | deploy/install.sh、scripts/install/lib/upgrade_order.sh、scripts/install/lib/secrets_cmds.sh |
scripts/install/lib/host_lib.sh、scripts/install/check_docker_host.sh |
A1 | 192 上把 lib 與 install.sh 換進解開的包後 sudo ./install.sh 走到底、不帶任何環境變數;status/logs/start/stop/restart 正常;維運子命令不再從映像 source;ps 無 root 的 dockerd | opus/high |
| A4 | P1 | 4 | agent | 主機側擁有者六處換算成子 uid | new:換算獨立一支;公式 起點+N−1(A0 實測);/run/guidant-agent 不動(D9) | deploy/install.sh(AGENT_UID、_storage_key_owner、prepare_agent_host_paths) |
scripts/install/lib/subuid_map.sh |
A3 | 重跑 install.sh 兩次擁有者都是換算後的值;三顆容器 healthy;服務 log 的 device_uuid 與 fingerprint 子命令印的一致 | sonnet/medium |
| A5 | P1 | 5 | agent | 升級全程 rootless:單元的 Wants/After 與 DOCKER_HOST、升級單目錄擁有者、舊版偵測 | extend:D6:單元仍 root;用維運指令 upgrade <包> 驗,不必接主系統 | scripts/install/lib/upgrade_order.sh、scripts/install/lib/upgrade_runner.sh、scripts/install/lib/systemd/guidant-agent-upgrade-order.service、scripts/install/lib/systemd/guidant-agent-upgrade-boot-check.service、scripts/install/lib/systemd/guidant-agent-secrets.service |
— | A3、A4 | 192 上 guidant-agent-compose upgrade <同一包改版號> 換版成功;過程中 systemctl is-active docker 始終 inactive;升級單目錄容器可寫;硬重開後 boot-check 正常 | opus/high |
| A6 | P1 | 5 | agent | compose 加容器 ping sysctl;uninstall 還原主機設定 | extend:D11:NET_RAW 保留、加一行 sysctls;teardown 函式加在 A1 的 lib | deploy/docker-compose.yml、deploy/install.sh(cmd_uninstall 段)、scripts/install/lib/rootless_host.sh |
— | A3、A4 | 容器內程式 fork 出的 ping/mtu/traceroute/traceroute -I 四項都通;uninstall 後帳號、sysctl 檔、委派檔、linger 清掉,docker/containerd 單元恢復原狀 | sonnet/medium |
| A7 | P2 | 6 | agent | deploy/README:裝機改了哪些主機設定、IT 怎麼看容器、常見錯誤 | extend:底稿是 Claude Docs 手冊加 A0 證據頁第 7 節的七處修正 | deploy/README.md |
— | A5、A6 | README 新增兩節;前置需求補 cgroup v2/systemd;常見錯誤表併入;只寫現況 | sonnet/medium |
| A8 | P2 | 6 | main | 資安報告 SR-09.5 改寫為現況:Agent 端採用 rootless | extend:只改一段;依「資安報告只列我們交出去的東西」,主機設定細節留在手冊 | docs/security-report/requirements/SR-09-agent.md |
— | A5、A6 | SR-09.5 讀起來是現況;grep 不採用/原本/改判 無命中 | sonnet/medium |
| A9 | P2 | 7 | agent | 出貨包全流程總驗:新裝、重開機、升級、uninstall(需決策者先出測試包) | new:唯一需要真包與主系統的一張;測試包由決策者在 GitLab 按,小弟不自己出包 | — | docs/features/FR-193-2611-docker-rootless/evidence/agent-rootless-acceptance.md(BE repo,證據) |
A2、A7 | 192 還原成乾淨後用測試包 sudo ./install.sh 到底;audit_nonroot_stack.sh --agent 全 ✔;十二項過;主系統派掃描收結果、發更新換版成功;uninstall 清乾淨 | opus/high |
sudo ./install.sh 到底,中間不手動介入。scripts/build/audit_nonroot_stack.sh --agent 全 ✔;ps -eo user,comm | grep -E 'dockerd|containerd' 無 root。uninstall 後 grep guidant /etc/passwd /etc/subuid 空、sysctl 回預設、無 dockerd。A0 在乾淨 Ubuntu 24.04 實測後修正五處,證據在 evidence/agent-rootless-probe.md。
DOCKER_HOST。v1.2 的 sudo -u guidant 實測讓 install.sh 三個基本動作失敗。連帶 /run/guidant-agent 不必開放給 guidant。guidant 的映像庫取來以 root 執行。traceroute -I 不必改。起點 + N − 1,不是 起點 + N;換算點從三處變六處(v1.2 漏了 prepare_agent_host_paths)。Wants=docker.service 也要換。卡仍是十張,批從六變七:A3 與 A4 都改 deploy/install.sh 且共用同一台 VM,拆成前後兩批。A3、A4、A5、A6 的做法改寫;A1 到 A6 的驗收改成「把改過的檔換進 192 上解開的包」,不必每張卡出一次包;A9 的測試包由決策者出。
新增 D8 發行版自帶 Docker 的處理。決策者 2026-11-10 問「Ubuntu 內建的會不會移除」,首腦查到 v1.1 的三段式會讓裝了 docker.io 的主機撞牆:版本號過門檻被判「沿用」,但 docker.io 沒有 rootless 附加套件、與官方 extras 互斥。改四段式:發行版自帶且沒有容器沒有 volume 就移除換官方組,有就停下列清單。改到 §1 D8、§3.1 第 2 步、§6 A1 驗收;卡的批次不變。
只改 D1。PM 2026-11-10 裁示:主系統對外一律 8443,不走「預設 443 貼能力、客戶可切」的路三。理由是產品還沒出給客戶,改預設沒有相容成本;省下 rootlesskit capability 的升級重貼與主機掃描報告解釋。
這條只影響主系統(FR-193.1),Agent 這輪十張卡的內容與批次完全不變。FR-193.1 開卡時要涵蓋:
| 要改 | 在哪 | 說明 |
|---|---|---|
| 預設值 | scripts/installer/install.sh:102-103、install.conf.example:35-36、docker/production/docker-compose.yml:549-550 |
HTTPS 預設 8443;HTTP 80 整條拿掉(80 也是低埠) |
| 網址判斷 | install.sh:2671 if GUIDANT_HTTPS_PORT == 443 |
刪掉分支,SITE_URL 一律帶 port |
| http 轉址 | FE nginx.onprem.conf:134-137 的 8080 server 段 |
拿掉;compose 不再映射 80 |
| Agent 註冊網址 | Agent install.sh 問主系統網址那題 |
預設值範例改 https://<主機>:8443,驗證接受帶 port |
| 手冊 | docs/user-manual/ 八份寫 https://<主機> 的頁、docs/spec-site/current/ |
全改帶 :8443 |
| 測試 | test repo e2e-env/ |
映射改 8443 |
| 客戶防火牆說明 | 安裝手冊前置需求 | 放行 8443 取代 443 |