結論:rootless Docker 本身一次就裝起來,十二項驗證主機層全過,重開機不登入 17 秒內容器自己起來。壞的全在 Agent 安裝包:sudo ./install.sh 原樣跑在第一步就停;讓它連到 guidant 的 Docker 後走到第六步,卡在 4 個 bind 掛載的擁有者沒有換算。其中設備指紋那一條不會報錯,只會靜默算出另一個指紋。設計 v1.2 有 5 處要在 A1~A6 開工前改,見最後一節。
| 項目 | 值 |
|---|---|
| 主機 | 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 節前提逐項查 | 全符合;但 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 節) | — |
| # | 看到什麼 | 原因 | 修法(已在 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。這一條是讀腳本得知,本次照手冊順序先停了,沒有實際撞到。
| # | 看到什麼 | 原因 | 修法(已在 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 沒有多給容器權限) |
| # | 檢查 | 指令 | 實際輸出 | 結果 |
|---|---|---|---|---|
| 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。
目標 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 驗。
規則(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) |
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。/etc/shadow 對照)。A4 也就不必把 /run/guidant-agent 開給 guidant 讀。維運入口(B19)與兩支升級單元(B17)一起好。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)。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)。Wants=docker.service 也要換成 user@<uid>.service,不然會把 root Docker 拉起來(B18)。cap_add: [NET_RAW];改成加 sysctls: { net.ipv4.ping_group_range: "1000 1000" }。traceroute -I 不用改成 UDP(第 5 節)。uninstall 要記得 containerd 在 B2 被停過。useradd 不配子 uid(B3)、範例區段會重疊(B4)、containerd(B2)、overlayfs(B7)、「起點+1000」改成「起點+999」(B13),第 12 項改查 volume 路徑。fingerprint 子命令」。999:166535 0440),重跑 install.sh 會被蓋回。ping_group_range 全開;delegate.conf 已寫。/tmp/100m、~/a0/(交付包與 sink/ 收端腳本)、iperf3 套件、guidant 映像庫裡的 alpine:3.20、curlimages/curl:8.11.1、networkstatic/iperf3。這台本來就給本案拆裝用,沒清。165536 231071):alpine 以容器 root ping 不通;只開 guidant 主 gid 987 987 就通。真正發 ICMP 的是以 guidant 主 gid 執行的 slirp4netns,子 gid 區段對它沒有作用。