---
title: FR-193 Docker rootless — 需求討論稿
brand: Guidant AI · **FR-193** Docker rootless
eyebrow: FR-193 · 需求討論稿 · 2026-11-10
h1: Docker 引擎改以非 root 帳號執行
subtitle: 主系統與 Agent 兩邊的安裝包都改成 rootless Docker；程式與 image 不動
lede: "FR-180 把容器裡的程式全部改成非 root，這案把開容器的 Docker 引擎本身也改成非 root。要動的全在安裝程式、維運指令與客戶主機的系統設定，後端與 Agent 程式碼不改。這份給 PM 看：做什麼、客戶會感覺到什麼、哪些事要先決定。"
chips: [{text: D1–D4 全部定案, kind: ok}, {text: Agent 先行, kind: accent}, {text: 程式碼不動, kind: ok}]
footer: 沿革見 git log；定案後落 design.md
---

## 這案在做什麼 {#what}

客戶主機上的 Docker 引擎（開容器、接網路、掛磁碟的那支常駐程式）現在是用主機最高權限 root 跑。這案把它改成用一個專用的普通帳號 `guidant` 跑，九顆主系統容器與 Agent 端三顆容器都由這個帳號開。

這是 Docker 官方正式支援的執行模式，官方名稱就叫 Rootless mode。不是外掛、不是自製方案。

**為什麼要做**：FR-180 做完後，容器裡的程式已經不是 root，但萬一有人靠核心漏洞從容器逃出來，碰到的 Docker 引擎還是 root，等於拿到整台主機。改成 rootless 後，逃出來拿到的只是 `guidant` 這個普通帳號，動不了主機其他東西。資安稽核問「Docker 有沒有 rootless」時，答案從「沒有、理由如下」變成「有」。

**不做什麼**：

- 容器裡面不動。FR-180 做好的 `USER 1000`、`cap_drop`、`no-new-privileges` 全部沿用。
- 後端與 Agent 的程式碼不動（查過：兩邊都沒有程式在叫 docker、沒有依賴主機 uid）。
- 不保留「root Docker」的舊安裝路徑。照 FR-180 慣例，沒有舊客戶就不留相容。
- 不用 userns-remap（Docker 另一個官方設定，引擎仍是 root 只對映容器身分）。決策者 2026-11-10 裁示走完整 rootless。

## 做完之後客戶會感覺到什麼 {#customer}

這一節是 PM 最需要知道的。

::: grid2
::: {.card .ok}
#### 不變的
客戶打 `https://主機` 進站，不帶 port。登入、上傳、掃描、報表全部一樣。升級照樣 `sudo ./install.sh --upgrade`。
:::
::: {.card .warn}
#### 會變的
主機上多一個 `guidant` 系統帳號。客戶 IT 以 root 下 `docker ps` 會是空的（容器屬於 `guidant` 的 Docker），要改用 `guidantai status`／`guidantai logs`，或切到 `guidant` 帳號再下 docker 指令。手冊要明寫，否則客戶第一反應是「服務沒起來」。
:::
::: {.card .warn}
#### 裝機改了哪些主機設定
安裝程式會動六項主機 OS 設定（見 §4），客戶 IT 會問。全部自動做、移除時自動還原，但要列在手冊讓他們知道。
:::
::: {.card .crit}
#### 效能
rootless 的容器網路走使用者態 TCP/IP 堆疊，Docker 官方自己標「慢」。大檔上傳、Agent 回傳掃描結果的吞吐會降。第一棒要量實際數字，看是否要換官方列的較快驅動。
:::
:::

## 技術上要動哪些地方 {#changes}

程式碼與 image 零改動。要動的分四層。

### 3.1 客戶主機 OS 設定（安裝程式自動做）

| # | 設定 | 為了什麼 | 不做會怎樣 |
|---|---|---|---|
| 1 | 建 `guidant` 系統帳號，並在 `/etc/subuid`、`/etc/subgid` 配一段子 uid（65536 個） | rootless Docker 的前提，容器內身分對映到這段 | rootless Docker 裝不起來 |
| 2 | 開啟 `guidant` 的 linger（`loginctl enable-linger`） | 沒人登入時這個帳號的 Docker 也要開機自起 | 重開機後服務不回來，要有人登入才起 |
| 3 | 給 `rootlesskit` 執行檔加 `CAP_NET_BIND_SERVICE` | 讓 `guidant` 的 Docker 能綁 443 | 容器起不來，報 cannot expose privileged port |
| 4 | sysctl `net.ipv4.ping_group_range=0 2147483647` | 讓容器裡的 ping 能開 ICMP socket | Agent 的 ping 永遠「無法連線」，網路診斷誤判 |
| 5 | cgroup v2 委派檔 `/etc/systemd/system/user@.service.d/delegate.conf` | compose 的記憶體與行程數上限才生效 | 上限全部靜默失效，吃爆記憶體拖垮主機 |
| 6 | 裝 `uidmap`、`slirp4netns`（或 `pasta`）、`dbus-user-session` 套件 | 對映工具與使用者態網路 | rootless Docker 裝不起來 |

第 3 項有兩個官方做法：改 sysctl `ip_unprivileged_port_start`（全機所有帳號都能綁低 port）或只給 `rootlesskit` 這支執行檔加能力。建議後者，影響範圍只有我們自己，客戶資安問起好答。第 4 項放寬的只是「核心代填的 ICMP echo」，無法偽造來源、無法嗅探，且 Ubuntu 與 RHEL 預設本來就是全開，等於把 Debian 對齊到其他發行版的預設。

這六項在 Agent 端的被掃主機上要再做一次。

### 3.2 主系統安裝程式與維運指令（BE repo）

- **建帳號、裝 rootless Docker、設開機自起**：新增一整段，現在完全沒有。離線安裝包預帶 Docker Engine 完整一組加 rootless 附加套件（D4 定案，照 tpm2-tools 的先例，每個支援 OS 版本一份），install.sh 依主機有無 Docker 三段式處理。
- **137 處 docker 呼叫改身分**：安裝程式 83 處、`guidantai` 54 處，每處改成以 `guidant` 帳號、指向它的 socket 執行。做法是改一支共用函式讓所有呼叫走它，不逐行改。
- **`require_root` 拆段**：建目錄、裝 systemd 單元、碰 TPM 仍要 root；起容器、看狀態、看 log 切到 `guidant`。
- **`/srv/guidant-ai` 底下目錄 owner 換算**：現在 chown 給 999（資料庫）與 1000（應用），rootless 下要改成「`guidant` 的子 uid 起點 + 999／1000」，每台主機的數字可能不同，安裝程式要讀 `/etc/subuid` 算。算錯的症狀是容器起不來或寫入靜默失敗、log 只有 permission denied。
- **`/run/guidant` 解密設定目錄**：現在 root 700，`guidant` 的 Docker 要讀得到才能把 `guidant.env` 掛進容器。密碼檔仍只有 root 與 `guidant` 能讀。
- **三支 systemd 單元**：`guidant-secrets`（要碰 TPM，留 root，但解完的檔要給 `guidant`）、`guidant-upgrade-boot-check`、`guidant-upgrade-order`（兩支都叫 docker，要能叫到 `guidant` 的 Docker）。
- **升級與回滾流程整條重驗**：邏輯不改，但每一步叫 docker 的身分變了，要從舊版帶資料升到新版跑一遍。
- **`guidantai uninstall` 補還原**：sysctl 檔、委派檔、linger、帳號、subuid 一起清掉。

### 3.3 Agent 安裝程式（Agent repo）

- 同 3.2 的建帳號、裝 rootless Docker、106 處 docker 呼叫、`require_root` 拆段、owner 換算、`/run/guidant-agent`。
- compose 的 `cap_add: [NET_RAW]` 拿掉：rootless 下 `cap_add` 只在容器自己的 namespace 內有效（官方文件明寫），這行不會報錯但沒有作用。ping 改靠主機 sysctl 第 4 項。
- 程式唯一要看的一處：網路診斷的 traceroute 在某情況下會加 `-I` 走 ICMP 模式，那需要 raw socket。rootless 下要實測會不會壞，壞就只走預設 UDP 模式。

### 3.4 測試、文件、資安報告

- 190 測試機整台重裝成 rootless 跑 e2e 回歸（test repo）。
- 安裝手冊與維運手冊：新增「裝機改了哪些主機設定」「客戶 IT 怎麼看容器」兩節。
- 資安報告 SR-09.5「不採用 rootless docker」整段改寫成「已採用」。

## 主機系統需求 {#requirements}

rootless 要 cgroup v2 加 systemd 才有資源上限。目前支援清單 Ubuntu 22.04 以上、Debian 12 以上、RHEL 9 以上剛好都符合，門檻不用改。

## 決策定案 {#decisions}

四項全部定案（2026-11-10）。Agent 先出安裝包、主系統後續；D1 只有主系統要面對。

::: {.callout .decided}
**D1 主系統對外一律 8443（PM 2026-11-10 定案，取代同日稍早的路三）**

不貼 `rootlesskit` 能力、不改 sysctl，compose 對外只映射 8443；80 的 http 轉址拿掉。客戶網址 `https://<主機>:8443`。理由：還沒出給客戶，改預設沒有相容成本；省掉 capability 升級重貼與主機掃描解釋。被排除：預設 443 貼能力（路三）；全機 sysctl。只影響主系統（FR-193.1），Agent 對外本來就是 8444。
:::

::: {.callout .decided}
**D2 ping sysctl 全開 `0 2147483647`（決策者 2026-11-10 定案）**

與 Ubuntu、RHEL 出廠預設相同，等於把 Debian 對齊主流。放寬的只有核心代填的 ICMP echo，無法偽造來源、無法嗅探。被排除：只開 `guidant` 子 gid 區段——每台數字不同、客戶日後動 sysctl 可能蓋掉；客戶明確要求時再改。
:::

::: {.callout .decided}
**D3 網路驅動用預設 `slirp4netns`（決策者 2026-11-10 定案）**

Agent 流量是單向上傳掃描結果，對吞吐不敏感。第一棒在乾淨機量上傳 100 MB 的秒數留紀錄，真的影響到才換 `pasta`。`bypass4netns` 要自訂 seccomp，不考慮。
:::

::: {.callout .decided}
**D4 離線安裝包連 Docker Engine 本體一起包（決策者 2026-11-10 定案）**

離線包內含 Docker Engine 完整一組（docker-ce、cli、containerd、compose plugin、rootless extras、uidmap、slirp4netns 與相依），每個支援的 OS 版本一份，照 tpm2-tools 先例由 `fetch_docker_debs.sh` 抓。安裝包約多 400 到 500 MB。

install.sh 三段式處理：①主機沒有 Docker → 從離線包裝完整一組；②主機已有 Docker 且版本不低於我們釘的最低版 → 沿用，只補裝 rootless 附加套件；③主機 Docker 太舊 → 停下印訊息請客戶自行升級或移除，不代客戶動既有 Docker。

定案理由：客戶拿到包就能裝、不必先自己裝 Docker，這個理由比體積重要。釘版對落地版客戶是優點，底層不會被自動更新。代價是 SBOM 與弱點掃描範圍多了 Docker 本體，有 CVE 要跟著出包。

被排除：只包 rootless 附加三支、Docker 本體客戶自備——省 500 MB，但客戶要先查怎麼裝 Docker 官方套件庫，離線主機更麻煩。
:::

## 怎麼開始 {#plan}

**順序：Agent 先出、主系統後續**（決策者 2026-11-10 裁示）。Agent 安裝程式較小、沒有資料庫、沒有 443 問題，先拿它把 rootless 的坑踩完；主系統照抄做法。Agent 的拆卡在 [design.md](design.md)。

第一棒不寫程式。乾淨的 Ubuntu 24.04 一台，手動照官方文件裝 rootless Docker，把現有安裝包用 `guidant` 帳號裝一次，記錄每一步壞在哪、怎麼修好的，順便量網路吞吐、驗 D1 到 D3。產出是一份「實際壞了什麼」的清單。

有了清單再開後面的卡。這樣卡是按實測開，不是按這份文件的推測開。預估整案 15 到 20 張卡，跨 BE、Agent、test 三個 repo，Agent 端的卡等主系統那套做法定了再開。

```{.mermaid cap="圖 1 — 第一棒之後的大致順序"}
%%{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
  A[第一棒<br>乾淨機手動裝<br>列壞掉清單] --> B[主系統<br>安裝程式改 rootless]
  B --> C[主系統<br>升級與回滾重驗]
  B --> D[Agent<br>安裝程式改 rootless]
  C --> E[190 重裝<br>e2e 回歸]
  D --> E
  E --> F[手冊<br>資安報告]
```
