FR-193.A0 乾淨機手動跑 rootless:壞掉清單與實測(CM-3805)

結論:rootless Docker 本身一次就裝起來,十二項驗證主機層全過,重開機不登入 17 秒內容器自己起來。壞的全在 Agent 安裝包:sudo ./install.sh 原樣跑在第一步就停;讓它連到 guidant 的 Docker 後走到第六步,卡在 4 個 bind 掛載的擁有者沒有換算。其中設備指紋那一條不會報錯,只會靜默算出另一個指紋。設計 v1.2 有 5 處要在 A1~A6 開工前改,見最後一節。

  • 實測日期:2026-11-10(VM 時間 UTC 06:20~07:50)
  • 範圍:照決策者瘦身令只做四件事——①手冊第 2~4 節 ②以 guidant 身分起現行出貨包 ③十二項 ④traceroute 兩種模式。吞吐與 D2 的 B 選項在瘦身令之前已經量了,只放在附錄、不算驗收項。
  • 心跳連不上主系統是預期行為,不列為壞掉項。

1. 環境

項目 值
主機 192.168.50.192(guidant-ai-e2e-2),PVE VM,8 核/47 GB/98 GB
OS/核心 Ubuntu 24.04.2 LTS,6.8.0-149-generic x86_64
前提 stat -fc %T /sys/fs/cgroup → cgroup2fs;systemd 255;/dev/tpmrm0 存在
裝前 subuid jedi:100000:65536(只有這一行)
裝前 ping_group_range 1 0(沒有人能用,見 B1)
Docker 官方 docker-ce 5:29.9.3、containerd.io 2.4.2、docker-compose-plugin 5.7.0、docker-ce-rootless-extras 29.9.3、rootlesskit 3.3.0、slirp4netns 1.2.1、runc 1.5.2
Agent 包 guidant-agent-compose-1.7.4.tar.gz(188 的 build 目錄,與 Agent pyproject.toml 版號一致),sha256sum -c OK
guidant 帳號 uid 999、gid 987、子 uid/gid 165536:65536

2. 逐步紀錄

步 做了什麼 結果 花多久
① 手冊第 2 節前提逐項查 全符合;但 ping_group_range 預設是 1 0(B1) —
② 手冊第 3 節裝官方 Docker 一組,systemctl disable --now docker.service docker.socket 裝好;root 的 containerd 還在跑(B2) 112 秒
③ useradd --system --create-home --shell /usr/sbin/nologin guidant 建好,但沒配子 uid(B3);照手冊範例補 100000-165535 會跟 jedi 重疊且不報錯(B4);改用 165536 起 —
③ loginctl enable-linger guidant Linger=yes —
④ 裝 systemd-container,machinectl shell guidant@ /bin/bash -c '… dockerd-rootless-setuptool.sh install; systemctl --user enable docker' 一次成功,非互動可用 4 秒
⑤ sysctl 全開、寫 delegate.conf、daemon-reload 控制器清單立刻有五個,但執行中的 dockerd 要重起才吃到 cpuset/io(B6) —
⑥ sudo ./install.sh(原樣) 第①步停:docker 指令存在但連不上 daemon(B8) 秒級
⑥ sudo DOCKER_HOST=unix:///run/user/999/docker.sock ./install.sh(不改檔,只指 socket) 走完 ①~⑤;⑥ seaweedfs unhealthy,180 秒後退出碼 5(B10) 687 秒(含 2×1.3 GB 載入)
⑥ 卡上指令 sudo -u guidant DOCKER_HOST=… bash -c 'cd /srv/guidant-ai-agent && docker compose up -d' 變數全缺:.env 只在 /run/guidant-agent;明指 --env-file 則 permission denied(B9) —
⑥ 4 處擁有者換算成子 uid、/run/guidant-agent 開給 guidant 讀,compose up -d --force-recreate 三顆容器全 healthy 37 秒
⑥ 再跑一次 install.sh(升級路徑,帶 DOCKER_HOST) 成功結束,但把 4 處擁有者全改回 1000;容器沒重建所以當下沒壞(B12) 248 秒
⑦ 重開機,不登入 guidant,跑十二項 主機層全過;手冊第 12 項指令對錯目標(見第 4 節) 開機到兩顆 healthy 27 秒
⑧ 容器內 traceroute 兩種模式 UDP 通、ICMP 不通;ping 也不通(見第 5 節) —

3. 壞掉清單(訊息 → 原因 → 修法)

主機層(手冊與 A1)

# 看到什麼 原因 修法(已在 VM 驗過) 影響卡
B1 sysctl net.ipv4.ping_group_range → 1 0 Ubuntu 24.04 的核心預設就是「沒有人」;/usr/lib/sysctl.d 沒有任何檔設它。手冊寫「與 Ubuntu、RHEL 的預設一致」不對 照 D2 寫 /etc/sysctl.d/90-guidant-rootless.conf;手冊措辭改掉 A1、A7、A8
B2 disable docker 之後 ps 仍有 root containerd containerd.service 是獨立單元、預設 enabled,手冊只停 docker 兩支 systemctl disable --now containerd.service;十二項第 3 項才會過 A1、A7
B3 grep guidant /etc/subuid 沒有輸出 Ubuntu 24.04 的 useradd --system 不配子 uid(手冊寫「多數發行版會自動配」) usermod --add-subuids/--add-subgids 補 A1
B4 照手冊範例補 100000-165535,usermod 回 0,/etc/subuid 出現兩行 100000:65536(jedi 與 guidant) usermod 不檢查區段重疊。重疊=兩個帳號的容器共用同一批主機 uid,隔離失效 起點=/etc/subuid、/etc/subgid 所有區段 start+count 的最大值(本機 165536);手冊範例要拿掉固定數字 A1、A7
B5 machinectl shell guidant@ /bin/false 回 0 machinectl shell 不回傳內層的結束碼 A1 不能用它的退出碼判斷 setuptool 成敗,要事後 docker info 驗;另外 sudo -u guidant XDG_RUNTIME_DIR=/run/user/999 systemctl --user is-active docker 實測可用(linger 已開時),systemd-container 可能不必裝——setuptool 本身用 sudo -u 跑未驗 A1
B6 寫完 delegate.conf 後 --cpuset-cpus 印 Cpuset discarded、--device-write-bps 印 limit discarded 控制器已委派,但 dockerd 啟動時才讀;要重起 user manager 設計 §3.1 第 6 步寫 loginctl terminate-user——實測之後 linger 不會自己把 user manager 拉回來(20 秒仍 inactive,socket 消失、容器全停)。要再 systemctl start user@999.service,約 30 秒 socket 才回應。之後 cpuset 0、io.max wbps=1048576 生效 A1
B7 docker info 的 Driver 是 overlayfs,不是 overlay2 Docker 29 預設 containerd snapshotter(driver-type io.containerd.snapshotter.v1) 手冊與十二項第 5 項的判準改成「不是 vfs」 A7、A9

setuptool 腳本另有一條:root Docker 的 /var/run/docker.sock 還在跑就中止(Aborting because rootful Docker … is running,要 --force 才過),所以 A1 要先停 root Docker 才呼叫 setuptool。這一條是讀腳本得知,本次照手冊順序先停了,沒有實際撞到。

安裝包(A3、A4、A5)

# 看到什麼 原因 修法(已在 VM 驗過) 影響卡
B8 ✗ docker 指令存在但連不上 daemon——請確認 docker 服務已啟動(systemctl start docker) root 的 docker CLI 只看 /var/run/docker.sock 帶 DOCKER_HOST=unix:///run/user/<uid>/docker.sock 就過。🔴 訊息叫客戶 systemctl start docker,照做會把 root Docker 打開,正好反方向;磁碟檢查也看錯位置(/var,實際映像在 /home/guidant/.local/share/docker,帶 DOCKER_HOST 後就對了) A1、A3
B9 sudo -u guidant … docker compose --env-file /run/guidant-agent/.env … → stat /run/guidant-agent/.env: permission denied /run/guidant-agent 是 root 700、.env 600 設計的做法(目錄 750 root:guidant、.env 640)可行;host.key 同目錄,維持 600 root 就讀不到。另一條路見 B11,不必開這個目錄 A4
B10 seaweedfs 一直 health: starting → unhealthy;容器內 cat /tls/seaweedfs.key → Permission denied install.sh:2333 _storage_key_owner 寫死 chown 1000:1000;主機 uid 1000 不在 guidant 的對映內,容器裡看成 65534 chown 166535:166535(見 B13 的換算) A4
B11 以 sudo -u guidant 做 install.sh 會做的三件事都失敗;以 root CLI 指 guidant 的 socket 三件都成功(表在下方) sudo -u 之後 CLI 讀寫檔的身分變成 guidant;root 專屬的暫存目錄、staging 目錄、/run/guidant-agent 全部進不去 見下表與第 7 節第 1 條 A3、A4、A5
B12 再跑一次 install.sh:4 處擁有者全回到 1000 _storage_key_owner、prepare_agent_host_paths 每次都重設 4 處全部改走換算函式,重跑才不會蓋回去 A4
B13 主機上 ps 看到 Agent 主程式 uid 是 166535,不是手冊與設計寫的 166536 rootless 的對映是 /proc/<dockerd>/uid_map = 0 999 1 + 1 165536 65536:容器 root=guidant 本人,容器 uid N(N≥1)=起點 + N − 1 設計 §3.3 host_uid_for 要寫成 BASE + N - 1(N≥1),N=0 回 guidant 自己的 uid A4
B14 Agent healthy,log 印 device_uuid computed: c9fdd2cb…;讀得到時是 e8f2c8cb… prepare_agent_host_paths 把 host-id/product_uuid 設成 0:1000 0440,容器 uid 1000 讀不到,指紋只用 machine-id 算。沒有任何錯誤,只會讓主系統多出一台陌生 Agent 擁有者 999:166535、0440(擁有者=guidant=容器內 uid 0,群組=容器內 uid 1000)。這樣服務與 fingerprint 子命令(exec -u 0)都讀得到,兩邊都算出 e8f2c8cb…。只設 0:166535 的話服務對了,fingerprint 子命令仍讀不到,算出 c9fdd2cb… 並警告兩邊不一致 A4
B15 容器寫 /data/content/cache 失敗;讀 /factory/factory-defaults.env 失敗 同一支 prepare_agent_host_paths 的另兩處 AGENT_UID;設計 §2.2 盤點漏了這支函式(只列 install.sh:2333、upgrade_order.sh:91、local_admin.sh:28) content/cache → 166535:166535;factory-defaults.env → 0:166535 0640 A4
B16 容器寫 /upgrade/agent-order → Permission denied upgrade/agent-order 是 1000:1000 750 chown 166535:166535 後可寫 A4、A5
B17 guidant-agent-upgrade-order.service、guidant-agent-upgrade-boot-check.service 失敗:已安裝的 agent 映像不含此子命令(映像內找不到 install-lib) 單元以 root 跑、不帶 DOCKER_HOST,看不到 guidant 的映像 單元內 docker 一律走同一個入口(A3 定的那支) A5
B18 07:15:14 root 的 dockerd 與 containerd 被拉起來 guidant-agent-upgrade-order.service 有 Wants=docker.service,install.sh 建好 agent-order 時 path 單元觸發 Wants/After 改成 user@<uid>.service(設計 §3.4 只寫改 After,Wants 也要改);本次手動停回 A5
B19 sudo guidant-agent-compose status 顯示兩顆都 未建立 維運入口同樣不帶 DOCKER_HOST 隨 A3 的共用入口一起好 A3

B11 的對照表(同一台、同一個 socket,只差 CLI 用誰的身分):

動作(install.sh 裡實際會做的) sudo -u guidant(設計 §3.2 的 dk) root CLI + DOCKER_HOST
image_read_file:docker cp 到 root 的 mktemp -d(700),之後 source permission denied,取不到 install-lib 成功
docker load -i 讀 root 700 目錄裡的 tar(升級 staging) permission denied(改成 stdin 餵可以) 成功
docker compose --env-file /run/guidant-agent/.env(現行 700/600) permission denied 成功
容器 bind 掛主機 /etc/shadow(640 root:shadow)後讀 Permission denied Permission denied(daemon 一樣是 guidant,root CLI 沒有多給容器權限)

4. 十二項驗證(重開機後、未登入 guidant)

# 檢查 指令 實際輸出 結果
1 系統層 Docker 沒在跑 systemctl is-active docker(另查 docker.socket、containerd) inactive / inactive inactive ✔
2 guidant 的 Docker 開機自起 sudo machinectl shell guidant@ /bin/systemctl --user is-active docker active ✔
3 主機無 root dockerd ps -eo user,comm | grep -E 'dockerd|containerd' guidant containerd/guidant containerd-shim ×2/guidant dockerd ✔(要先做 B2,否則有一行 root containerd)
4 rootless 模式 machinectl shell guidant@ /usr/bin/docker info --format '{{.SecurityOptions}}' [name=seccomp,profile=builtin name=rootless name=cgroupns] ✔
5 儲存驅動 docker info --format '{{.Driver}} / {{.DriverStatus}}' overlayfs / [[driver-type io.containerd.snapshotter.v1]] ✔(判準改「不是 vfs」,B7)
6 cgroup 委派 {{.CgroupDriver}} {{.CgroupVersion}};user@999.service/cgroup.controllers systemd 2;cpuset cpu io memory pids ✔
7 子 uid 對映 grep guidant /etc/subuid /etc/subgid guidant:165536:65536 各一行 ✔
8 linger loginctl show-user guidant -p Linger Linger=yes ✔
9 ping sysctl sysctl net.ipv4.ping_group_range 0 2147483647 ✔
10 容器內 ping 手冊指令(alpine,目標換 188) 1 packets transmitted, 1 packets received ✔(alpine 以容器 root 跑)。Agent 容器不通,見第 5 節
11 資源上限 docker run --rm --memory 64m alpine cat /sys/fs/cgroup/memory.max 67108864;Agent 實際 memory.max=4294967296 pids.max=512、seaweedfs 2147483648/1024 ✔
12 資料目錄 owner stat -c '%U %u' /srv/guidant-ai-agent/upload No such file or directory ✘ 指令對錯目標:資料在 named volume。改查 /home/guidant/.local/share/docker/volumes/guidant-ai-agent_agent-upload/_data → 166535:166535;bind 掛載 content/cache、seaweedfs.key、upgrade/agent-order 也是 166535(手動修過,B10~B16)

另記:重開機後 /run/guidant-agent 由 secrets 單元重建為 root 700,.env 600——B9 的權限要寫在 secrets.sh load 裡(設計 §3.3 已這樣寫),不能只在安裝時做一次。開機時間線:uptime 17 秒 agent healthy、27 秒 seaweedfs healthy;loginctl list-users 只有 guidant … lingering 與 jedi。

5. 容器內 traceroute 與 ping(④)

目標 192.168.50.188。Agent 容器照現行 compose:uid 1000、cap_drop: [ALL]、cap_add: [NET_RAW]、no-new-privileges。參數照 netdiag/adapters/probes/traceroute.py、ping.py 的 build_argv。

指令 rootless(現行 compose) root Docker 對照(同一顆映像、同一組參數)
traceroute -n(UDP) ✔ 通:1 172.18.0.1 → 2 10.0.2.2 → 3 192.168.50.188 ✔ 通
traceroute -I -n(ICMP) ✘ You do not have enough privileges to use this traceroute method. socket: Operation not permitted ✔ 通
ping -n -c2 -W2 -w10 ✘ ping: socket: Operation not permitted(走 SOCK_RAW) ✔ 通(走 SOCK_DGRAM)
ping -M do -s 1472(mtu) ✘ 同上 ✔ 通

原因:容器內看到的 ping_group_range,root Docker 下是 0 2147483647,rootless 下是 65534 65534。rootless 的容器網路 namespace 不帶主機的值,uid 1000 拿不到免特權的 ICMP socket,只好走 raw socket,而 no-new-privileges 下 NET_RAW 不會生效。

修法(已驗):compose 的 agent 服務加 sysctls: { net.ipv4.ping_group_range: "1000 1000" },cap_add: [NET_RAW] 保留:

組合 ping mtu 1472 traceroute traceroute -I
現行(NET_RAW,無容器 sysctl),主機全開 ✘ ✘ ✔ ✘
NET_RAW + 容器 sysctl 1000 1000,主機全開 ✔ ✔(1473 回 message too long, mtu=1500,判斷正確) ✔ ✔
同上,主機只開 guidant 自己的 gid(987 987) ✔ ✔ ✔ ✔
同上,主機維持 Ubuntu 預設 1 0 ✘(+1 errors) ✘ ✔ 三次裡一次失敗、兩次通
拿掉 NET_RAW + 容器 sysctl ✘ /bin/sh: ping: Operation not permitted ✘ ✔ ✔

最後一列推翻設計 A6「compose 拿掉 NET_RAW」。映像裡的 /usr/bin/ping 帶檔案能力 cap_net_raw=ep,能力不在 bounding set 時連 exec 都失敗。root Docker 下拿掉 NET_RAW 結果一樣,跟 rootless 無關。

有一處沒解釋清楚:docker exec guidant-ai-agent ping … 直接執行時會通(走 SOCK_RAW),經 sh -c 或從容器內程式 fork 出來就不通。兩者讀 /proc/self/status 的 CapEff 都是 0。netdiag 是從 Agent 主程式(PID 1,CapEff 0、NoNewPrivs 1)fork 出 ping,比較接近 sh -c 那條。所以驗收要以 fork 出來的結果為準,不能拿 docker exec ping 當證據。本卡沒有接主系統,沒有從主控台真的觸發 netdiag,留給 A6/A9 驗。

6. 資安規範對照

規則(Agent .claude/rules/security/) 本次對到的事
secrets:帳密只在記憶體解開(SR-06.4) /run/guidant-agent/.env 要給 Docker CLI 讀。走 root CLI(B11)就不必開權限;若照設計開 750 root:guidant,同目錄的 host.key 必須維持 600 root。容器的環境變數(含 SEAWEEDFS_SECRET_KEY)落在 /home/guidant/.local/share/docker/containers/<id>/config.v2.json(guidant 600),能以 guidant 身分讀檔的人讀得到,跟 root Docker 下「root 讀得到」同級,資安報告 SR-09.5 要寫清楚
sync-and-identity:註冊憑證名稱等於本機指紋 B14:指紋來源讀不到時靜默變成另一個值。A4 驗收要比對「服務 log 的 device_uuid」與「fingerprint 子命令」兩邊一致
external-calls-and-tools:外部程式參數陣列、核定的網路除錯十項 第 5 節:只改 compose 的 sysctl,不改 netdiag 參數;traceroute -I 可以保留
升級單與包一律不可信(upgrade_order.sh 檔頭) rootless 之後映像庫屬 guidant(/home/guidant/.local/share/docker,710/700)。install.sh 以 root source <(image_read_file …) 載入 install-lib,等於 root 執行 guidant 可改的東西:拿到 guidant 身分的人可以換映像、進而以 root 執行。root Docker 時代沒有這條路。A3/A5 要決定怎麼補(例如只從已驗章的主機副本 source)

7. 對後面九張卡的建議修正

  1. A3(要決策者拍板)
    • 問題:設計 §3.2 的 dk() 用 sudo -u guidant,install.sh 的三個基本動作會壞(B11):從映像取 install-lib、升級 staging 的 docker load、--env-file。
    • 建議:dk() 改成 root CLI + DOCKER_HOST=unix:///run/user/$RUN_UID/docker.sock。
    • 理由:本次 install.sh 原樣帶這個變數就跑完 ①~⑤;daemon 仍是 guidant、容器能力沒有變多(/etc/shadow 對照)。A4 也就不必把 /run/guidant-agent 開給 guidant 讀。維運入口(B19)與兩支升級單元(B17)一起好。
  2. A4:host_uid_for 寫成 BASE + N - 1(N≥1),N=0 回 guidant 的 uid(B13)。要換算的地方從 3 處擴成 6 處:_storage_key_owner、prepare_agent_host_paths 的 product_uuid/content/cache/factory-defaults.env、upgrade/agent-order、local_admin.sh:28。product_uuid 用 guidant:BASE+999 0440(B14)。重跑 install.sh 不能把擁有者蓋回 1000(B12)。
  3. A1:停 root Docker 時連 containerd.service 一起停(B2);子 uid 起點自己算、不能沿用 usermod 的預設,也不能用固定數字(B3、B4);委派後要 systemctl start user@<uid>.service 並等 socket(最多約 30 秒),不能只 terminate-user(B6);不能用 machinectl shell 的退出碼判斷成敗(B5);check_environment 的「請 systemctl start docker」訊息要改(B8)。
  4. A5:三支單元的 Wants=docker.service 也要換成 user@<uid>.service,不然會把 root Docker 拉起來(B18)。
  5. A6:不要拿掉 cap_add: [NET_RAW];改成加 sysctls: { net.ipv4.ping_group_range: "1000 1000" }。traceroute -I 不用改成 UDP(第 5 節)。uninstall 要記得 containerd 在 B2 被停過。
  6. D2:主機的值要涵蓋 guidant 的主 gid,子 gid 區段沒用(附錄)。全開照樣成立;「只開 guidant 的 gid」也實測可行,要不要收緊留給決策者。
  7. A7(手冊):Ubuntu 的 ping 預設(B1)、useradd 不配子 uid(B3)、範例區段會重疊(B4)、containerd(B2)、overlayfs(B7)、「起點+1000」改成「起點+999」(B13),第 12 項改查 volume 路徑。
  8. A9:十二項第 10 項改用 Agent 容器、從容器內程式 fork 出 ping 來驗(第 5 節),並加一項「服務指紋=fingerprint 子命令」。

8. VM 現況(留給後面的卡)

  • rootless 堆疊在跑,三顆容器 healthy;B10~B16 的擁有者是手動修好的(product_uuid 為 999:166535 0440),重跑 install.sh 會被蓋回。
  • root Docker 三支單元 disabled+inactive;ping_group_range 全開;delegate.conf 已寫。
  • 量測殘留:/tmp/100m、~/a0/(交付包與 sink/ 收端腳本)、iperf3 套件、guidant 映像庫裡的 alpine:3.20、curlimages/curl:8.11.1、networkstatic/iperf3。這台本來就給本案拆裝用,沒清。

附錄:瘦身令前已量到的數字(不在驗收範圍)

  • 上傳 100 MB HTTPS(收端是同一台 VM 上的 TLS server,測的是容器網路驅動本身的上限):主機直連 0.41~0.45 秒;root Docker 0.49~0.53 秒(約 200 MB/s);rootless(slirp4netns)1.78~2.11 秒(約 50~58 MB/s)。iperf3 出站 419~466 Mbit/s。rootless 慢約 4 倍,但每秒仍有 50 MB 以上,D3(用 slirp4netns)成立。對 188:443 量不到:nginx 回 413/405,不收 body。
  • D2 B 選項(只開 guidant 的子 gid 區段 165536 231071):alpine 以容器 root ping 不通;只開 guidant 主 gid 987 987 就通。真正發 ICMP 的是以 guidant 主 gid 執行的 slirp4netns,子 gid 區段對它沒有作用。