FR-193 · design v1.1 · 2026-11-10
主機準備層、docker 呼叫切身分、uid 換算、升級重驗;主系統後續另拆
Agent 安裝包改成以專用帳號 guidant 跑 rootless Docker。程式碼與 image 不動,改動全在 deploy/install.sh 與 scripts/install/lib/。十張卡六批,第一棒乾淨機手動踩坑,第二批起改安裝程式。
本版已被 design-v1.2.md 取代(新增 D8 發行版自帶 Docker 處理)。此檔凍結不改。
整案一句話: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 操作切 guidant |
staging 目錄只有 root 進得去、簽章驗證用的是 root 持有的現行版執行檔,整支改 user unit 會失去這層隔離 | 整支改成 guidant 的 user unit |
| D7 | 不留 root Docker 相容路徑 | 沒有舊客戶(FR-180 同一判準) | 偵測模式雙路徑 |
| 層 | 現況 | 目標 |
|---|---|---|
| 容器裡的程式 | 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 dc() |
docker compose --env-file … -f … |
加 sudo -u guidant 與 DOCKER_HOST;另補 dk() 包 77 處 bare docker |
deploy/install.sh:2333 chown 1000:1000 |
seaweedfs 私鑰 | 走換算 |
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 |
750 root:guidant、.env 640 |
lib/systemd/*.service 三支 |
root、After=docker.service |
仍 root;After 改等 user@<uid>.service;內部 docker 走 dk() |
deploy/docker-compose.yml:162 cap_add: [NET_RAW] |
給 ping | 拿掉(rootless 下無效);ping 靠 sysctl |
netdiag/adapters/probes/traceroute.py:30 -I |
ICMP 模式 | 依 A0 實測:不通就不加 -I |
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。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 印「請自行升級或移除後重跑」。裝完 systemctl disable --now docker.service docker.socket。GUIDANT_RUN_USER 預設 guidant。不存在 → useradd --system --create-home --shell nologin。存在 → 查 subuid 有無(無則 usermod --add-subuids)、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;若 guidant 的 user manager 已在跑,loginctl terminate-user 讓它重起。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。dk info --format '{{.SecurityOptions}} {{.CgroupDriver}}' 要含 rootless 與 systemd,否則 fail。新增兩個共用函式,所有 docker 操作只准走它們:
RUN_UID="$(id -u "$GUIDANT_RUN_USER")"
dk() { sudo -u "$GUIDANT_RUN_USER" XDG_RUNTIME_DIR="/run/user/$RUN_UID" DOCKER_HOST="unix:///run/user/$RUN_UID/docker.sock" docker "$@"; }
dc() { dk compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }陷阱:sudo -u 不帶起 user manager,但 daemon 已由 linger 常駐,所以這裡用 sudo -u 足夠;只有 dockerd-rootless-setuptool.sh install 那一次要 machinectl shell。ENV_FILE 在 /run/guidant-agent,guidant 要讀得到(A4)。守衛:scripts/install/lib/README.md 加一行規則,並補一支 test/check_no_bare_docker.sh grep 整個 install 樹。
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 → 101000三處 chown 改成 chown "$(host_uid_for 1000):$(host_uid_for 1000)"。local_admin.sh 走 docker run -u 0 在容器內 chown 1000 的那條不用改(容器內看到的仍是 1000,對映由引擎做)。
/run/guidant-agent:secrets.sh load 結尾 chown root:$GUIDANT_RUN_USER、chmod 750、.env 640。這是 tmpfs 每次開機重建,所以放在 load 裡不放 install。
三支單元維持 User=root。After=docker.service 改 After=user@$RUN_UID.service(install 時代入)。upgrade_order.sh 第 0 步的 docker load/docker tag/_uo_tags 與 upgrade_runner.sh 的 dc_up/docker images 全走 dk()。staging 目錄仍 root 700,解包、驗簽在 root 做完才交給 dk load。
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 附加套件 | 套件 | 是 | 不移除(客戶可能另有用途),印提示 |
| 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、/run/guidant-agent 權限 |
/srv/guidant-ai-agent、tmpfs |
否 | 隨資料目錄 |
pasta 驅動切換(A0 量出吞吐不夠才開卡)。批=可同時開 session 的一組;階段=給 PM 看的分組。由 handoff/cards.py 產生並驗過依賴與撞檔。A8 在 BE repo(資安報告),其餘在 Agent repo。
| 卡 | 階段 | 批 | Repo | 做什麼 | reuse/extend/new+理由 | 改哪些既有檔 | 新開檔 | 依賴 | 驗收 | Model/Effort |
|---|---|---|---|---|---|---|---|---|---|---|
| A0 | P0 | 1 | agent | 乾淨機手動跑 rootless:列壞掉清單、量吞吐、驗 D2 可行性 | new:第一棒不寫程式;後面的卡按實測清單校正,不按推測開 | — | docs/features/FR-193-2611-docker-rootless/evidence/agent-rootless-probe.md(BE repo,證據) |
— | 乾淨 Ubuntu 24.04 以 guidant 帳號 rootless Docker 手動裝現有 Agent 包,逐步記錄壞在哪、怎麼修;量上傳 100MB 秒數;驗 traceroute -I 與 ping;十二項驗證清單各項結果;產出證據頁 | opus/high |
| A1 | P1 | 2 | agent | rootless 主機準備層:建帳號、子 uid、linger、sysctl、cgroup 委派、附加套件;三段式 Docker 安裝 | new:現有 install.sh 完全沒有這層,獨立一支 lib 由 install.sh source;Docker 三段式判斷放這裡 | deploy/install.sh(check_environment/write_config 段) |
scripts/install/lib/rootless_host.sh |
A0 | 乾淨機 sudo ./install.sh --check-only 列出六項主機設定狀態;實裝後 guidant 帳號、subuid、Linger=yes、ping_group_range、delegate.conf、docker info 顯示 rootless 全部符合;已有新版 Docker 的主機不被動到;太舊 Docker 停下印訊息 | opus/high |
| A2 | P1 | 2 | agent | 出貨包帶 Docker Engine 完整一組(每個 OS 版本一份) | extend:照 fetch_tpm2_debs.sh 先例,目標清單與 tpm2 同一組;build_agent_bundle 多一步呼叫 | scripts/build/build_agent_bundle.sh |
scripts/build/fetch_docker_debs.sh |
A0 | 四個目標目錄各有 docker-ce/cli/containerd/compose-plugin/rootless-extras/uidmap/slirp4netns 與相依;el-9 含 repodata;在對應版本乾淨容器內離線 dpkg -i/dnf 裝得起;包體積增量寫進 README | sonnet/medium |
| A3 | P1 | 3 | agent | docker 呼叫全部切 guidant 身分:共用函式、require_root 拆段、77 處 bare docker 收攏 | extend:dc() 已是共用入口,補一支 dk() 包 bare docker 並以 guidant socket 執行;不逐行改身分 | deploy/install.sh、scripts/install/lib/upgrade_order.sh、scripts/install/lib/upgrade_runner.sh、scripts/install/lib/upgrade_guard.sh、scripts/install/lib/local_admin.sh、scripts/install/lib/image_verify.sh、scripts/install/lib/enroll.sh |
— | A1 | grep 整個 install 樹找不到未經 dk()/dc() 的 docker 呼叫(守衛腳本);以 root 跑 install.sh 起來的容器 docker top 無 root、owner 是 guidant;status/logs/start/stop/restart 五個子命令正常 | opus/high |
| A4 | P1 | 3 | agent | chown 1000 全部換算成子 uid;/run/guidant-agent 讓 guidant 讀得到 | new:換算函式獨立一支讓 A3 之外的檔也能用;secrets 服務解完改 750 root:guidant | scripts/install/lib/secrets.sh、scripts/install/lib/secrets_cmds.sh、scripts/install/lib/systemd/guidant-agent-secrets.service |
scripts/install/lib/subuid_map.sh |
A1 | install.sh 2333、upgrade_order.sh 91、local_admin.sh 28 三處 chown 走換算;/run/guidant-agent 開機後 750 root:guidant、.env 640;容器能寫 upload/agent-order;重開機後 Agent 自起 | sonnet/medium |
| A5 | P1 | 4 | agent | 升級與回滾全程 rootless:三支 systemd 單元內的 docker 切身分、從舊版帶資料升級重驗 | extend:單元仍以 root 跑(staging 目錄與簽章驗證要 root),裡面 docker load/tag/compose 走 A3 的 dk() | 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 |
— | A3、A4 | 主系統發更新指令 → Agent 寫單 → 主機單元接手 → 新版起來;中途拔電重開機 boot-check 退回舊版;進度檔與 log.gz 上傳正常 | opus/high |
| A6 | P1 | 4 | agent | compose 拿掉 NET_RAW;traceroute ICMP 模式依 A0 實測結果調整;uninstall 補還原 | extend:三件小事同一人做:都是 A0 清單上的收尾項 | deploy/docker-compose.yml、guidant_agent/netdiag/adapters/probes/traceroute.py、deploy/install.sh(cmd_uninstall 段) |
— | A3、A4 | compose 無 cap_add;netdiag 的 ping/traceroute/mtu 在 rootless 容器內跑出結果;uninstall 後 guidant 帳號、sysctl 檔、delegate.conf、linger 全清,grep 無殘留 | sonnet/medium |
| A7 | P2 | 5 | agent | deploy/README 與安裝手冊:裝機改了哪些主機設定、IT 怎麼看容器、常見錯誤 | extend:以 Claude Docs 那份 Agent 手冊為底稿,拿掉過渡期手動段 | deploy/README.md |
— | A5、A6 | README §1 前置需求補 cgroup v2/systemd;新增「rootless 之後 docker ps 為何是空的」「裝機改了六項主機設定」兩節;常見錯誤表併入 §10 | sonnet/medium |
| A8 | P2 | 5 | main | 資安報告 SR-09.5 改寫為「Agent 端已採用 rootless」,主系統待續 | extend:原段落「不採用」三層表改成 Agent 已做第三層、主系統預計;ping sysctl 與 capability 的資安說明放這 | docs/security-report/requirements/SR-09-agent.md |
— | A5、A6 | SR-09.5 讀起來是現況不是沿革;grep 原本/改判/第 N 棒 無命中 | sonnet/medium |
| A9 | P2 | 6 | agent | 乾淨機用出貨包全流程總驗:新裝、重開機、升級、回滾、uninstall | new:拿 A2 出的真包在乾淨 Ubuntu 24.04 與 Debian 12 各跑一次,十二項驗證清單+audit_nonroot_stack.sh --agent | — | docs/features/FR-193-2611-docker-rootless/evidence/agent-rootless-acceptance.md(BE repo,證據) |
A2、A7 | 兩個 OS 各一份證據頁;audit_nonroot_stack.sh --agent 全 ✔ 且主機 ps 無 root dockerd;主系統端收到心跳、派掃描、收結果正常 | 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。只改 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 |